Functions that Return Functions
Before we can collapse those eight operator rules, we need to solve a practical problem.
A curried add looks like this in our language:
["add", "=", ["lambda", "x", ["lambda", "y", ["x", "+", "y"]]]];
Calling it means two nested calls: first ["add", 2] returns a new function, then we apply that to 3:
[["add", 2], 3];
But the old call rule couldn’t handle this. It expected the function position to always be a name like "square" — it would look it up directly. Here, the function position is ["add", 2], which is itself an expression that needs to be evaluated first.
The New Call Rule
The match library now passes you the raw expressions: fnExpr (the function position) and argExpr (the argument). Your job is to:
- Evaluate
fnExpr— this might be a name like"square", or a nested call like["add", 2] - The result is a function object. Check what kind:
- A lambda looks like
{ param: "x", body: ["x", "+", "x"] }— bind the parameter withset, then evaluate the body - A native function looks like
{ native: true, fn: (a) => ... }— callfn.fnwith the evaluated argument
- A lambda looks like
- Return the result
NOTE
The operators +, -, *, /, <, >, ==, != are now pre-defined in the environment as native curried functions, each one stored as its name. So [["+", 2], 3] means look up the function named "+", apply to it 2, then to 3 exactly like calling add.
Write the call rule to handle both kinds of functions.
TIP
Design note. We model the built-in operators as native function-objects so that calling + goes through the same call rule as a function you defined. This is a teaching choice, not a claim about how real interpreters are built — many keep primitives as a separate category on purpose, since primitives genuinely are different (they bottom out in the host language). We erase that seam here because this module’s whole point is the realization that operators were never special — and that lesson is only true if the evaluator really can’t tell + from add.