For example Asn1Type can be of form Builtin, Referenced, or Constrained. So a sum type.
parseType = Builtin <$> parseBuiltin <|> (Referenced <$> parseReferenced) <|> (Constrained <$> parseConstrained)
Assuming you have the parsers for Builtin, Referenced, and Constrained, you're golden. (Haskell PCs look very similar, possibly even exactly the same minus different parenthesis for operator precedence reasons.Compare Parsy for Python, particularly the verbosity (this parses SELECT statements in SQL):
select = seq(
_select=SELECT + space,
columns=column_expr.sep_by(padding + string(",") + padding, min=1),
_from=space + FROM + space,
table=table,
where=(space >> WHERE >> space >> comparison).optional(),
_end=padding + string(";"),
).combine_dict(Select)The same thing in a FP-style language would be something like
eq "SELECT" *>
Select
<*> (sepBy columnExpr (eq ",")) <* (eq "FROM")
<*> parseTable
<*> optional (eq "WHERE") <* eq ";"
which would feed into something like type Select = { cols: [ColumnExpr], table: Table, where: Optional Where}One thing you will definitely need in any language to produce a concise parser is the ability to define custom operators, like >>. and .>>. These are the “combinators”.
The key feature is first class functions, so you can have combinators like "or()", "and()", "repeat()", "optional()", etc, which take a function pointer as a parameter, as well as the input character stream; and returns the matched characters from the input stream, as well as the rest of the stream. When all combinators have this function signature, you can compose them easily, and writing the parser is becomes a matter of transcribing the ebnf grammar.
The closer a task is to the limit of your ability to handle complexity, the more you will benefit from tools that simplify the task.
The choice of programming language unfortunately what tools are possible to implement ergonomically (in terms of both implementation and interface).