Getting specific about generics
emilymaier.net
emilymaier.net
What's the current conversation on sharing binary code? Unless I'm missing something, this is fundamentally incompatible with generic functions that are optimized at compile time. Either the compiled version has to have implementations for "every valid type" or the shared code must be recompiled at the time it is consumed, right?
It looks like the Go ecosystem tends to discourage distributing precompiled binary packages, so maybe this is not a real issue.
Go programs typically use RPC to communicate with plugins instead of loading dynamic libraries and calling functions in these libraries.
The reason is that while you can generate a specific implementation when you need it, all the client code will still be using non-generic code. You can't write a function that takes a Set(T) as argument, returns Set(T) as a return value, or receives a Set(T) from a called function — unless you make that part of your code generation pipeline. So the code generation becomes viral, infecting each piece of code that wants to be generic. The entire language needs to be transpiled.
You could lower the barrier considerably by supporting some kind of pluggable preprocessing ("//go:preprocess my_generics_syntax_plugin"), but you'd still run into the problem with combining packages from multiple sources that all wanted to share the same generic types. They would all have to be transpiled the same way.
It won’t work for primitives but I find myself using arrays of primitives less and less.
Your point still stnds though, which is why proper generics aren't something you 'tack on' to a language. You have to be prepared for it to be used pervasively. This is probably why templates are deep in the C++ StdLib; as an attempt to ensure templates really are 'part of the language'. Sadly, in this case it really expanded the language.
Having a lot of code go through generics isn't necessarily an issue. The potential issue comes from having a lot of different concrete instantiations of your abstract code. That issue might be solveable by smarter compilers.
Isn't that the whole idea?
In principle it could work in te JVM as well right?
This is only true generics using erasure, such as in Java. Since Java will store Objects (pointers to objects) in both cases. But in languages that use e.g. monomorphization a List<T> stores the Ts adjacent in memory, since the size of each element is known, whereas a non-generic list has to store pointers to objects, since the object sizes are not known and may vary per element. A generic List<T> that is monomorphized has two large benefits: fewer cache misses due to locality and more opportunities to inline T method implementations.
To check that, the compiler either has to see all code that will run in the process, which means whole program compilation and no plugins, or the language needs a way to make classes that cannot be subclassed, preventing code that the compiler didn’t see from subclassing T (https://en.wikipedia.org/wiki/Class_(computer_programming)#N...)
Only if generic type parameters are covariant, if type parameters are invariant, this is not a problem. I guess that most languages with subtypes use invariance by default.
Also, many languages that do generics through monomorphization do not support subclassing (Rust, Haskell, etc.).
no plugins,
Well, there is always the option of boxing collection elements. (E.g. by using Box in Rust.)
(Aside: Type checkers essentially see recursive function definitions as applications of a fixed point operator to non-recursive functions. If your function has a rank-1 type but uses polymorphic recursion, the type checker sees it as the application of a rank-2 fixed point operator to a non-recursive function. This is why I see polymorphic recursion as “morally higher-rank polymorphism”, even when the type signatures in your code are ostensibly rank-1 ones. Polymorphic recursion is widely used in Haskell.)
However, IMO, you only need rank-1 polymorphism 95% of the time anyway, so optimizing for the common use case is a good strategy. By far, the main use case for generics is implementing efficient and reasonably reusable data structures and algorithms in a reasonably type-safe way. For this use case, monomorphization and aggressive inlining of small functions are evidently the right things to do. Other uses of generics (say, streaming I/O frameworks) strike me as a lot more questionable.
For this to be an implementation detail, the compiler needs to be able to choose not to inline sometimes.