I used Text.Megaparsec.Expr to write a minimal (56 lines) 2-operator recursive parser. It's on github[1]. It outputs a binary tree of the following form:
data AExpr = Var String | Pair AExpr AExpr deriving (Show)
The operator table supplied to makeExprParser defines two operators, # and ##. ## binds after #, but both of them refer to the same function, the Pair constructor of the AExpr data type:
aOperators :: [[Operator Parser AExpr]]
aOperators =
[ [ InfixN # symbol "#" *> pure (Pair) ]
, [ InfixN # symbol "##" *> pure (Pair) ]
]
The # operator works in isolation:
> parseMaybe aExpr "a # b"
Just (Pair (Var "a") (Var "b"))
Parentheses work with the # operator:
> parseMaybe aExpr "(a # b) # (c # d)"
Just (Pair (Pair (Var "a") (Var "b")) -- whitespace added by hand
(Pair (Var "c") (Var "d")))
And the # and ## operators work together as intended:
> parseMaybe aExpr "a # b ## c # d"
Just (Pair (Pair (Var "a") (Var "b")) -- whitespace added by hand
(Pair (Var "c") (Var "d")))
But the ## operator in isolation does not parse!
> parseMaybe aExpr "a ## b"
Nothing
--
Jeff Brown | Jeffrey Benjamin Brown