Description
This is something I implemented in a toy language some years ago and I really enjoyed the results (at a personal scale mind you), so I wanted to share it in case it's useful for Nim.
This proposal seeks to introduce a more ergonomic syntax hopefully better suited for functional style programming than proc expressions or the do notation. It seeks to be very lightweight so it's the natural go to solution for what it works well. Its limited scope is to ensure it's not abused to create complex anonymous functions that become hard to test and might also help the compiler with optimizations.
The syntax is simply "\" expr
. The \
character has some convenient features in my opinion, it's very obvious when reading code yet lightweight, has a strongly stablished association with escaping stuff and, perhaps more importantly, resembles λ.
Semantics of the expression is escaping, similarly to its common use inside string literals, just that in this case escapes an expression so it's not computed at that point but wrapped in a lambda for future use. In a sense it converts an expression into a deferred computation.
The next trick it does is that it works without explitic placeholders, inferring which values are missing from the, potentially incomplete, expression. Given how Nim handles operators and since it's statically typed this seems complicated, so no idea if it's even possible to implement.
Ideally the incomplete expression would add a placeholder node to any potentially missing value position deferring typing, once it gets attached as a callback it would check how many arguments are required by the callback and instantiate it filling in the placeholders in some prioritized order, disambiguating prefix/binary operators, and producing and error only if arguments > placeholders
.
Some contrived examples to clarify the idea:
lst.map \ $
# lst.map( proc (x: int): string = $x )
lst.map \ + 2
# lst.map( proc (x: int): int = x + 2 )
lst.filter \ 3 < # arguable code style though
# lst.map( proc(x: int): bool = 3 < x )
lst.reduce(\ +, 0)
# lst.reduce( proc(a,b: int): int = a+b, 0 )
lst.sort \ cmp(.lower, .lower)
# lst.sort( proc(a, b: string): int = cmp(a.lower, b.lower) )
Although I can imagine that a first implementation would require explicit placeholders (i.e. \ _ + 2
) and then those examples offer little advantage over the current templates in sequtils
for instance. There is however a key difference in that these lambda expressions can be used anywhere that accepts a callback, no need to create specific templates for it, it's just a callable from the user's perspective. Library authors can of course leverage it though and easily inline the expression on a macro if desired, but from the user's point of view it's fully transparent if it's being used on a proc
or a macro
.
Additionally they are clearly demarcated by the \
character, which once learned you can quickly understand that the computation is being delegated. Moreover, my intuition is that this could replace many uses of an anonymous proc or a do
notation, oftentimes they just need to apply a simple operation over a value, hence making those a bit of a code smell that the code is perhaps not well structured.
Unlike the fat arrow sugar =>
this goes all the way in on terseness by eliding the declaration, it's a specific tool for a common problem, in a sense it's similar to a regex. In my opinion though, if all it ends up doing is replacing the =>
macro with a different syntax then it might not be worth it, it should feel as a language feature regardless of how it's implemented.
Note also that the \
character is currently a valid operator so using it for this could break some code. I couldn't say how popular of an operator it is though.