Skip to content

Instantly share code, notes, and snippets.

@chrispsn
Last active March 20, 2024 12:57
Show Gist options
  • Save chrispsn/779ae79088fd6f432804ea33e3bdf74f to your computer and use it in GitHub Desktop.
Save chrispsn/779ae79088fd6f432804ea33e3bdf74f to your computer and use it in GitHub Desktop.

K: Computed entries in dict literals

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?

The idea: 'computed dict entries'

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.

Notes

  • 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 not 1). 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.

Concerns

  • 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?

See also

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment