Odin's Most Misunderstood Feature: `context`
32 points by gingerBill
32 points by gingerBill
the post mentions threadlocals being bad solutions. i've hit this issue a number of times, so i'll put in my $0.02:
the biggest but most easily fixed issue is that you can leave the variable set to the wrong thing. you can handle that with RAII/defer, though. just have your bind operation reset it when it goes out of scope.
the main problem with threadlocal is that when you create a new thread, or grab one from a thread pool, or you have some async operation that resumes on a different thread, it breaks in similar ways as a global
Clojure and C# both solve this, with special variables and TaskLocal<T>, respectively. under the hood, those are passing around a copy-on-write context, much like what odin is doing. odin's appears to be less flexible, but saves an indirection or two by hardcoding the set of items instead of making it open ended
and the other main difference is that odin passes it in function calls, while clojure and C# use a threadlocal and has some extra bookkeeping that happens on async boundaries
go also uses this pattern in a lot of places, but you pass the context explicitly. i think doing it implicitly probably solves a lot of problems without much of a perf hit
It reminded me a lot of dynamic scope as well. Clojure uses this for things like "where does the output go when you print things". This falls into the same category of "things you might want to change about libraries you're using".
I don't understand how Odin's context parameter is better than well-executed dynamic scope; it seems like it's just has more overhead and is more limited.
I have seen a lot of people compare it dynamic scoping but it isn't really anything like it. Odin's context is much less overhead that dynamic scoping because it's just a basic struct that is passed by pointer allocated on the stack. It doesn't allow the user to define any new variables, it's just a fixed layout.
it adds overhead basically everywhere, but the overhead is small and easy to optimize out in many cases. it's just an extra arg, which is going to disappear if the function is inlined or something
C#'s has 0 extra overhead for calling a function, but a fair bit more when it has to shuffle a task's state around, and when accessing a value
if i were designing a "well-executed dynamic scope", i'd probably do it this way, but add a copy-on-write table of some sort for additional values. (you could technically abuse the usr_ptr for that in odin, i guess)
Interesting design. This seems to be heavily related to the implicit parameter features in certain other languages like Scala, and I can see how it is useful to things like allocators or loggers. From my understanding, implicit parameter is primarily used to pass contextual information that a trait's signature does not express, this is also similar to Odin's goal of "intercept third-party code and libraries and modify their functionality".
That said, I've used neither the implicit parameter feature nor Odin, so I can't really compare the pros and cons of the two approachs
Also related: https://matklad.github.io/2023/05/02/implicits-for-mvs.html
I've always quite liked the look of this feature in Odin. Having the allocator implicit but locally configurable seems like a nice balance between convenience and capability. I hope to one day have enough free time and the right project to learn what it is like in practice.
One thing I was wondering: there are other things one may want to have "contextually" for parts of a program. For example, telemetry tracing spans, permissions/capabilities for the current user/actor/workflow, etc. I think if I was accustomed to being able to implicitly and conveniently provide the allocator I would also feel a desire to provide this context in a similar way. Is there a way to do this? If not, is having to use arguments an annoyance?
I could see this working well on a microcontroller. I've got a fun project written in C that has a webserver. I send a TCP request to the MCU to get the temperature, and it returns the value to me. I put this in a script and once a second it pings the MCU to get the temperature. After about 50 pings the microcontroller stops responding. I suspect this happens because it ran out of memory. If this were a PC, maybe I could use something like Valgrind to help debug the issue, but Valgrind doesn't work on microcontrollers, so the next best thing seems like it would be to add a log of allocations and frees to the memory allocator (yes, this would fill up the heap even faster). Unfortunately, the source code for the malloc in the standard library isn't available, and instead of using a macro as a level of indirection to call malloc (as Bill said good libraries sometimes do), all allocations call malloc directly in the source. So what I will probably end up doing is find-replacing all instances of malloc with my own MALLOC macro, and then write a wrapper around the built-in malloc that tracks allocations. If C had this context struct, I could just set context.allocator to my wrapper and I wouldn't have to do the find-replace.