F#, FParsec и извикване на анализатор на поток рекурсивно, втори дубл

Благодаря ви за отговорите на първата ми публикация и втората ми публикация за този проект. Този въпрос по същество е същият въпрос като първия, но с моя код, актуализиран според обратната връзка, получена по тези два въпроса. Как да извикам моя анализатор рекурсивно?

Почесвам се по главата и се взирам безизразно в кода. Нямам идея накъде да отида от тук. Тогава се обръщам към stackoverflow.

Включих в коментарите на кода грешките по време на компилиране, които получавам. Един препъникамък може да бъде дискриминираният ми съюз. Не съм работил много с дискриминирани синдикати, така че може да използвам моя неправилно.

Примерният POST, с който работя, части от който включих в предишните си два въпроса, се състои от една граница, която включва втора публикация с нова граница. Този втори пост включва няколко допълнителни части, разделени от втората граница. Всяка от тези няколко допълнителни части е нова публикация, състояща се от заглавки и XML.

Моята цел в този проект е да изградя библиотека, която да се използва в нашето C# решение, като библиотеката приема поток и връща POST, анализиран на заглавки и части рекурсивно. Наистина искам F# да блесне тук.

namespace MultipartMIMEParser

open FParsec
open System.IO

type Header = { name  : string
              ; value : string
              ; addl  : (string * string) list option }

type Content = Content of string
             | Post of Post list
and Post = { headers : Header list
           ; content : Content }

type UserState = { Boundary : string }
  with static member Default = { Boundary="" }

module internal P =
  let ($) f x = f x
  let undefined = failwith "Undefined."
  let ascii = System.Text.Encoding.ASCII
  let str cs = System.String.Concat (cs:char list)

  let makeHeader ((n,v),nvps) = { name=n; value=v; addl=nvps}

  let runP p s = match runParserOnStream p UserState.Default "" s ascii with
                 | Success (r,_,_) -> r
                 | Failure (e,_,_) -> failwith (sprintf "%A" e)

  let blankField = parray 2 newline

  let delimited d e =
      let pEnd = preturn () .>> e
      let part = spaces
                 >>. (manyTill
                      $ noneOf d
                      $ (attempt (preturn () .>> pstring d)
                                  <|> pEnd)) |>> str
       in part .>>. part

  let delimited3 firstDelimiter secondDelimiter thirdDelimiter endMarker =
      delimited firstDelimiter endMarker
      .>>. opt (many (delimited secondDelimiter endMarker
                      >>. delimited thirdDelimiter endMarker))

  let isBoundary ((n:string),_) = n.ToLower() = "boundary"

  let pHeader =
      let includesBoundary (h:Header) = match h.addl with
                                        | Some xs -> xs |> List.exists isBoundary
                                        | None    -> false
      let setBoundary b = { Boundary=b }
       in delimited3 ":" ";" "=" blankField
          |>> makeHeader
          >>= fun header stream -> if includesBoundary header
                                   then
                                     stream.UserState <- setBoundary (header.addl.Value
                                                                      |> List.find isBoundary
                                                                      |> snd)
                                     Reply ()
                                   else Reply ()

  let pHeaders = manyTill pHeader $ attempt (preturn () .>> blankField)

  let rec pContent (stream:CharStream<UserState>) =
      match stream.UserState.Boundary with
      | "" -> // Content is text.
              let nl = System.Environment.NewLine
              let unlines (ss:string list) = System.String.Join (nl,ss)
              let line = restOfLine false
              let lines = manyTill line $ attempt (preturn () .>> blankField)
               in pipe2 pHeaders lines
                        $ fun h c -> { headers=h
                                     ; content=Content $ unlines c }
      | _  -> // Content contains boundaries.
              let b = "--" + stream.UserState.Boundary
              // VS complains about pContent in the following line: 
              // Type mismatch. Expecting a
              //    Parser<'a,UserState>
              // but given a
              //    CharStream<UserState> -> Parser<Post,UserState>
              // The type 'Reply<'a>' does not match the type 'Parser<Post,UserState>'
              let p = pipe2 pHeaders pContent $ fun h c -> { headers=h; content=c }
               in skipString b
                  >>. manyTill p (attempt (preturn () .>> blankField))
                  // VS complains about Content.Post in the following line: 
                  // Type mismatch. Expecting a
                  //     Post list -> Post
                  // but given a
                  //     Post list -> Content
                  // The type 'Post' does not match the type 'Content'
                  |>> Content.Post

  // VS complains about pContent in the following line: 
  // Type mismatch. Expecting a
  //    Parser<'a,UserState>    
  // but given a
  //    CharStream<UserState> -> Parser<Post,UserState>
  // The type 'Reply<'a>' does not match the type 'Parser<Post,UserState>'
  let pStream = runP (pipe2 pHeaders pContent $ fun h c -> { headers=h; content=c })


type MParser (s:Stream) =
  let r = P.pStream s

  let findHeader name =
    match r.headers |> List.tryFind (fun h -> h.name.ToLower() = name) with
    | Some h -> h.value
    | None   -> ""

  member p.Boundary =
    let header = r.headers
                 |> List.tryFind (fun h -> match h.addl with
                                           | Some xs -> xs |> List.exists P.isBoundary
                                           | None    -> false)
     in match header with
        | Some h -> h.addl.Value |> List.find P.isBoundary |> snd
        | None   -> ""
  member p.ContentID = findHeader "content-id"
  member p.ContentLocation = findHeader "content-location"
  member p.ContentSubtype = findHeader "type"
  member p.ContentTransferEncoding = findHeader "content-transfer-encoding"
  member p.ContentType = findHeader "content-type"
  member p.Content = r.content
  member p.Headers = r.headers
  member p.MessageID = findHeader "message-id"
  member p.MimeVersion = findHeader "mime-version"

РЕДАКТИРАНЕ

В отговор на обратната връзка, която получих досега (благодаря!), Направих следните корекции, като получих анотираните грешки:

let rec pContent (stream:CharStream<UserState>) =
    match stream.UserState.Boundary with
    | "" -> // Content is text.
            let nl = System.Environment.NewLine
            let unlines (ss:string list) = System.String.Join (nl,ss)
            let line = restOfLine false
            let lines = manyTill line $ attempt (preturn () .>> blankField)
             in pipe2 pHeaders lines
                      $ fun h c -> { headers=h
                                   ; content=Content $ unlines c }
    | _  -> // Content contains boundaries.
            let b = "--" + stream.UserState.Boundary
            // The following complaint is about `pContent stream`:
            // This expression was expected to have type
            //     Reply<'a>    
            // but here has type
            //     Parser<Post,UserState>
            let p = pipe2 pHeaders (fun stream -> pContent stream) $ fun h c -> { headers=h; content=c }
             in skipString b
                >>. manyTill p (attempt (preturn () .>> blankField))
                // VS complains about the line above:
                // Type mismatch. Expecting a
                //     Parser<Post,UserState>    
                // but given a
                //     Parser<'a list,UserState>    
                // The type 'Post' does not match the type ''a list'

// See above complaint about `pContent stream`. Same complaint here.
let pStream = runP (pipe2 pHeaders (fun stream -> pContent stream) $ fun h c -> { headers=h; content=c })

Опитах да вмъкна Reply ()s, но те просто върнаха анализатори, което означава, че c по-горе стана Parser<...>, а не Content. Това изглежда беше крачка назад или поне в грешната посока. Все пак признавам невежеството си и приветствам корекцията!


person Jeff Maner    schedule 12.11.2014    source източник
comment
Изглежда, че искате да дефинирате pContent като функция за анализатор, т.е. като функция, която връща стойност Reply, но вместо това връщате функции за анализатор и на двата клона.   -  person Stephan Tolksdorf    schedule 12.11.2014
comment
@StephanTolksdorf Опитах да вмъкна Reply (), но c след това промени типа от Content на Parser<...>. Признавам невежеството си, но мисля, че това е грешна посока. Моля, поправете ме, ако греша.   -  person Jeff Maner    schedule 13.11.2014
comment
Можете да накарате кода си да се компилира, като предадете аргумента stream на pContent като аргумент на функциите за анализатор, които конструирате и в двата клона. В първия клон също трябва да обвиете стойността Post {...} в списък и след това в Content.Post. Можете бързо да видите това, като добавите изрична анотация за типа на връщания тип pContent.   -  person Stephan Tolksdorf    schedule 13.11.2014
comment
Обърнете внимание, че конструирането на функциите на анализатора в движение, както правите в pContent, може да бъде доста неефективно. Бих препоръчал да разделите анализатора на компоненти и след това да използвате createParserForwardedToRef, за да разбиете директната рекурсия. Също така бих ви препоръчал да се опитате да разберете малко как функциите на анализатора и комбинаторите работят под капака (напр. като прочетете източника или ръководството за потребителя), което трябва да ви улесни при конструирането и отстраняването на грешки в анализаторите.   -  person Stephan Tolksdorf    schedule 13.11.2014
comment
@StephanTolksdorf, благодаря за помощта. Разтърсвам Ръководството за потребителя и справочника за FParsec, но не искам да отделям твърде много време в ровене как всичко работи под капака. Може би просто е време да започнем да копаем...   -  person Jeff Maner    schedule 14.11.2014


Отговори (2)


Мога да помогна с една от грешките.

F# обикновено обвързва аргументи отляво надясно, така че трябва да използвате или скоби около рекурсивните извиквания към pContent, или обратен оператор <|, за да покажете, че искате да оцените рекурсивното извикване и да обвържете върнатата стойност.

Също така си струва да се отбележи, че <| е същият като вашия оператор $.

Content.Post не е конструктор за Post обект. Имате нужда от функция за приемане на списък с публикации и връщане на публикация. (Нещо от модула List прави ли това, от което се нуждаете?)

person Christopher Stevenson    schedule 13.11.2014
comment
1) Не виждам как добавянето на скоби около рекурсивното извикване към pContent помага. Ще поема вината и ще кажа, че съм груб. Ще уточниш ли? 2) Бях наясно с идентичността между <| и $. Идвайки от Haskell, просто мисля, че $ е по-красив и по-сбит. :) 3) Това, което ме обърква в третата ви точка, е, че VS се оплаква, че Post е Content. Така че добавям Content конструктор и той се оплаква, че Content е Post. Но не видях нищо в List, което да изскочи като полезно. Тъй като дефинирам Post като of Post list, изглежда, че трябва да има проверка на типа. - person Jeff Maner; 13.11.2014
comment
Всъщност <| и $ не са съвсем еквивалентни. F# <| е ляво-асоциативен, докато Haskell $ е дясно-асоциативен. Така че f <| g <| x е f g x, докато f $ g $ x е f (g x). - person Tarmil; 14.11.2014

Първият ми отговор беше напълно грешен, но реших да го оставя.

Типовете Post и Content се дефинират като:

type Content =
    | Content of string
    | Post of Post list
and Post =
    { headers : Header list
    ; content : Content }

Post е Запис, а Content е Дискриминиран съюз.

F# третира случаите за Дискриминирани съюзи като отделно пространство от имена от типове. Така че Content е различно от Content.Content, а Post е различно от Content.Post. Тъй като те са различни, наличието на един и същ идентификатор е объркващо.

Какво трябва да връща pContent? Ако се предполага, че връща дискриминирания съюз Content, трябва да обвиете записа Post, който връщате в първия случай, в случая Content.Post, т.е.

$ fun h c -> Post [ { headers=h
                    ; content=Content $ unlines c } ]

(F# може да заключи, че „Публикуване“ се отнася до Content.Post случай, вместо Post тип запис тук.)

person Christopher Stevenson    schedule 14.11.2014