Background
I'm building a lisp-like toy language in Haskell with the following (stripped down) AST:
type Lisp = ExceptT Error (StateT Env IO)
type PrimOp = [Value] -> Lisp Value
data Value
-- | any identifier, e.g. "x", "lambda"
= VName Name
-- | s-expression, e.g. "(x y z)"
| VExpr (NonEmpty Value)
-- | curried lambda, e.g. "((lambda (x y) (y x)) 123)"
-- or even simply "(lambda (x) x)"
| VCurry (NonEmpty Name) Value Env
-- | primitive / built-in syntactic operations, e.g. "lambda"
| VPrimOp PrimOp
Part of the language is a primitive operation named lambda
:
(lambda (<args>...) <value>) -- syntax
(lambda (x y) x) -- example: const function
... which gets translated to the following in Haskell AST:
VExpr (VName "lambda" :| [VExpr (VName "x" :| [VName "y"]), VName "x"])
... then evaluated by the primitive operation primLambda
, converting it from a s-expression to a curried function:
VCurry ("x" :| ["y"]) (VName "x") <env> <- primlambda <above-ast>
The Problematic Code
Due to various quirks of the language, I cannot write the lambda
into the parser. Thus I have ended up with the following implementation of lambda
as a VPrimOp
:
primLambda :: PrimOp
primLambda values = do
(params, result) <- case values of
[params, result] -> pure (params, result)
_ -> throwError $ EArgCount "lambda" 2 (length values)
names <- case params of
VExpr names -> pure names
_ -> throwError $ EArgType "lambda" "VExpr" params
names' <- forM names $ \case
VName name -> pure name
name -> throwError $ EArgType "lambda" "VName" name
gets (VCont names' result)
Obviously there is a repeated pattern here: I'm doing some pattern matching at each of the 3 steps, and calling throwError
if the pattern does not match. However, the second and third step looks only at part of the first result, and the third step has to match everything in a list. Is there a more concise way to do this?
throwError
as part of theLisp
monad stack, because the user gave the interpreter an invalid program. Edited to clarify. \$\endgroup\$