Parser rekurencyjny i programowanie funkcyjne

Ostatnio pracowałem nad napisaniem prostego kompilatora, aby lepiej zrozumieć pojęcia kompilatora. Będąc uważnym czytelnikiem stackoverfolow, wydaje się, że istnieje konsensus, że pisanie kompilatora w języku funkcyjnym jest łatwiejsze niż imperatywne. W tym celu pomyślałem, że spróbuję zabić dwie pieczenie i napisać kompilator W F#, aby zarówno nauczyć się języka funkcjonalnego, jak i napisać kompilator w tym samym czasie.

Czytałam książkę smoka i postanowiłam zacząć z rekurencyjnym parserem opadania napisanym ręcznie w F#. Jednak Księga smoków zawiera prawie wszystkie próbki kodu w imperatywnym stylu. Na przykład funkcja match token wykonuje znaczną część swojej pracy poprzez efekt uboczny.

Więc moje pytanie brzmi, jak wyglądałoby bardziej tradycyjne funkcjonalne podejście do parsowania (tj. kilka efektów ubocznych)? Wiem, że kompilator Haskell (GHC) jest napisany w Haskell, ale byłbym wdzięczny za nieco mniejszy i łatwiejszy do zrozumienia kod próbka.

Po drugie, czy warto spróbować i przyjąć funkcjonalne podejście do parsowania, czy naprawdę na optymalizacjach do kodu pośredniego, że języki funkcyjne świecą i po prostu nie dostałem się tam jeszcze? Oznacza to, że powinienem przebrnąć przez parsowanie W F# używając imperatywnego stylu i przełączyć się na bardziej funkcjonalne podejście później?

Author: Samsdram, 2010-12-11

5 answers

Odpowiedź pochodzi z tego artykułu na blogu:

Moje pytanie brzmi więc, jak wyglądałoby bardziej tradycyjne funkcjonalne podejście do parsowania (tj. kilka efektów ubocznych)?

Wygląda na to, że trzeba oddzielić funkcje (jak w Lispie, Scheme, Standard ML, CAML, OCaml, F#) od czystości (brak efektów ubocznych, jak w Haskell) i przypadkowych cech języka (algebraiczne typy danych, dopasowywanie wzorców).

Dzięki algebraicznym typom danych, dopasowywaniu wzorców i funkcje wyższego rzędu, F# jest dobre do parsowania i Świetne do transformacji i generowania kodu, ale większość parserów produkcyjnych napisanych w F# nie jest czysta. Historycznie rodzina języków F # wywodzi się głównie z (MetaLanguages, czyli MLs), które zostały wyhodowane specjalnie dla tego rodzaju metaprogramowania.

Oto bardzo prosty zbiór wzajemnie rekurencyjnych aktywnych wzorców, które analizują i oceniają wyrażenia matematyczne złożone z pojedynczych cyfr, + - * operatorów i podrozdziały:

> let rec (|Term|_|) = function
    | Factor(e1, t) ->
        let rec aux e1 = function
          | '+'::Factor(e2, t) -> aux (e1 + e2) t
          | '-'::Factor(e2, t) -> aux (e1 - e2) t
          | t -> Some(e1, t)
        aux e1 t
    | _ -> None
  and (|Factor|_|) = function
    | '-'::Factor(e, t) -> Some(-e, t)
    | Atom(e1, '*'::Factor(e2, t)) -> Some(e1 * e2, t)
    | Atom(e, t) -> Some(e, t)
    | _ -> None
  and (|Atom|_|) = function
    | c::t when '0'<=c && c<='9' -> Some(int(string c), t)
    | '('::Term(e, ')'::t) -> Some(e, t)
    | _ -> None;;
val ( |Term|_| ) : char list -> (int * char list) option
val ( |Factor|_| ) : char list -> (int * char list) option
val ( |Atom|_| ) : char list -> (int * char list) option

Oto przykład użycia go do analizy i oceny wyrażenia:

> let (Term e) = List.ofSeq "1+2*(3-4)*-5";;
val e : int * char list = (11, [])

To czyste rozwiązanie, które używa dopasowywania wzorców na listach z aktywnymi wzorcami F#. W rzeczywistości będziesz chciał zdefiniować typ dla abstrakcyjnego drzewa składni i zwrócić wartość tego typu. To jest naprawdę proste W F#:

type expr =
  | Int of int
  | Neg of expr
  | Add of expr * expr
  | Sub of expr * expr
  | Mul of expr * expr

  static member (~-) f = Neg f
  static member (+) (f, g) = Add(f, g)
  static member (-) (f, g) = Sub(f, g)
  static member (*) (f, g) = Mul(f, g)

let rec (|Term|_|) = function
  | Factor(e1, t) ->
      let rec aux e1 = function
        | '+'::Factor(e2, t) -> aux (e1 + e2) t
        | '-'::Factor(e2, t) -> aux (e1 - e2) t
        | t -> Some(e1, t)
      aux e1 t
  | _ -> None
and (|Factor|_|) = function
  | '-'::Factor(e, t) -> Some(-e, t)
  | Atom(e1, '*'::Factor(e2, t)) -> Some(e1 * e2, t)
  | Atom(e, t) -> Some(e, t)
  | _ -> None
and (|Atom|_|) = function
  | c::t when '0'<=c && c<='9' -> Some(Int(int(string c)), t)
  | '('::Term(e, ')'::t) -> Some(e, t)
  | _ -> None

let (Term e) = List.ofSeq "1+2*(3-4)*-5"

Zauważ, że wymagana była tylko jedna drobna zmiana parsera, ponieważ AST może być również skonstruowany przy użyciu +, - oraz * operatory.

Po drugie, czy warto spróbować i przyjąć funkcjonalne podejście do parsowania, czy naprawdę na optymalizacji do pośredniego kodu, że języki funkcyjne świecą i po prostu nie dostałem się tam jeszcze?

Mówisz o czystości, nie o programowaniu funkcjonalnym. Czystość nie jest szczególnie przydatna w kontekście parsowania tekstu i w rzeczywistości może być prawdziwą przeszkodą(np. internowanie symboli jest koszmarem w Haskell). Jednak F # ma wiele innych korzyści, które czynią go dobrym dla tego zestawu problemów. W szczególności, chociaż inne języki, takie jak OCaml, mają znacznie lepsze narzędzia do parsowania, myślę, że F# jest najlepszym językiem. NET w tym kontekście.

To znaczy, czy powinienem przebrnąć przez parsowanie W F# używając imperatywnego stylu i przełączyć się na bardziej funkcjonalne podejście później?

Zależy całkowicie od tego, co chcesz uczynić funkcjonalnym. Użyłbym fslex i fsyacc z czystym kodem do konstruowania ASTs w akcjach ale zanieczyszczenia dla wszystkiego, co jest związane z Hash lub generowaniem unikalnych identyfikatorów.

Możesz docenić następujące artykuły, które napisałem na ten temat na ten blog (uwaga paywall):

    "Parsing text with Lex and Yacc" (30 września 2007).
  • "Optimizing a simple bytecode interpreter" (31 października 2007).
  • "Parser combinators "( 30 listopada 2007).
  • "Programowanie zorientowane na język: Interpreter poziomu terminowego" (31 grudnia 2007). "Language-oriented programming: Term Rewriting" (16 sierpnia 2008).
  • " Generowanie kodu w czasie wykonywania za pomocą System.Reflection.Emit "(31 sierpnia 2008).
  • "Parsing and visualizing binary Geographic Information System data" (30 listopada 2009).
 12
Author: Jon Harrop,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-07-19 21:41:25

Jedną ze strategii parsowania funkcjonalnego są monadyczne kombinatory parserów. Możesz poczytać o tym tutaj (i podążać za linkami) lub skorzystać z biblioteki typu FParsec. Nie polecam tego podejścia, jeśli dopiero uczysz się/uruchamiasz Kompilatory F#/.

Innym podejściem z F# jest użycie FsLex/FsYacc (w PowerPack ). Trochę nienawidzę technologii Lex / Yacc, więc nie polecam tego.

Myślę, że powinieneś napisać rekurencyjny porządny parser ręcznie. I nie masz silnych odczuć co do tokenizera, ale po prostu tokeninizowanie całego pliku do(n niezmiennego) list tokenów, a następnie rekurencyjne opadanie (i wykorzystanie niektórych dopasowań wzorców) jest dobrym sposobem na radzenie sobie z parsowaniem. I oczywiście, będziesz chciał użyć zniekształconych związków do reprezentowania wyjścia AST parsera (a la tutaj ).

Dawno nie czytałam księgi smoka, ale najwyraźniej jestem jedyną osobą na świecie, która jej nie lubi. Ja bym rozważ porzucenie tego tekstu na rzecz książki, która omawia Kompilatory używające jakiegoś języka opartego na ML, choć nie mogę polecić jednej z nich.

Edytuj

Dawno tego nie robiłam, więc poświęciłam kilka minut na zakodowanie małej próbki.
// AST for tiny language
type Op = 
    | Plus 
    | Minus 
type Expr = 
    | Literal of int 
    | BinaryOp of Expr * Op * Expr // left, op, right 
type Stmt =
    | IfThenElse of Expr * Stmt * Stmt // cond, then, else; 0=false in cond 
    | Print of Expr

// sample program
let input = @"
    if 1+1-1 then 
        print 42 
    else 
        print 0"

// expected AST
let goal = 
    IfThenElse(
        BinaryOp( BinaryOp(Literal(1),Plus,Literal(1)), Minus, Literal(1)), 
        Print(Literal(42)), 
        Print(Literal(0))) 

////////////////////////////////////////////////////////////////////////////
// Lexer

type Token =
    | IF
    | THEN
    | ELSE
    | PRINT
    | NUM of int  // non-negative
    | PLUS
    | MINUS
    | EOF

let makeTokenizer (s:string) =
    let i = ref 0
    let keywords = [
        "if", IF 
        "then", THEN
        "else", ELSE
        "print", PRINT
        "+", PLUS
        "-", MINUS ]
    let rec getNextToken() =
        if !i >= s.Length then
            EOF
        elif System.Char.IsWhiteSpace(s.[!i]) then
            incr i
            getNextToken()
        elif System.Char.IsDigit(s.[!i]) then
            let mutable j = !i
            while j < s.Length && System.Char.IsDigit(s.[j]) do
                j <- j + 1
            let numStr = s.Substring(!i, j - !i)
            i := j
            NUM(System.Int32.Parse(numStr)) // may throw, e.g. if > MAXINT
        else 
            let keyword = keywords |> List.tryPick (fun (kwStr,kwTok) ->
                if s.IndexOf(kwStr, !i) = !i then
                    i := !i + kwStr.Length
                    Some(kwTok)
                else
                    None)
            match keyword with
            | Some k -> k
            | None -> 
                failwith "unexpected char '%c' at position %d" s.[!i] !i
    getNextToken

let tokens = 
    let nextToken = makeTokenizer input
    let t = ref(nextToken())
    [ 
        yield !t
        while !t <> EOF do
            t := nextToken()
            yield !t
    ]

printfn "%A" tokens // sanity check our tokenizer works

/////////////////////////////////////////////////////////////////////////
// Parser

let parseExpr toks =
    match toks with
    | NUM x :: rest ->
        let mutable rest = rest
        let mutable expr = Literal x
        while rest.Head = PLUS || rest.Head = MINUS do
            let op,y,r = 
                match rest with
                | PLUS::NUM y::t -> Plus, Literal y, t
                | MINUS::NUM y::t -> Minus, Literal y, t
                | _ -> 
                    failwith "parse error in expression, expected number"
            expr <- BinaryOp(expr, op, y)
            rest <- r
        expr, rest
    | _ -> failwith "parse error in expression, expected number"
let rec parseStmt toks =
    match toks with
    | PRINT :: rest -> 
        let e,rest = parseExpr(rest)
        Print(e), rest
    | IF :: rest ->
        let e,rest = parseExpr(rest)
        match rest with
        | THEN :: rest ->
            let s1,rest = parseStmt(rest)
            match rest with
            | ELSE :: rest ->
                let s2,rest = parseStmt(rest)
                IfThenElse(e,s1,s2), rest
            | _ -> 
                failwith "parse error after if branch, espected 'else'"
        | _ -> 
            failwith "parse error after if expression, expected 'then'"
    | _ -> failwith "parse error, expected statement"
let parseProgram toks =
    let s,rest = parseStmt toks
    match rest with
    | [EOF] -> s
    | _ -> failwith "parse error after statement, expected EOF"

let p = parseProgram tokens
printfn "%A" p
assert( p = goal )

(Mam nadzieję, że nie ma skandalicznych błędów.)

 8
Author: Brian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-11 22:00:08

Kombinatory parserów są naprawdę piękne! FParsec jest bardzo zgrabną biblioteką monadic parser combinator, którą powinieneś sprawdzić. Jeśli chcesz zacząć od czegoś prostego i wciąż czysto funkcjonalnego, możesz cieszyć się tokenizerem / parserem z interpretera Scheme w serii F # tutaj (mój blog): http://blogs.msdn.com/b/ashleyf/archive/2010/09/24/fscheme-0-0-0.aspx

 6
Author: AshleyF,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-11 03:26:32

ODPOWIEDŹ prostsza od innych dobrych odpowiedzi:

Parser w języku funkcji pobiera strumień tokenu do drzewa parsowania, a resztę strumienia tokenu. Oznacza to, że ma typ

 token list -> ast * token list

Rekurencyjny Parser zwykle ma dużą liczbę funkcji tego typu, które zjadają strumień tokenu, a następnie budują niewielką część drzewa parsowania. Wywołując je rekurencyjnie (recursive decent) - dostajesz to, czego chcesz.

Następnym krokiem jest użycie wyższego rzędu parsery: parsery działające na innych parserach. To właśnie robią biblioteki kombinatorów parserów. Być może mógłbyś zacząć od prostego schematu rekurencji, a następnie uaktualnić go do kombinatorów parserów.

 6
Author: I GIVE CRAP ANSWERS,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-11 20:36:17

Pracuję nad kompilatorem ECMAScript W F# przez jakiś czas, więc jestem w tej samej łodzi co Ty. Może niektóre z moich prac mogą ci się przydać. Oto prosta biblioteka combinatora parserów, nad którą pracuję, której używam w połączeniu z FParsec. Nie jest to idealne miejsce, ale powinno dać ci coś prostego do nauki, abyś mógł przejść do bardziej zaawansowanych rzeczy. Jeśli skończy się korzystanie z FParsec można zauważyć, że wiele rzeczy tutaj zostały zainspirowane przez to.

module Tools =

    open System
    open System.Diagnostics
    open LazyList 

    [<Struct;DebuggerStepThrough>]
    type State<'a, 'b> (input:LazyList<'a>, data:'b) = //'
        member this.Input = input
        member this.Data = data

    type Result<'a, 'b, 'c> = //'
    | Success of 'c * State<'a, 'b>
    | Failure of list<string> * State<'a, 'b>    

    type Parser<'a, 'b, 'c> = //' 
        State<'a, 'b> -> seq<Result<'a, 'b, 'c>>

    let zero<'a, 'b, 'c> (state:State<'a, 'b>) = //'
        Seq.empty<Result<'a, 'b, 'c>>

    let item<'a, 'b> (state:State<'a, 'b>) = seq { //'
        match state.Input with
        | Cons (head, tail) ->
            yield Success(head, State (tail, state.Data))
        | Nil -> ()
    } 

    let result<'a, 'b, 'c> (value:'c) (state:State<'a, 'b>) = seq  { //'
        yield Success (value, state)
    }

    let run p i d =
        p (State(i, d)) 

    let (>>=) (m:Parser<'a, 'b, 'c>) (f:'c -> Parser<'a, 'b, 'd>) (state:State<'a, 'b>) = //'
        let rec run errors = seq {
            for r in m state do
                match r with
                | Success (v, s) ->
                    yield! f v s
                | Failure (ms, s) ->
                    yield! run (errors @ ms)
        }
        run []

    let (<|>) (l:Parser<'a, 'b, 'c>) (r:Parser<'a, 'b, 'c>) (state:State<'a, 'b>) = //'  
        let rec run p = seq {
            for result in p state do
                match result with
                | Success (_, _) ->
                    yield result
                | Failure (_, _) -> ()
        }
        Seq.append (run l) (run r)

    type ParseMonad() =        
        member this.Bind (f:Parser<'a, 'b, 'c>, g:'c -> Parser<'a, 'b, 'd>) : Parser<'a, 'b, 'd> = f >>= g //'     
        member this.Combine (f, g) = f <|> g      
        member this.Delay (f:unit -> Parser<'a, 'b, 'c>) (state:State<'a, 'b>) = f () state //'
        member this.Return x = result x
        member this.ReturnFrom p = p
        member this.Zero () = zero

    let parse = ParseMonad()

    let (|>>) (parser:Parser<'a, 'b, 'c>) (f:'c -> 'd) = parse { //'
        let! v = parser
        return f v   
    }

    let satisfy predicate = parse {
        let! value = item
        if predicate value then
            return value 
    }

    let maybe parser = parse {
        return! parser |>> Some <|> result None 
    }

    let choice (ps:seq<Parser<'a, 'b, 'c>>) (state:State<'a, 'b>) = seq { //'
        if not (LazyList.isEmpty state.Input) then
            for p in ps do
                yield! p state    
    }

    let between left right parser =
        parse {
            let! _ = left
            let! v = parser
            let! _ = right
            return v
        }

    let skip p = parse {
        let! v = p
        return ()
    }

    let many parser = 
        let rec many result = parse {
            let! v = parser
            let result = v::result
            return! many result
            return result    
        }
        many []

    let many1 parser = parse {
        let! r = many parser
        if not r.IsEmpty then
            return r
    }

    let manyFold parser start (f:_ -> _ -> _) = parse {
        let! r = many parser
        return r |> List.fold f start
    }

    let many1Fold parser start (f:_ -> _ -> _) = parse {
        let! r = many1 parser
        return r |> List.fold f start
    } 

    let isNotFollowedBy p =
        parse {
            let! v = maybe p
            match v with
            | Some _ -> ()
            | None -> return ()
        }

    let pipe2 (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (f:'c -> 'd -> 'e) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            return f v1 v2
        }

    let pipe3 (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (p3:Parser<'a, 'b, 'e>) (f:'c -> 'd -> 'e -> 'f) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            let! v3 = p3
            return f v1 v2 v3
        }

    let pipe4 (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (p3:Parser<'a, 'b, 'e>) (p4:Parser<'a, 'b, 'f>) (f:'c -> 'd -> 'e -> 'f -> 'g) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            let! v3 = p3
            let! v4 = p4
            return f v1 v2 v3 v4
        }

    let pipe5 (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (p3:Parser<'a, 'b, 'e>) (p4:Parser<'a, 'b, 'f>) (p5:Parser<'a, 'b, 'g>) (f:'c -> 'd -> 'e -> 'f -> 'g -> 'h) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            let! v3 = p3
            let! v4 = p4
            let! v5 = p5
            return f v1 v2 v3 v4 v5
        }

    let tuple2<'a, 'b, 'c, 'd, 'e> (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (f:'c * 'd -> 'e) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            return f (v1, v2)
        }

    let tuple3 (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (p3:Parser<'a, 'b, 'e>) (f:'c * 'd * 'e -> 'f) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            let! v3 = p3
            return f (v1, v2, v3)
        }

    let tuple4 (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (p3:Parser<'a, 'b, 'e>) (p4:Parser<'a, 'b, 'f>) (f:'c * 'd * 'e * 'f -> 'g) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            let! v3 = p3
            let! v4 = p4
            return f (v1, v2, v3, v4)
        }

    let tuple5 (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (p3:Parser<'a, 'b, 'e>) (p4:Parser<'a, 'b, 'f>) (p5:Parser<'a, 'b, 'g>) (f:'c * 'd * 'e * 'f * 'g -> 'h) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            let! v3 = p3
            let! v4 = p4
            let! v5 = p5
            return f (v1, v2, v3, v4, v5)
        }

    let createParserRef<'a, 'b, 'c> () = //'
        let dummyParser = fun state -> failwith "a parser was not initialized"
        let r = ref dummyParser
        (fun state -> !r state), r : Parser<'a, 'b, 'c> * Parser<'a, 'b, 'c> ref //'

Uwaga: będziesz potrzebował Fsharp PowerPack do typu LazyList.

Przykład:

and conditionalExpressionNoIn = 
    parse {
        let! e1 = logicalORExpressionNoIn
        return! parse {
            do! skip expectQuestionMark
            let! e2 = assignmentExpression
            do! skip expectColon
            let! e3 = assignmentExpressionNoIn
            return ConditionalExpressionNoIn (e1, e2, e3)
        }
        return ConditionalExpressionNoIn (e1, SourceElement.Nil, SourceElement.Nil)
    }
 3
Author: ChaosPandion,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-11 19:41:36