Algebraic data types: things I wish someone had explained about FP (2019)
jrsinclair.com
jrsinclair.com
Actually, even the main text on the site with the `blotchy` background is hard to read for me.
At least FF reader mode makes it readable.
But, you might also notice a functional programmer over there. The one with a smug look and condescending smile on their face. And after bracing yourself, you ask them 'What’s so amusing?'
[...]
Now, let’s assume you manage to refrain from physical violence.
"""
Hilarious!
This means that there are certain states that may be impossible in practice, but possible to represent in your data. This can lead to unnecessary code at best, or undefined behaviour and bugs at worst.
For example, let's say we want to model loading data from a server.
With ADTs:
type WebData
= NoRequest
| Loading
| Failure HttpError
| Success MyData
With product types only (and enums), how would we reconcile that the request could return an error _or_ data?We could try something along the lines of:
type WebDataStatus
= NoRequest
| Loading
| Failure
| Success
type WebData = (WebDataStatus, Maybe HttpError, Maybe MyData)
Where the error and data fields are optional.But in this case, it would be possible to have both error _and_ data fields populated. Not ideal!
Category theory has a wealth of other concepts which suggest more general non-ADT properties, too. For example (ignore the jargon):
* Finite limits are a generalisation of finite products (i.e. the ADT product type), where we additionally require intersection types.
* Finite colimits are a generalisation of finite sums (i.e. the ADT union type), where we additionally require quotient types - and I'm not aware of any language, other than maybe Lean and friends, which has user-space quotients! Think of trying to define the rational numbers in user-space, but without forcing the user to deal with normalisation.
For instance, (A^B)^C = A^(B*C):
(f) => (b, c) => f(c)(b)
(f) => c => b => f(b, c)
which, of course, is currying and uncurrying.The best I can come up with is that if you want to talk about the product of function types (A -> X) x (B -> X) then a sum type allows you to summarize this a single function type (A+B -> X). With just a product you could already do something similar for the type (X -> A) x (X -> B) which is equivalent to (X -> AxB), but with a sum type you have the dual variant as well.
And I suppose that with sum and product types together you can kind of express BNF like rules directly in the type system. Which makes things like expressing syntax trees a hell of a lot cleaner.
That said ultimately a type system is just a part of the language you can use to express certain rules, you can usually still decide on your own rules you'll just have to enforce them manually.
Relatedly, a single field of an interface type + the ability to cast to a concrete type is very similar to a sum type; Go programmers will use this trick sometimes.
Sum type:
I can be A | B | C.
Interface thype:
I am ILetter. Types A, B, C implement ILetter.
Plausible in memory representation of sum type value: 8 bits for the type tag, plus max-size(A,B,C) bits for type.
Plausible in memory representation of interface type value: 64 bits for the type pointer and 64 bits for a pointer to A, B or C.
(Not as nice as the sum type!)
- https://news.ycombinator.com/item?id=21571938 (November 19, 2019 — 242 points, 112 comments)
Hum... I have a very strong impression that it's only the type system that can be algebraic. For single types, the name doesn't make any sense.
It's like asking if "2" is a vector. You can have a vector space where it's an element, but without context, the question doesn't make sense.
And if you just have unit type, empty type, sums and products, then you can only make types with finitely many inhabitants. Adding the integers to that makes you able to make any countable type, but potentially in a trivial way as described above.
Therefore just viewing the types as sets does not give a very satisfactory basis for determining whether a type is an "algebraic data type". Maybe we could do better if we consider some more aspects related to type safety, but it's not immediately obvious to me how.
I agree that viewing the types just in terms of their cardinality isn't terribly insightful. But I do think it's helpful to realize that arrays aren't some other kind of thing that's separate from ADTs, and that you can bring the same algebraic reasoning tools to bear on arrays (and other pedestrian data types).
Now that I think of it a bit more, it seems to be a category error to say that a "set" or an "object" or a "dictionary" is an algebraic data type. Algebraic data types are types (values) constructed out of simpler types (values) in a certain way, and that can be deconstructed in a way that mirrors the way they were created. You can implement various APIs/interfaces on top of that, including dictionaries and sets and whatnot.
But my point was that you can do the same on top of just integers (assuming you have the common integer operations), and you could even package it up in a type safe way so that the fact that you used integers in the implementation does not leak out, assuming the language has some modularity/abstraction mechanism so that you can make stuff private to a module. But based on this it seems to be a stretch to say that dictionaries are REALLY just integers or bitstrings.
And just like you probably wouldn't want to bring "integer reasoning" to bear on dictionaries, you probably don't want to use ADT reasoning to reason about dictionaries either. Because then you are really reasoning about the concrete implementation details and generally speaking those details are hidden from clients of the dictionary (and subject to change at any time).
I think a nice corollary to your second point is that the existence of an isomorphism between two collections doesn't mean they are the same thing. This is easy to see when you discuss integer reasoning. E.g., if we have a infinitely countable data type, so we have an isomorphism to Z. But that doesn't automatically make our data type a ring (not in any meaningful sense).
I guess you explained it better than me. What makes them algebraic is the way you think about them and the full set of types they are part of. For the types themselves, the name is meaningless.
For what it's worth, not all functional languages emphasize the ADT notion per se. E.g., the Ocaml manual generally talks about records, tuples, and variant types; "algebraic" really only comes up with GADTs are discussed.
https://www.typescriptlang.org/docs/handbook/unions-and-inte...
Might make the example a bit clearer.
I use the Grammarly Chrome plugin, which sometimes causes text to behave differently when it's copied and pasted.
> d = new Digit('')
Digit { value: NaN }