The first time I happened on that word was when reading about Scheme continuations. The simplest way to describe continuations is: reification of return addresses.
In a language like C every function call has a hidden argument: a return closure that consists of a text address and a value to restore the frame pointer to upon return.
Reifying the return address closure requires giving it a name, and that name is simply "continuation" -- it refer to what follows when this function returns by... calling its continuation.
Because returns (now: calls to continuation functions) are always done in the tail position by definition, the caller's stack frame can be reused. This is a key point.
There's only one other mystery needed to fully explain Scheme continuations. Namely, that function frames have to be allocated on the heap in order to get full Scheme semantics. Add delineated continuations and you can have function frame stacks again at the price of a bit more ceremony so that the run-time can know when to call a function on a new stack.
You say that a continuation is a reification of return addresses. But what's reification? Never mind that, what's a return address? It's a return closure that consists of a text address? You mean, text, like, a string, an array of bytes? Oh, uh, no.
And, if you're really trying to explain this to a C programmer: what's a closure? Who are you imagining talking to that already understands what a closure is but doesn't know what a continuation is? Closures are at least as confusing as continuations; arguably more so.
What's "the tail position"? What does it mean to reuse a stack frame? ("This is a key point.") Are they different from function frames?
Who does this explanation serve? Certainly not a JS programmer trying to learn for the first time how to use function* and yield. Not a C programmer.
I think this explanation is for a Scheme programmer playing Ockham's razor to define things in terms of the smallest possible number of other things. This behavior can entertaining, but it's not educational, not explanatory.
You can define arithmetic in terms of set theory, but that's not how we teach arithmetic to children. Similarly, we shouldn't explain continuations in terms of implicit return closures.
Try this: Continuations allow you to pause and resume functions. The paused functions fold up into "continuation" values, which you use to resume the function where it left off.
The explanation helped me, a primarily C programmer who has done some work in higher level languages. When I read "reification of return addresses" that helped the concept click in a way it has not before.
The environment that a function executes in is important if, as in most interesting languages, there can be any free variables within the body of the function. What is the environment of a running function? Usually, its a collection of variable-name binding frames that determine the value of a programs variables at any point in time. The ep stands for the pointer into the data-structure (e.g. stack and its back links) that determine how variables are attached to values at a particular moment.
Lots of languages use stacks to hold these frames. In dynamic scoping languages, for example some Lisps like Emacs Lisp, a free variable's value is found in the stack frames determined by the run-time order of calls. Most modern languages use static scoping and the frames that determine a function's free variables values are based on the static nesting of functions or modules as they appear in the source code. In either case, a stack is usually where these frames that bind names to values are found. Dynamic vs static scope just changes which frames are searched for the binding of a variable at runtime (optimizations make this faster enough to be practical).
The text address is the reference to the next instruction (symbolic, virtual, or machine level) to be executed in a text segment (or code segment), the part of an object file that holds executable instructions ; I think of it as the IP of assembly language.
These two elements form a pair that in a simplified view represent a program in execution at a specific moment in time. So, this (ip, ep) pair is like a frozen program that can be brought back to life by simply loading two values into the basic execution machinery, the instruction pointer and the environment pointer. They need to be stored somewhere when a function is invoked and reloaded so that the caller is resumed upon return of the called function. When narrowly interpreted as parts of a call stack it's easy to lose sight of the power of the (ip, ep) pair. Part of Scheme's beauty was the expicit embrace of continuations as first class objects that can be flexibly manipulated by a running program and reified to put the program into a previously frozen state and awakened to continue there.
The Borrough's 5700 was designed to support a specific model of computation (Algol 60) and it's hardware description in [1] has pretty diagrams of the interaction of the ip,ep pair and the hardware (although the book is quite dated now).
[1] E. Organick, Computer System Organization: The B5700/B6700 Series, 1973, https://www.amazon.com/Computer-System-Organization-B5700-B6...
For a language runtime, it's the way to transform a language artifact to a runtime object.
By example, in Java, apart the reification of the type argument, the transformation of a lambda to a proxy that implement the functional interface is also a reification.
I'd only heard reification in the context of data modelling, where it's often meant to model the meta-model.
For example, RDF reification. RFD involves statements in the triple form (subject, predicate, object). The triple (Mary, enrolled, UCSF) corresponds to the statement that "Mary is enrolled at UCSF". A reification might be to turn that statement into a subject, and to make a statement about the statement. Eg. ((Mary, enrolled, USCF), created, 2018-12-06) - the statement that Mary is enrolled at USCF was created on 2018-12-06.
I'd like to throw some more thoughts out there, though. Focusing overmuch on what Java did leaves the picture somewhat incomplete.
First off, I've seen some people suggest that type erasure is a problem because it leads to a loss of type safety. This isn't really true. Haskell, for example, also preserves no run-time type information. The difference there is really about strength of typing - Haskell is very strongly typed, whereas C is weakly typed, and Java falls somewhere in the middle.
Second, the Java convention of passing Class variables into generic methods in order to give them all the type information they need:
<T> T getT(Class[t] clazz) { ... }
looks a bit like the C/C++ type erasure pattern that the author mentioned in passing at the start of the article, if you squint at it.My own sense is that the reason why type erasure was a problematic choice in Java is that it places Java in this uncomfortable middle ground where you have neither the robust compile-time type checking of a strong statically typed language like Haskell, nor the robust run-time reflection of a modern dynamic language. The end result, being robust at nothing, did not turn out to be the best of both worlds.
Why does it? In C#, for example, variance policy for generic type parameters is expressed explicitly - invariant by default, covariant or contravariant if you ask for it. While C# also has reified generics, I don't see why this couldn't be done with non-reified ones.
The JVM has been more successful than the CLR at hosting other languages, and having unreified generics I believe has contributed to that success. For instance, I have seen both the creator of Scala and one of the JRuby contributors both say they prefer JVM generics to CLR generics.
https://visualstudio.uservoice.com/forums/121579-visual-stud...
As GP mentions, Haskell is a great example of achieving both superb type safety and superb type expression with type erasure. That said, sometimes you don't want superb type safety - things like dependency injection and mocks are examples of frameworks you can use in languages with some amount of type reification without restructuring your entire codebase. Java/JVM achieve a decent balance here imo.
I'd argue it's the opposite.
Giving your programs the ability to access reified types at runtime weaken the robustness of your code, because you can second guess the compiler and introduce crashing code, such as incorrect type casts.
Reification also comes at a cost: performance, more problematic interoperability (Scala could notoriously not be ported to .net because it was not possible to make its type system work properly on a reified platform).
Contrary to popular belief, Java's choice of erasure was not dictated by backward compatibility concerns but because erasure is a sound approach for type system representation.
Do you have evidence for the "because"? I agree that erasure is a sound approach for type system representation when designing a language, but I thought that backwards compatibility was explicitly a concern in the case of Java.
Also, that good languages can be built with type erasure doesn't mean it's a good fit for the Java context where the runtime is doing a lot of dynamic dispatch based on tags that now only represent a part of the type.
When the debate was raging around 2004 when generics were being discussed, Neal Gafter offered a proposal of a reified JVM that would maintain backward compatibility.
Erasure was ultimately chosen over that proposal.
Backward compatibility was not the issue: technical soundness was.
This is false. I cannot find the source at the moment, but in a talk about implementing improved lambda support in Java 8 a Java compiler dev clearly labeled type erasure as technical debt they have to work around.
It frequently bites them when they try to implement new things, and causes weird interaction with new language features they have to work around.
”Supporting generic types at run time seems undesirable for the following reasons:
- Lack of experience with such constructs in widely used languages
- Burden of extensive VM changes on vendors throughout the industry
- Increased footprint on small devices
- Decreased performance for generic methods
- Compatibility”
However, that doesn’t rule out it is technical debt, too. “Conscious design choice” and “technical debt” do not rule out each other.
Which I could see being a problem, because then you need to figure out how to smoothly marshal data back and forth between (type erased) Scala collections and .NET functions, which typically expect reified types these days.
If they had gone with producing a .NET native version of Scala's standard libraries, they might have had easier success. But it also would have been a Pyrrhic victory, because you'd have lost the ability to run any existing Scala code, which would have scads of dependencies on other bits of the JDK as well, on Scala for .NET. At which point, what's the point?
I expect that porting F# to the JVM would encounter similar problems. And I expect that which platform you want to blame them on depends on which one is your home platform.
IMO the real story here is that the two big managed platforms are effectively walled gardens, by virtue of having VMs that implement such very high-level bytecode. Both of them effectively demand that all languages running on them can interact with, in effect, a thoroughly object-oriented ABI. There being approximately as may flavors of OOP as there are OO languages, that creates a situation where, if your language isn't set up to play nice with that platform's particular brand of OOP, you're going to have a bad time.
The JVM was never meant for other languages, that just happened, and most of the non-Java languages were designed to live nicely in the JVM world. The CLR is a bit more ambiguous, but I think it’s safe to say at this point that it is even less suited to other languages not designed specifically for it.
Wouldn't it be in the same boat as scala-js? There are quite a few libraries that support scala-jvm and scala-js.
Ask the parade of JVM languages porting themselves to native and JS runtimes.
All modern .NET APIs use generics heavily, and have done so since at least C# 3.5 (e.g. LINQ is pretty much all generics).
The reason why there are two collection APIs is because one of them predates generics. It is generally considered deprecated and rarely used, with exception of untyped IEnumerable, and that mostly because it's the base interface of IEnumerable<T>. It's not dissimilar to how Java also has legacy collections from 1.0 days.
Object based collections are generally deprecated over the generic versions and were existed prior to generics being introduced.
On the contrary, reified generics give the implementation more leeway with representation choices which can help with performance. In Java for instance a `List<Integer>` is stored as an array of boxed Integer objects, without easily being able to store the values inline.
This is another of those unfortunate implications of Java's having disappeared into this ugly chasm between static and dynamic typing. It doesn't fully erase types, so you can't do like C++ or Haskell do and just generate a completely different type, because it wouldn't be compatible with any functions that work with the generic type. At least, not without breaking backward compatibility, which was the whole point of doing type erased generics in the first place.
I wouldn't call it a "sound approach for type system representation", when it can't even consistently represent the fact that generic interfaces instantiated with different type parameters are different interfaces. I mean, it's obviously true given that any parameter-dependent method signatures are different, yet you can't implement Foo<Bar> and Foo<Baz> on the same class.
> Reification also comes at a cost: performance
What's the performance cost of reification? When comparing C# to Java, the only thing I can think of is that C# has an advantage in performance, because, due to generics being reified, its JIT can compile instantiations separately.
Works for me:
import java.util.List;
class Test {
public void setListBoolean(List<Boolean> list) {}
public void setListString(List<String> list) {}
public List<Boolean> getListBoolean() { return null; }
public List<String> getListString() { return null; }
public static void main(String[] args) {}
}...for languages that don't have casting and have correct variance. Java fails on both.
That statement is obviously wrong since Java did not choose type erasure.
https://docs.oracle.com/javase/8/docs/api/java/lang/Object.h...
In Java 5, generic types were added. And those Java chose to erase.
So Java chose both: to reify and erase.
https://www.schoolofhaskell.com/user/jfischoff/instances-and...
You can't have these two functions co-existing in the same class:
void Frob(List<String> someList) { ... }
void Frob(List<Integer> someList) { ... }
because, after erasure, they have identical signatures.You've got to repeat yourself a bunch, because generic methods don't know their own type parameters. So, to take an example using the Jackson JSON-handling library:
MyClass foo = objectMapper.readValue(json, MyClass.class);
is a bit annoying if you're coming from a language like C#, where you'd expect to be able to write code more like: MyClass foo = objectMapper.readValue(json);
Generic type instantiations aren't really types in Java, they're more this sort of type-ish concept. List<String> and List<Integer> exist as distinctions made by Java's type checker, but their actual class is just List - there's no such thing as List<String>.class. That's what's going on with that method overload example above. It's also the reason that this won't even compile, and you've got to resort to hacky workarounds in this sort of situation: Node<MyClass> foo = objectMapper.readValue(json, Node<MyClass>.class);
Then there's all the stuff about boxing, and how you can't have generic collections of atomic values, only boxed values. Which is responsible for a lot of excess memory consumption and pointer chasing, and also for things like that whole extra side quest in Java 8 streams where you get classes like IntStream and DoubleStream (but not FloatStream). Object xyz = ...;
if (xyz instanceof List<String>) { doA(); }
else if (xyz instanceof List<Integer> { doB(); }
because there is only a single List type at runtime. if (xyz instanceof List) {
if (xyz.isEmpty()) { ... }
else if (xyz.get(0) instanceof String) { ... }
else if (xyz.get(0) instanceof Integer) { ... }
}
Which, I admit, is a gross hack.Well, kinda cheating since the language’s about inlining these away. But technically true...
https://gbracha.blogspot.com/2018/10/reified-generics-search...
I really like this guy's articles. If you look in the archives [1], he's been writing prolifically for at least 15 years.
Let's zoom on 2018: [2] * 3 summaries of reading for at least 40 books read in a number of miscellaneous subjects (some of them re-reads) * Articles on type theory * Some articles centered around specific programming languages like python, Haskell, and Go * Articles on concurrency * And much more! :-)
I think he has "seasons" of learning particular languages. I think I've read some of his articles on Clojure, but he stopped writing about it eventually. According to StackOverflow [3], he appears to be an expert in Python.
In his Github account [4], I see a bunch of repos including one for code on his blog, but I don't think there's any particular repo he focuses on (maybe pycparser?).
Apart from making me feel inadequate about my own year accomplishments (how many books have I read this year?), I'm curious what one would do after such sizable knowledge ingestion. I'm surprised this guy doesn't attempt to come up with his own programming language. Maybe he knows is not worth it? Maybe he knows that is better to know the nooks and crannies of existing languages than trying to come up with a new one?
Maybe he just reads books instead of going dancing, watching a movie, socializing in a bar, listening music, browsing the web, playing the piano, or what not. Maybe he does all this things and still has time for reading and learning and writing his blog.
Sometimes I wish/hope/expect that everything I've learnt will sometime in the future allow me to create something, anything, where I can mix and match pieces of everything I've taken in. For sure this guy already benefits from all his knowledge on his day job, his side projects and maybe on his everyday life and interactions. I don't know, but I'm curious.
I mostly punish myself, all the time, for not being more productive, all the time.
1: https://eli.thegreenplace.net/archives/all
2: https://eli.thegreenplace.net/archives/2018
3: https://stackoverflow.com/users/8206/eli-bendersky?tab=profi...
4: https://github.com/eliben?utf8=%E2%9C%93&tab=repositories&q=...