if you want more material for "shape" you could include the nifty implementation I discovered a while back (with the caveat that it only works if a structure is rectangular):
@Razetime I don't think k9 has nulls, so its prototypes are more typical "falsey" values. Most other k's (at least k4-k6) have nulls as the prototypes, which aren't strictly "falsey", but can be tested for using ^x and "filled" with x(atom)^y. There are different nulls for each datatype; if there are separate integer widths, or things like date/time formats, there can end up being a lot
@chrispsn I've come to the same conclusion. the only issue I see with having it fill with something else is the impact on initializing tables. if you have a dict, with some keys having lists of values and others just scalars (perhaps because it's shorter when typing it out), flipping it into a table will broadcast any scalars to all rows of the table
the alternative, which also exists when initializing empty string columns, is annoying (something like (#tbl)#,"")
@ngn I've found another inconvenient ? find edge case; e.g. ()?"e" or ()?"abc". ideally it would just do a flat lookup and return a 0N for each item of y
@chrispsn hmm, that could work. I don't think there are many "organically created" lists containing both atoms and lists (maybe from some primitives that return both atoms and scalars, but that's pretty contrived)
@ngn ok my attempt at a more sound rationale: it's a base case, especially given the ambiguity around how nested () is
would all ops "pass through" the prototypes if they would be a no-op on it? some operations can change the types, given their ranges (e.g. <x(list) always returns an integer list)
@ngn hmm. I'm likely just missing the bigger picture here. in my experience, when there's a chain of functions and a () or other empty/edge-casey type value gets generated early on in the chain, some ops are OK with it and others aren't (error'ing out somewhere downstream)
@coltim most primitives should work fine. the big problem is "each on empty"
if you have f'empty the interpreter would have to either just return empty with whatever prototype it already has (which may be wrong) or try to deduce it from f somehow
for instance now #'() is !0 but {#x}'() is () because the interpreter is not clever enough to figure out that {#x} always returns ints
I mean in practice I think what I'm grasping for is essentially "try this line of execution, and if stuff fails, just bubble the failing value up to the end" for empty lists
type inference for a k-like language wouldn't be that bad, I think, since the types are fixed and non-extensible. There's just a lot of cases where definitions are so general there aren't any constraints you can infer from the structure of expressions
at one point I pitched the idea of optional type annotations on lambda arguments, with a concise syntax, to arthur, with the idea that if you grounded a few types you could JIT hot functions quite simply, melting all the rank-agnostic stuff into specific fixed loops. K leaves a bit of performance on the table wrt. loop fusion
@coltim I think this also crops up when looking up a key in an empty dictionary (at least where the keys are () versus a typed empty list), e.g. (()!0)@`Jimmy
@JohnE a lower-level k is an interesting concept (e.g. some level of explicit typing, maybe some kind of vector loop syntax - iterating over chunks of a large array, etc etc)
@ngn would you be interested in test versions of my golfs?
@coltim I don't even think it would be necessary to change/extend the semantics of the language. It's just that type- and rank- polymorphism imply a fair degree of runtime complexity and dispatch that could be totally avoided if you could, in bounded contexts, provide assertions like "this argument is always a flat float vector", etc
if you add some constraints to the inputs of a lambda it can be enough to crystallize all the types flowing through it, and then you can perform some simple optimizations
@coltim the JIT stuff currently in CBQN is just lame mostly-constant code generation for each bytecode instruction. Soon are some plans for some scalar optimizations (going the JS-like route of assuming arguments are of a certain type and falling back to interpreter on unexpected arguments). Loop fusion is something for a very far future (there are barely even any vectorized builtin impls currently!).
needing to fall back when you aren't on a happy path is exactly what optional types could help avoid
make a typed lambda, compile it at parse time, perform the type assertions when you call the lambda, and either it's hella fast or you get a type error at invocation time
lift the sequence of vectorized ops into a single loop body. It's a fairly straightforward AST fold
@dzaima only on entry to the lambda, though- that's the point. you put your hot code inside the lambda and within it, no runtime dispatch would be required
@JohnE but then you need to dynamically compile that loop body to vectorized instructions, with correct overflow checks depending on language guarantees
@JohnE still don't quite get how that's different. Manual type markers any dynamically generated type markers should be precisely the same, no?
@JohnE how I understand it, you're saying something like a×b+c → {(⍵⊃a)×(⍵⊃b)+(⍵⊃c)}¨⍳n. Which isn't at all beneficial if × and + are implemented with vectorized loops, and the ¨ is not
explicit type markers can constrain the types sufficiently for codegen at parse time. A lambda like {x+y*z} requires dynamic dispatch for * and +. If you know x,y, and z are conformable vectors of floats, you have enough information to generate a vectorized loop without dynamic dispatch.
@JohnE but the runtime interpreter could see on the first call of the lambda that x,y,z are conformable float arrays, and just act like type markers were there in the first place (but instead of erroring when they don't match, you deoptimize instead)
generating loop fusion code shouldn't be too expensive, for large enough arrays you could just always do it. In practice, most type variance you'd have would just be 8/16/32-bit ints
explicitly "fixing" subgraphs of a program gives a programmer control over the mechanism. There'd be sharp corners, but the implementation would stay relatively simple.
@JohnE if you have ideas for nice type markers, I'd be open to hearing your thoughts though. All I'm saying is that they "just" simplify the implementation and decrease the startup time by a bit, hot loops would look pretty much the same for optimistic optimization of regular code
I personally would by far prefer type markers to dynamically generated assumptions, if they were sanely actually usable.
@dzaima (hard part with type markers is that in impls that don't allow the user to distinguish 8/16/32-bit arrays you still need to dynamically dispatch anyways)