-
-
Save stepchowfun/849288d793d5c58755b23d3539acf131 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/********************************************/ | |
/* 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. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.