Skip to content

Instantly share code, notes, and snippets.

@stepchowfun
Last active September 28, 2020 10:14
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stepchowfun/849288d793d5c58755b23d3539acf131 to your computer and use it in GitHub Desktop.
Save stepchowfun/849288d793d5c58755b23d3539acf131 to your computer and use it in GitHub Desktop.
/********************************************/
/* Existential types tutorial for Juliusz! */
/********************************************/
// You are already familiar with the idea of generics:
function pickOne<T>(x: T, y: T): T {
return Math.random() > 0.5 ? x : y;
}
console.log(pickOne("foo", "bar"));
// There's something interesting about the `pickOne` function: from the type signature
// alone, you can tell that this function must return one of its arguments unmodified
// (or loop forever). Since `pickOne` doesn't know what `T` will be when the function
// is called, there's not much it can do with `x` or `y` other than just return one of
// them. And `pickOne` can't return anything other than `x` or `y`, since it doesn't
// know how to construct a `T` out of thin air.
// In programming language theory, this insight that generic types give you information
// about what functions do is called "parametricity", and it was discovered just before
// I was born.
// The `pickOne` example is trivial, but you can also use parametricity in more
// interesting situations. Consider these two "mystery" functions:
function mystery1(x: number): string {
// <implementation unknown>
}
function mystery2<T>(x: T[]): T[] {
// <implementation unknown>
}
// Suppose you have an array of numbers called `myList`. Using parametricity, you can
// conclude that the following two expressions will always give you the same answer:
//
// 1. `mystery2(myList).map(mystery1)`
// 2. `mystery2(myList.map(mystery1))`
//
// That's amazing, because we don't even know what `mystery1` and `mystery2` do!
// Now, let me tell you about something even cooler that you can do with parametricity.
// We are going to learn about "existential types". Consider this interface for abstract
// math-like stuff:
interface MathStuff<T> {
value: T;
add: (x: T, y: T) => T;
multiply: (x: T, y: T) => T;
toString: (x: T) => string;
}
// We can implement that interface if we choose some specific type for `T`:
const mathStuff: MathStuff<number> = {
value: 3,
add(x: number, y: number) {
return x + y;
},
multiply(x: number, y: number) {
return x * y;
},
toString(x: number) {
return x.toString();
},
};
// It's tempting to think of `mathStuff` as an "object" (in the OOP sense) with a
// field and three methods. But I think it is more natural to think of it like a
// module (like something you might import somewhere).
// We can manipulate `value` using the operations provided by the module:
const six = mathStuff.add(mathStuff.value, mathStuff.value);
// Since we know `value` is a `number`, we can also directly add to it:
console.log(mathStuff.value + 42);
// But what if we want `value` to have an *abstract type*? In other words, what if we
// don't want people to know that it's a `number`, and we only want to allow people to
// manipulate it abstractly using the provided operations? I am about to show you how
// to do it using parametricity. Don't be scared:
function abstractMathStuff<S>(c: <T>(x: MathStuff<T>) => S): S {
return c(mathStuff);
}
// "Wait a minute," I hear you say, "the module is a function now?" Yes, that's right.
// If you want to access the contents of the module, you have to do it in a callback:
abstractMathStuff((x) => console.log(x.toString(x.add(x.value, x.value))));
// But we can only manipulate `value` abstractly, e.g., using the `add` and `multiply`
// operations. If we try to treat it like a `number`, we get a type error!
abstractMathStuff((x) => console.log(x.value + 42)); // TYPE ERROR!
// How does that work? The secret is that the callback is generic, so from the callback's
// perspective `value` just has some abstract type `T`. So the callback isn't allowed to
// do anything with it other than pass it to functions that take a `T` as an input. This
// only works if the language allows generic functions to be first-class values. The vast
// majority of mainstream languages do not, but fortunately TypeScript does.
// So we have successfully used parametricity to hide the representation of some data,
// allowing you to manipulate it only by using certain provided operations. That's the
// magic of existential types.
// In particular, the type of `abstractMathStuff` is our *existential type*:
type theExistentialType = <S>(c: <T>(x: MathStuff<T>) => S) => S;
// A language with built-in support for existential types would let you write something
// like this instead:
type theExistentialType = <exists T>(MathStuff<T>);
// But, it's okay that TypeScript doesn't have this, since we now know that we can
// implement it in terms of generics.
@AntoineSebert
Copy link

Great job !
I'm glad content like this exists, I think type theory often gets way too theoretical very quickly.

Concerning L69-L71 I would say that the comparison with modules is risky, since they may disappear at runtime. That would also make L86-L91 a bit more clear, since objects are sometimes callables.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment