-> for LAMBDA (or... "LAMBDA-Lite?")

So I'm still shoring up a new bootstrap executable, patching around in a 6-years-out-of-date codebase. It's in some ways a cruel and unusual punishment... but in other ways a good trip down memory lane to revisit decisions that were made, and ask "was that the right decision?"

The 6-year-old EXE defined an enfix form of lambda as =>. I shifted it to the lighter form as ->. Contrast:

y: case [
    1 > 2 [<no>]
    1 < 2 [<yes>]
] then (lambda [x] [
    assert [x = <yes>]
    1000 + 20
])
assert [y = 1020]

y: case [
    1 > 2 [<no>]
    1 < 2 [<yes>]
] then x => [
    assert [x = <yes>]
    1000 + 20
]
assert [y = 1020]

y: case [
    1 > 2 [<no>]
    1 < 2 [<yes>]
] then x -> [
    assert [x = <yes>]
    1000 + 20
]
assert [y = 1020]

(Supplemental: Reddit post on "What's the syntax of lambda expressions in your language")

I like the -> and don't think there's a greater purpose for it in the box. As with everything else, overriding it is a personal choice.

A Speaking-With-Tics Note

Mechanically getting this to work is non-trivial:

The Most Vexing Evaluation: LAMBDA meets THEN/ELSE

Part of what makes it non-trivial is the "literal lookback" by which -> infixedly snatches the X without letting it evaluate.

@bradrn might argue such mechanics shouldn't be necessary, because source-level non-evaluation should be explicit, e.g. 'x -> [...]

y: case [
    1 > 2 [<no>]
    1 < 2 [<yes>]
] then 'x -> [
    assert [x = <yes>]
    1000 + 20
]
assert [y = 1020]

But that's one more stroke of key than I want, and one more piece of dirt than I want to see. I know from context that slot is a variable name, in the most common case. I'm not upset by intricate work to faciliate it, if it actually works (and empowers other creative things). So I need to see hard disproofs before sacrificing what I consider to be "the point" of the design.

I'm always ready to look at it from a further perspective to see a "greater point". But still--from where I stand--that apostrophe sucks relative to not having it. (And if you read the details of the implementation post, the only reason it's allowed to work in a quoted slot is because of left literalism, so...)

What About Multiple Arguments?

Notationally there are questions about this form of lambda. Does it use a block for multiple arguments?

>> foo: [a b] -> [a + b + 20]

>> foo 400 600
== 1020

It could, but you could be weirder:

foo: a.b -> [a + b + 20]

foo: a/b -> [a + b + 20]

This would look a little tighter with branching, I think, since it wouldn't compete with the branches:

case [
    ...
] then [a b] -> [
    ...
]

case [
    ...
] then a.b -> [
    ...
]

But wait, you'd never use it with a branch... because a branch only produces one value.

UNLESS... what if what this form of lambda did was unpack packs?

case [
    true [pack [10 + 20, 3 + 4]]  ; makes antiform ~['30 '7]~
    ...
] then [a b] -> [
    assert [a = 30, b = 7]
]

case [
    true [pack [10 + 20, 3 + 4]]
    ...
] then a -> [
    assert [a = 30]
]

So I've been thinking this is what it should actually do. It means -> won't be a good way to define functions or lambdas generally, but you have FUNC(TION) and LAMBDA for that.

I don't know that enabling a lighter notation like a.b or a/b is worth it.

case [
    true [pack [10 + 20, 3 + 4]]
    ...
] then a.b -> [
    assert [a = 30, b = 7]
]

case [
    true [pack [10 + 20, 3 + 4]]
    ...
] then a/b -> [
    assert [a = 30, b = 7]
]

Maybe just confusing, and limits what you can put in the spec. Easier to add later if it seems useful than put it in now and take out later.

Is There A Good Name For "Lambda Lite"?

We could call it an "unpacking lambda". Maybe it's controlled with a refinement:

 >> foo: lambda/unpack [a b] [a + b + 20]

 >> foo pack [400 600]
 == 1020

Then ->: :lambda/unpack

We could just call it "an unpacker".

 >> foo: unpacker [a b] [a + b + 20]

 >> foo pack [400 600]
 == 1020

Then ->: :unpacker

The problem with calling it an "unpacker" is that 9 (or more) times out of 10 it will only take a single argument and not unpack anything. So it seems better to classify it as a shade of distinction on lambda, but still when you point to an -> on the screen say "then it passes the argument to the lambda..."

1 Like

I remembered while writing this that you have to put branches in GROUP!s, because branches are "soft-quoted" slots (or, more accurately "soft literal") slots.

The reason you don't have to put the -> form in groups when you use them as branches is because there's deference to a leftward-literal operation in a soft-literal slot. The left literal beats the right literal.

But this has a cost: you're paying to generate the function whether it gets called or not.

Historical Rebol has this problem anywhere functions are passed as handlers. Consider TRY/EXCEPT in R3-Alpha:

r3-alpha>> try/except [1 / 0] func [e] [print ["Error Handler:" form e/id]]  
Error Handler: zero-divide

Putting it in a GROUP! won't help there, because /EXCEPT doesn't suppress any evaluation from that GROUP!. But in Ren-C, branches are literal, so the groups can be used to suppress the evaluation unless the branch runs.

Is Making Uncalled (Unpacking-) Lambdas A Problem?

That depends...

  1. does an unpacking lambda make a deep copy of the block?

  2. or does it just make a small structure that pairs the name of the parameter(s) with the body of code?

Today's it's [2]... -> just makes that small structure (while LAMBDA makes the full copy). But this means you get the semantics of a non-copying construct:

>> block: [print ["Hello" x]]

>> one: x -> block

>> append block spread [print ["Goodbye" x]]

>> two: x -> block

>> one 1020
Hello 1020
Goodbye 1020

>> two 1020
Hello 1020
Goodbye 1020

So you would have to say x -> copy/deep block to get a unique copy of the body.

Beyond the semantic implications of not copying, there's a performance implication if you call it more than once... because the preparations that make the body a little faster to call that happen during the copy are not done.

There are some other options like going in some strange "branch dialected" direction, and say that BLOCK!s with a certain format were "parameterized", moving the parameter name into the block somehow, not necessarily this but like this:

y: case [
    1 > 2 [<no>]
    1 < 2 [<yes>]
] then [x ->
    assert [x = <yes>]
    1000 + 20
]

I'd rather put every parameterized branch in a group than do that.

y: case [
    1 > 2 [<no>]
    1 < 2 [<yes>]
] then (x -> [
    assert [x = <yes>]
    1000 + 20
])

But still, no. It's much better to push on optimizations of the stylized function generation so that it's cheap as can be to make regardless of the branch being taken, it's the source we want to write:

y: case [
    1 > 2 [<no>]
    1 < 2 [<yes>]
] then x -> [
    assert [x = <yes>]
    1000 + 20
]

Just another devil in the details to worry over. But wanted to write up a reminder of why plain LAMBDA has to be in a GROUP! if you're going to use it in a branch, because I'd forgotten you had to do that.

I’ve come to accept that this idea has valid usecases. It’s still not my favourite design choice, but I’m OK with it.

1 Like