The Most Vexing Evaluation: LAMBDA meets THEN/ELSE

Whenever the evaluator code gets churned around, there are a few things that break first.

One of the most frequent nightmares is having to fix the mixture of lambdas and then/else. Here's the working behavior:

 >> if true [<branch>] then x -> [print ["THEN" x]] else [print "ELSE"]
 THEN <branch>

 >> if false [<branch>] then x -> [print ["THEN" x]] else [print "ELSE"]
 ELSE

This is a showpiece of composition. It's one of the things that I want people to encounter and go "wow" when they see how it's being done. So letting it break is not an option.

What Typically Breaks

When it goes south, what happens is the ELSE won't run.

We want the precedence to work out as if you had written this:

 ((if false [<branch>]) then (x -> [print ["THEN" x]])) else [print "ELSE"]

So when the THEN goes to get its branch argument, it gets the function constructed from the lambda but stops there...deferring the ELSE. The THEN runs, and the ELSE acts on the completed output of the entire IF..THEN expression.

Instead, when this breaks, the typical broken interpretation is:

 (if false [<branch>]) then ((x -> [print ["THEN" x]]) else [print "ELSE"])

Here we see that THEN went to get its branch argument of the lambda. But the lambda--once constructed--is passed as the first argument to ELSE, and the ELSE runs. Since the lambda isn't void the ELSE evaluates to that lambda...which the THEN runs. If the THEN gets void as input it doesn't run, and now considers itself the end of the chain...since the ELSE ran prematurely.

Why Does It Break?

Remember that THEN and ELSE are "deferring enfix operations". This is to say that they don't greedily take the first opportunity to run as an enfix (the way something like + does). Instead, they pass on that first opportunity...and run on the second opportunity.

Consider that if ELSE ran on its first enfix opportunity, it would be pointless:

 if false [print "not run"] else [print "run"]

The first time ELSE gets seen by the evaluator is while it's putting the [print "not run"] argument into IF's branch argument slot. If it ran at that moment, you'd have:

if false ([print "not run"] else [print "run"])

The ELSE would have a block on its left that wasn't void, and evaluate to that. So this would act the same as:

if false [print "not run"]

So now back to our broken situation:

if false [<branch>] then x -> [print ["THEN" x]] else [print "ELSE"]

ELSE will only defer once. Which means it should only be "seen" twice. When it breaks it gets seen three times:

  1. It gets seen while -> is fulfilling [print ["THEN" x]] into its second argument, and does a lookahead.
  2. It gets seen after THEN fulfills its argument
  3. It gets seen after THEN executes

We definitely want the second time ELSE is seen--when it executes--to be at (3). But which of (1) or (2) is the right "first" time to see ELSE? Both can't happen...but one of them has to happen.

A Case Of Conflicting Requirements for Lookahead

We know that 1 + 2 * 3 is 9 and not 7. So the first time * gets a chance to be looked-ahead-at is after the 1 + 2 enfix is finished.

If we looked only at this rule, it would suggest (1) is the "bad" time for ELSE to be "seen". -> is enfix like + is, and ELSE is enfix like * is. So we might see an analogy:

  • 1 + 2 * 3 => (1 + 2) * 3
  • x -> [...] else [...] => (x -> [...]) else [...]

But if enfix operators don't look ahead in their argument fulfillment, why would the -> have been seen during the THEN? Why wouldn't it be:

  ((if false [<branch>] then x) -> [print ["THEN" x]]) else [print "ELSE"]

It looks like we pretty much have to gerrymander this so that something about the properties of the parts get the desired outcome.

Something Has To Bend

The easiest-seeming option is: Quoted lookbacks like the one done by -> supersede the "no lookahead" rule. That would mean you get this:

>> 1 + 2 * 3
== 9  ; `*` is not quoting

>> left-quoting-multiplier: enfixed func [:left right] [
       print ["left-quoting-multiplier" left right]
       left * right
   ]

>> 1 + 2 left-quoting-multiplier 3
left-quoting-multiplier 2 3
== 7  ; what happens when you quote

I don't know if it's the best rule. But it doesn't seem to come into conflict with any of the crazy examples I've come up with yet. (And I try a fair number of "crazy" things.)

I'm patching the latest breakage of this behavior, and I'm kind of sure it will probably break again. But I wanted to have a reference to why this seems to work by taking away the "(1)" case.

A key point I want to make here is that since we've gotten this far with it, I'm not foreseeing accepting any designs which don't allow this to work.

1 Like