The Chain Operator (LLO Archive)

Created 2025-11-30, last modified 2025-11-30

Part of my archive of Layover Linux Official posts on Tumblr.


2025-09-27

Feature update for Prone, since I've been doing most of my work lately in behind-the-scenes ways, like reorganizing the way the code is parceled into files for better parallel compilation. Yeah yeah, that's cool, but what have I done that's visible?

Probably a lot of stuff since my last post. I went on a big spree making everything that comes out of an object's repr into valid source code. So, list parsing works, which is probably the biggest thing, but also various functions added to the standard library!

prn> [#a, #b, #c]
[#a, #b, #c]
prn> construct([#FOO, "Bar"])
construct([#FOO, "Bar"])
prn> conv::fn(op::plus)
conv::fn(op::plus)

As above, so below. Neato.

Something that was surprisingly hard, but important groundwork, was the chain operator, AKA the colon. This is an area of syntax that's still in flux, but it's just the syntax sugar for sneaking the value on the left side into the function call params on the right. These two lines are semantically equivalent, but the second one is pouring on some syntax sugar with the chain operator!

prn> plus(u8(1), u8(2))
u8(3)
prn> u8(1):plus(u8(2))
u8(3)

This is pretty useful in regular code, but it's particularly compelling in a REPL, where it's easier to add to the end of a line. Consider a contrived example of building up a list an element at a time, interactively. Bear in mind that the "push" function returns a new list, with the additional element added:

prn> push(list(), #a)
[#a]
prn> push(push(list(), #a), #b)
[#a, #b]
prn> push(push(push(list(), #a), #b), #c)
[#a, #b, #c]

See how hard to read that is? And imagine typing this iteratively in a REPL session. Guh. Let's try that again with chaining.

prn> []:**push(#a)
[#a]
prn> []:push(#a):push(#b)
[#a, #b]
prn> []:push(#a):push(#b):push(#c)
[#a, #b, #c]

Oooh, it reads left-to-right, it was easy to iterate in the REPL, and it covers an extremely common pattern in many languages that our standard library can intentionally lean into. That's the good stuff.

Technically this is the non-mutating chain operator. So, maybe you remember from previous posts about how Prone has Copy-on-Write (COW) semantics for passing parameters into functions. This is why "push" doesn't affect the original copy of an object:

prn> stuff = [#a, #b]
[#a, #b]
prn> [stuff:push(#x), stuff:push(#y), stuff:push(#z)]
[[#a, #b, #x], [#a, #b, #y], [#a, #b, #z]]
prn> stuff
[#a, #b]

One of the next things I'm doing is adding a mutating chain operator, which is an arrow (hyphen, then angle bracket). This actually updates the variable in your local scope, so you can only chain off an identifier.

prn> stuff = [#a, #b]
[#a, #b]
prn> [stuff->push(#x), stuff->push(#y), stuff->push(#z)]
[[#a, #b, #x], [#a, #b, #x, #y], [#a, #b, #x, #y, #z]]
prn> stuff
[#a, #b, #x, #y, #z]

What's cool about this is that the "push" function didn't change, we just used it in a different way. When it returned the new version of the list, we just used that as the new value in the caller scope. This is a design goal! But what about cases where you care about the mutated object AND a return value, like "pop", which removes and returns the last element?

Well, I'm thinking about implementing "pop" as returning a #MULTI_RETURN construct with two elements, the first of which is the new version of the list, and the second is the popped element. The idea is you have one function that always works the same way internally, but can be called in multiple ways to different effects.

prn> stuff = [#a, #b]
[#a, #b]
prn> pop(stuff)
construct([#MULTI_RETURN, [#a], #b)
prn> stuff:pop()
construct([#MULTI_RETURN, [#a], #b)
prn> stuff, popped = pop(stuff)
[#a], #b 
prn> stuff
[#a]
prn> popped
#b
prn> stuff = [#a, #b]
[#a, #b]
prn> stuff->pop()
#b
prn> stuff
[#a]

I'm not totally in love with this approach, but I think I'm in the ballpark of the right track. After using Perl, I'm a little wary of making the callee function too context-aware about the caller. I want the callee to just return some kind of value that can be useful in both mutating and non-mutating situations.