@ngn I really don't like the attitude of "remove everything below usage threshold of X". Many things are used very rarely, but for the cases they are, they can be very important
@ngn Both are true, which is why impure features need to be few and well designed. In BQN, variable redefinition in closures is the only source of mutability, and it can only affect the results of function or modifier calls and namespace dotting or destructuring.
@ngn What exactly would you say is the best in the trade? Essentially all modern general-purpose languages have converged on lexical scoping with closures, even as immutable data is becoming more popular. Julia, Rust, and Go all have them.
@ngn some form of "utility" - how much time on average of all future usage of the language will this feature save. Of course this is definitely not something you can score, so you guess. And, if the utility of the feature underperforms, I'd only consider cutting it if the score is very very very tiny, or negative.
@ngn k has to pass surrounding variables to every invocation of a function to get around no closures, and I doubt anyone's implemented proper GUIs in k.
@ngn Haskell has monads and syntactic mechanisms that are intended mainly to replicate that style of programming though. It's a valid model, but it requires a lot of type-based machinery.
@dzaima it's surprisingly rare, but yes, that can be annoying
@dzaima gui-s - yes, they had them in k3 (iirc the version), that's when dicts appeared. then they dicided "we don't do guis, the terminal is the ui" but kept the dicts because they saw they were good for other purposes
@dzaima well, then k5 (kOS) was about to appear, and it was going to have a gui
unfortunately it never came out
but generally k is doing well without mutability. afaik the only mutable things are the stack (e.g. locals) and the root of the k-tree (the hierarchy of namespaces).
@dzaima i don't think they dropped guis to steer clear of mutability.. most likely it wasn't a profitable enough activity for the amount of support it requires
@dzaima btw, you can always program as if things are mutable. you just have to make sure you have a single reference to the object in question, to make sure its refcount remains 1.
@ngn that completely forgets the unique identity, makes it incredibly easy to get things wrong, and keeps the huge issue of you either needing a consistent storage place for the object, or having incredibly messy code of fake pointers (i.e. if I have a canvas at base.ch[0].graph.g, to draw a line I need to do base.ch[0].graph.g ← base.ch[0].graph.g Ln 0 0 100 100)
and to tick every item (which could include mutating things, i.e. text is typed, button starts being pressed, etc) you'd need to rebuild every children list from scratch whereas with mutation you just do F←{⍵.tick⍬ ⋄ F¨⍵.children}base
@dzaima i guess the immutable variant isn't much worse - F←{(tick ⍵) withChildren F¨ ⍵.children} ⋄ base ← F base
are you assuming that an assignment like base.ch[0].graph.g:something will make copies of everything along the path to the root (base, base.ch, base.ch[0]..) ?
@ngn a GUI might have a textfield at arbitrary depth. How do you keep track of which textfield is focused (if the precise location of it might change with items being added and removed around it)? How do you append to it? I don't see any way to do that besides having a "focused" key on each, and rescanning the entire GUI on every frame, if the GUI is a tree
@dzaima what's the identity of the thing you want to identify then? (address in memory is not an option - that's against the philosophy of value semantics)
if indices in base.ch don't identify components, maybe they should have a separate kind of id, and base.ch could be a dict
@ngn the identity is the human-perceived thing, it's the job of the GUI system to figure out what that is. If doing that in an acceptable manner means introducing mutable objects, I do that.
(i'm not saying an immutable GUI system is bad, just that it means having weird workarounds around very common tasks)
@dzaima maybe. i don't have experience with guis in k. we could ask john earnest how he deals with the need for mutability (if any) in oK and iKe but i'm not even sure how to formulate the question.
@ngn iKe just redraws everything on every frame, and doesn't give any facility of making GUIs, leaving that to the user and their own management of things
@ngn So to be clear here, you're arguing for principles you admit your own programming language doesn't fully support, by advocating a style you haven't used in a significant way? That… doesn't seems like the strongest rhetorical position.
@Marshall i'm arguing for value semantics. ngn/k supports that. (i think you're conflating that with closures - i don't support them, and i don't have a fully formed opinion of closures in the context of k)
@Marshall i didn't get the second part - what is the style you say i'm advocating?
@ngn As in, all I am doing is paraphrasing the statement "you can always program as if things are mutable. you just have to make sure you have a single reference to the object in question, to make sure its refcount remains 1".
so if namespaces were actually numbers, used as indices in a global list of dictionaries, that'd be fine?
@dzaima (actually that sounds cool - bodging proper mutable namespaces on ngn/k by abusing the global context :D)
@dzaima if you think about it, that's what mutable namespaces are - just that the "global list of dictionaries" is the pointer 0x0, and an index in it is equivalent to the address of the wanted namespace
@ngn In the case of BQN's mutable data, calling a function you got from somewhere else can have changing behavior. For global variables, using a name that isn't localized can have changing behavior. I don't see why you'd object to one but not the other.
@Marshall i might be missing something. don't you think updates should happen in-place when refc=1? or do you think they should happen in-place even when refc>1? or..
@ngn I'm not saying anything about the suggestion specifically. I'm pointing out that you offered a complex system you haven't tried as a solution to a problem. I don't think this is very credible.
@ngn I was actually describing BQN. I guess K functions are mutable in pretty much the same way as closures, though! The only difference is they can only change when something in the global state changes, which in theory you could see from the caller. But how would you know its name?
@ngn It's not up to interpretation. There is a local that does one thing, you call some other code, and it does a different thing. This is exactly the behavior you're complaining about with namespaces, but with a dot instead of juxtaposition.
@ngn it's effectively as bad as a language with mutable namespaces though, just with a different point in time when mutability becomes apparent (i.e. reading a global variable, vs reading a field of a mutable namespace; and, arguably, the former is much harder to notice than the latter)
Obviously whether that's a problem in practice depends on how you use it, but your point was that the mere presence of this kind of mutability in the language is a problem.
I'd argue that a scope is more like a namespace then. Sure, you could model it as a progression of dictionaries, but that's also how you'd model a namespace in terms of dictionaries.
@dzaima again: dyalog doesn't have a concept of a value-semantics dict. if it had, i'd use that term. "namespace" is the closest, that's why i use "namespace".
@ngn A namespace doesn't even need to contain name-value bindings. As far as I care, there are two functions that can be called on a namespace that completely define it - get field X, and set field X to Y (with appropriate syntactical sugar). A specific namespace may choose to do whatever it wants with those - ignore sets, give a new value for each separate invocation of get, etc.
@ngn can we agree (at least in this conversation) to always use "namespace" for the mutable string key→value structure, and "dictionary" for immutable arbitrary key→value map?
@dzaima (that's at least how i've been using the two names)
@dzaima Disagree with that one—if get doesn't get the value from the last set with a matching key, then it's not a namespace to me. BQN also has the requirement that a given namespace uses a particular set of field names specified when it's created and that they are identifiers, but I don't think that's fundamental.
@Marshall requiring set to be reflected in get is reasonable. But i'd think that a namespace representing a window shouldn't need to wrap it's size in a method just because the user might resize the window mid-execution
@ngn so: a namespace is not, nor was it made to be used as a dictionary. A dictionary is a data structure, but a namespace is "an encapsulation mechanism". Both can live along each other in a language, as each serves a completely different purpose and has a different syntax.
@dzaima so what should i be saying instead of "dyalog misdesigned their namespaces"? "dyalog should have implemented dictionaries instead of namespaces, and used them as scopes"?
@dzaima I see. I would say in that case that the window resize is really calling set behind the scenes concurrently. For performance reasons it might be necessary to eliminate these calls in practice.
@ngn "smaller code" doesn't mean much; "faster gc" it'd be called rarely enough for me to not worry about it; "better language" i'll respectfully disagree that value semantics are always 100% no-matter-what the absolute best thing, and most code will have value semantics anyways
Kind of a tangential point is that common cases where garbage collection isn't needed are easy to check for: an operation can't create closures if (0) it never accesses variables outside its scope or (1) every operation it contains can't create closures, and the enclosing scope only uses it by calling it. Operations that can't create closures will never use values from any parent scope once freed, so a block can be allocated on the stack if none of the blocks it contains can create closures.
@ngn it needs to be >0% best thing to warrant inclusion in the language, and needs to be 100% best thing to warrant excluding alternatives from it. Anything reasonably in the middle should have both options
including mutable namespaces should never make code (or coding) worse if you ignore them, and can, often enough, be beneficial (at least to me). So I, personally, have decided it's useful, at least to me.
@ngn so even this wouldn't work. To update something at depth, you must either write out the path as a list of symbols/indexes and have a function trace though them, or have it be a constant path and write it out twice
@ngn of what, a GUI system in k? (i'm pretty settled on what one I'd make would look like - a "global" list/dict of all "things", and containers having referencing children by their index in that list/dict. that way mutation doesn't need to worry about depth issues, and i can still have a nice LN function whose result doesn't need to be assigned anywhere)
@ngn Yes, I think medium and large arrays should be refcounted in order to reuse them when possible. It's easiest to add refcounts to all arrays, but it would also be reasonable to pass very small arrays by value.
@dzaima (or, with additional sass, "reinventing the square wheel" - why have two completely different ways to access things when you could have just one?)
@Adám except that only works because numbers are atoms. You'd need to use ‿ in the general case
and it keeps all the warts of strand notation (no empty arrays, no single-item arrays)
we still have square brackets completely unused if we wanted to add some syntax for higher-rank arrays
@Adám that syntax will necessarily always be longer than e.g. [1‿2,3‿4], excluding that rare case that all your items are variables (or atoms), at which point >a‿b‿c isn't much worse
imo a‿b‿c should only be used when a, b, c, … are all single tokens (so don't need parentheses around them), you should use the full list syntax for everything else
@ngn For unknown callees, the caller should pass the lowest possible refcount so the callee can consume it. I don't think I have any particularly interesting opinions about reference counts; why the questions?
@Adám Agree that dzaima that a high-rank ligature isn't really worth it. [] are reserved for high-rank arrays and at this point I should probably just add them.
@ngn i've seen other strategies too, but i don't like them either
i also tried very speculative refcounting in which objects with refcount=0 could exist with limited lifespans, it was very fast but it caused too many subtle problems
@ngn Strategies that result in different memory allocation patterns, rather than just different reference count values? As far as I know the only real choice you get is which callee to call last if multiple ones will use the same value, and even that only applies if you know enough to reorder functions.
@Marshall you have a lot of choice. for instance if you have triadic primitives (or i guess a 1-modifier in bqn), you could consume the operand and one of the arguments but not the other.
or maybe never consume the operand, but always consume both arguments.. many combinations are possible
in some cases i design my functions to never consume, for instance when processing an ast
@ngn But either there are other references to those values out there or there aren't. Any scheme should give the same answer regarding whether they exist, right?
If multiple arguments are reusable, you can choose which one to take, but I wouldn't expect that to have any impact on performance.
@ngn I have no experience with that sort of optimization. My inclination would be to say you should do more static analysis in the compiler instead of fighting over how references are counted. Most values are just created and used, and don't really require reference counts.
@Marshall it's actually measurable - you can find out if memory is being reused or not just by the timing. so if the primitive is given more chances of reuse, well, it's gonna be faster on average.
on the other hand, if you always try to reuse only one of the args, that may simplify the code, which could lead to another sort of speed-up..
@ngn So basically the question is whether to sometimes strategically keep reference counts too high to avoid overhead? Again, well outside my competence.