A quick note on an idea. I'm not sure it's a good one.
Dict literals let us put keys and values next to each other. Compare [k1:"a";k2:"b"]
to !/+((`k1;"a");(`k2;"b"))
.
Unfortunately, []
syntax is used by other k features. For example, k9 2021 had dict literals, but not blocks (like JavaScript's blocks but as [a;b]
), and they needed disambiguation from m-exprs via a space (f [a:1]
vs f[a:1]
) or rounds ([a:1])
.
Blocks can be handy in conds; IIFEs aren't a perfect substitute as they don't have local scope access.
Could we shuffle around the language syntax so that we can have it all?
Maybe we could merge dict literal and lambda syntax so that both use {}
.
Dicts and lambdas are both maps, and they already share indexing syntax and some semantics. But dicts let you specify a finite list of entries, while lambdas let you specify infinite sets of points. We want a literal format that covers both cases, ideally within the same literal instance. We can do this by using []
s to wrap a condition that represents a set of keys.
This also includes default dict values as discussed earlier, and we can use computed defaults for simple {xyz}
inline lambdas.
The result? No more space-sensitivity of dict literals - and we keep blocks.
Here's how it looks. It superficially resembles JavaScript's computed property names but is closer to Rust's match
semantics.
{a:1;b:2} / explicit points. if input is `a, return 1; if input is `b, return 2
{[x]:x+1} / computed points. if input is truthy, return input+1, else null (or error?)
{a:1;[x]:x+1} / if input is `a, return 1; otherwise same as above (mixes explicit and computed points)
{a:1;2} / explicit default value. if input is `a, return 1; otherwise return 2
{a:1;[x+1]} / computed default value
{0:1;[x*o x-1]} / factorial. recursive default with base case for 0 input. entry order matters!
{[~x~*x]:,/o'x;[x]} / if list, recurse; otherwise return the atom
{local} / default value. can see locals.
{[x+1]} / computed default value. this is what we'd use instead of a {x+1} lambda. can't see locals.
/ disambiguated from a computed domain because there's no colon after ].
/ disambiguated from an arg names list in a lambda because the lambda ends after the }.
{[x+y*1]} / computed default with two unknowns. equivalent to a {xyz} lambda. 2d, so can be projected.
{[preamble / sometimes we want to calculate some intermediate results before a lookup (like {a:1;a+1}).
test x]: x+1} / so, we use a block to compute the keys, putting the preamble first so that it always executes.
/ however, because it's in a computed entry, the preamble can't see locals outside the literal.
{[x+:1;y+x]} / normal xyz lambda with some preamble before return. again, [] is like a block.
{[explicit;arg] / normal lambda with named args (no colon after ]). no change to current syntax (?).
explicit+arg}
{} / empty dict, which is also an empty lambda!
Here is a real-world example: the main loop of this ngn/k unparser. Using computed entries, we can replace a big $[;;]
cond in a lambda with a dict literal. I'm not sure it looks better?
/ before
f:{$[`S=@x; ,"."/$x
(~#x)|`A<@x; ,`k x
1=#x; ,`k@*x
EMPTY~*x; squares semicolons@ o'1_x
2=#x; monad x
LIST~*x; rounds semicolons@ o'1_x
composition x; ,/(M[0];o)@'1_x
verb x; infix x
mexpr x]}
/ after, take 1
f:{[`S=@x]: ,"."/$x
[(~#x)|`A<@x]: ,`k x
[1=#x]: ,`k@*x
[EMPTY~*x]: squares semicolons@ o'1_x
[2=#x]: monad x
[LIST~*x]: rounds semicolons@ o'1_x
[composition x]: ,/(M[0];o)@'1_x
[verb x]: infix x
[mexpr x]}
Maybe we could omit the square brackets if a computed key is not a valid name, such as a composition. (Ideally there'd be no need for a colon suffix to force monadic - it's a computed dict entry, so we know it's intended to be monadic.)
/ after, take 2
f:{`S=@: ,"."/$x
[(~#x)|`A<@x]: ,`k x
1=#: ,`k@*x
EMPTY~*: squares semicolons@ o'1_x
2=#: monad x
LIST~*: rounds semicolons@ o'1_x
composition@: ,/(M[0];o)@'1_x
verb@: infix x
[mexpr x]}
That looks better but, depending on the dialect, it might conflict with use of :
as the 'identity' verb.
By the way, that observation that "if a computed entry is not a valid name, it doesn't need square brackets" might not work for the default value, if we want the option of default values being able to refer to local variables.
-
Most of my real-world lambdas do wrapped conds like
{$[cond;someVal;defaultVal]}
which is a lookup but across conditions instead of values. (It's not just me: grep for$[
in the ngn/k codebase.) They usually check type or structure or emptiness. If we could do those checks in a dict instead, it might reduce the need to have cond in the language at all. -
We gain a shorter way to give 'filter' a value via a function:
{local}#
. Right now, we need:[;local]#
. -
In a parse tree, dict literals with computed entries could be represented as-is, like how lambdas are handled in ngn/k. Dict literals without computed entries could be represented as dicts, so we can add/remove/edit entries mechanically.
-
While dict values can refer to names around them, nested lambdas can't (
{a:1; {a}[]}[]
is not1
). The same applies to computed entries, since they're basically lambdas. -
The dict default is wrapped in
[]
if it's dynamic, and not wrapped if it needs access to locals (like how a nested lambda can't see locals around it). So if we want to use a block in calculating the default value, and we want it to see locals, we need to wrap the block in()
. -
What would
{a;b;c}
do now that it's not a lambda - do they get default keys? (And how would that interact with default values?) It can't be a block instead of[a;b;c]
, because blocks need to allow assignment within statements.
-
It might not result in shorter code than just putting a cond in a lambda, since you're constantly putting conditions in square brackets while they wouldn't need to be wrapped in a
$[;;]
. (Unless we could omit the square brackets for compositions. Which would be most of them!) -
It doesn't reduce the size of the language: we probably still need some kind of cond anyway since they're useful for controlling global state or for doing gnarly condition checks where arrows divide all over the place.
-
Might be harder to parse?
-
We gained round-less dict literals (
([a:b])
to[a:b]
), but lost square-less lambdas with implicit args ({xyz}
to{[xyz]}
).
So we get non-space-sensitive dict literals, keep blocks, and could maybe remove cond. Is it worth longer {xyz}
lambdas?