F#ParsingActive PatternsFunctional Programming

Parsing a simple expression language in F# using Active Patterns

How to parse a string into an Abstract Syntax Tree using F# Active Patterns — a concise and composable approach to building parsers.


Sometimes it is necessary to transform a string into an Abstract Syntax Tree (AST) for further manipulation. In this post I’ll show how to do that in F# using a feature called Active Patterns.

If you’re not familiar with pattern matching in F#, the official docs are a good starting point. Active Patterns extend pattern matching by letting you run a function — rather than matching against a constant — in a match arm.

The AST

We start by defining the data structures that represent our expression language:

/// Abstract syntax tree (AST)
type BinaryOperator = 
    | And | Or | Equal | NotEqual | NotEqual2
    | GreaterThan | GreaterThanOrEqual | LessThan | LessThanOrEqual

type UnaryOperator =
    | Not

and Expr = 
    | Paren of Expr
    | Binop of Expr * BinaryOperator * Expr
    | Unop  of UnaryOperator * Expr
    | Ident of string
    | String of string
    | EndOfFile

And a small pretty-printer so we can verify the output:

let BinOpToStr = dict [
    And,              "&&"; Or,             "||"
    Equal,            "=="; NotEqual,       "!="
    NotEqual2,        "<>"; GreaterThan,    ">"
    GreaterThanOrEqual,"=>"; LessThan,      "<"
    LessThanOrEqual,  "<="
]

let rec BinaryOperatorToStr op =
    match BinOpToStr.TryGetValue op with
    | true, str -> str
    | _         -> failwithf "Unknown operator %A" op

and ExprToStr = function
    | Paren e1          -> sprintf "(%s)" (ExprToStr e1)
    | Binop(e1, op, e2) -> sprintf "%s %s %s" (ExprToStr e1) (BinaryOperatorToStr op) (ExprToStr e2)
    | Unop(op, e1)      -> sprintf "%s%s" (UnaryOperatorToStr op) (ExprToStr e1)
    | Ident s           -> s
    | String s          -> sprintf "\"%s\"" s
    | EndOfFile         -> ""

and UnaryOperatorToStr = function
    | Not -> "!"

Helpers

The parser works on lists of characters, so we define a few helpers:

let charsOfStr (s : string) = List.ofArray(s.ToCharArray())
let strOfChars (chars : char seq) = System.String(Seq.toArray chars)

let lowerChar  = set (['a'..'z'] @ ['æ';'ø';'å'])
let upperChar  = set (['A'..'Z'] @ ['Æ';'Ø';'Å'])
let letter     = lowerChar + upperChar
let digit      = set ['0' .. '9']
let letterOrDigit = letter + digit

let consume s = function
    | c :: cs when Set.contains c s -> Some(c, cs)
    | _ -> None

let basic pred = function
    | c :: cs when pred c -> Some(c, cs)
    | _ -> None

let (|Any1|_|)    e = basic ((=) e)
let (|Any1But|_|) e = basic ((<>) e)

Combinator Active Patterns

The real power comes from Active Patterns that are parameterised over other Active Patterns:

let (|ZeroOrMore|) q inp =
    let rec queryAcc rvs e =
        match q e with
        | Some(v, body) -> queryAcc (v :: rvs) body
        | None          -> (List.rev rvs, e)
    queryAcc [] inp

let (|OneOrMore|) q inp =
    match q inp with
    | Some (v, body) ->
        let xs, t = (|ZeroOrMore|) q body
        v :: xs, t
    | None -> [], inp

let (|ConsumeChar|_|)       = consume letter
let (|ConsumeCharNumber|_|) = consume letterOrDigit
let (|ConsumeNumber|)       = (|OneOrMore|) (consume digit)

let (|WS|) = function
    | ZeroOrMore ((|Any1|_|) ' ') (ws, xs) -> (ws, xs)

The Parser

The string and identifier parsers build on those combinators:

let (|STRING|_|) = function
    | Any1 '"' (_, ZeroOrMore ((|Any1But|_|) '"') (xs, Any1 '"' (_, WS(_, t)))) ->
        Some (String(strOfChars xs), t)
    | _ -> None

let (|IDENTEND|_|) = function
    | '_' :: '_' :: ConsumeNumber(n, (WS(_, cs))) -> Some(['_';'_'] @ n, cs)
    | _ -> None

let (|IDENT|_|) = function
    | ConsumeChar (c, OneOrMore (|ConsumeCharNumber|_|) (cs, IDENTEND(identend, xs))) ->
        Some (Ident(strOfChars(c :: cs @ identend)), xs)
    | _ -> None

And finally the full expression parser, with mutually recursive Active Patterns:

let rec (|EXPR|_|) = function
    | NOT(u, EXPR(e, xs))      -> Some(Unop(u, e), xs)
    | VALUE(e1, xs) ->
        match xs with
        | []                             -> Some(e1, [])
        | BINOP(o, WS(_, EXPR(e2, xs))) -> Some(Binop(e1, o, e2), xs)
        | xs                             -> Some(e1, xs)
    | xs -> failwithf "EXPR: unexpected '%s'" (strOfChars xs)

and (|BINOP|_|) = function
    | '&' :: '&' :: xs -> Some(And,              xs)
    | '|' :: '|' :: xs -> Some(Or,               xs)
    | '!' :: '=' :: xs -> Some(NotEqual,          xs)
    | '>' :: '=' :: xs -> Some(GreaterThanOrEqual,xs)
    | '>'       :: xs  -> Some(GreaterThan,       xs)
    | '=' :: '>' :: xs -> Some(GreaterThanOrEqual,xs)
    | '=' :: '=' :: xs -> Some(Equal,             xs)
    | '<' :: '>' :: xs -> Some(NotEqual2,         xs)
    | '<' :: '=' :: xs -> Some(LessThanOrEqual,   xs)
    | '<'       :: xs  -> Some(LessThan,          xs)
    | _                -> None

and (|NOT|_|) = function
    | '!' :: Any1But '=' (c, WS(_, cs)) -> Some(Not, c :: cs)
    | _ -> None

and (|VALUE|_|) = function
    | STRING(s, xs)                         -> Some(s,  xs)
    | IDENT(i,  xs)                         -> Some(i,  xs)
    | '(' :: (EXPR(e, ')' :: WS(_, xs)))   -> Some(Paren e, xs)
    | []                                    -> Some(EndOfFile, [])
    | xs -> failwithf "VALUE: unexpected '%s'" (strOfChars xs)

Testing it

let parse str =
    match charsOfStr str with
    | EXPR(e, []) -> ExprToStr e
    | _           -> failwith "Parse error"

parse "!member60__1 && company__1 == \"LP\""
// → "!member60__1 && company__1 == \"LP\""

parse "(t1__1 >= t1__1 && t1__1 > t1__1 && t1__1 < t1__1)"
// → "(t1__1 => t1__1 && t1__1 > t1__1 && t1__1 < t1__1)"

Why this approach?

Compared to parser combinator libraries (FParsec) or regex, Active Patterns give you:

  • Composability — small patterns combine into larger ones naturally
  • Readability — the parser code mirrors the grammar
  • No dependencies — pure F# standard library
  • Mutual recursionand keyword handles left-recursive grammars cleanly

The pattern of ZeroOrMore/OneOrMore parameterised over other Active Patterns is particularly powerful and appears in several production parsers I’ve written for the pensions domain.