Algebraic Data Types: Things I wish someone had explained about FP
jrsinclair.com
jrsinclair.com
> Arrays, Objects, Maps, WeakMaps, and Sets are all algebraic data types
This isn't really true on a couple levels. Algebraic Data Types are described through a small set of composition (data) or decomposition (codata) operations. Arrays, Maps, and Objects might be explainable in this way (though, really, we'd just be modeling them with ADTs). WeakMaps and Sets really cross the line though. WeakMaps, by their nature, need to appeal to something much more than just ADTs and Sets require "quotienting" to unify sets which contain the same elements in various ways. You can build the "raw stuff" from ADTs, but you won't have something that really behaves much like these types at all until you add in something extra.The emphasis on sum types is huge, but it leaves out something critical: "JavaScript doesn’t have a lot of built-in features for sum types" is true of user-defined sum types (and related to it just not having types all together), but it has great support for a few special sum types: integers, booleans, characters! These sum types are important to talk about because it helps make clear that we already work with sum types all the time! Our basis for distinction and choice is built atop them in any language.
Finally, while products and sums get you some wonderful modeling capabilities, the place where Algebraic Data Types really shine is when you add two further capabilities: function types (exponentials) and recursion (fixpoint, which doesn't really map to high school algebra). Those are clearly built into JavaScript as well and would be great to discuss as well.
- User defined Sum types - Pattern matching on these Sum types in switch statements - Blocks and things-that-are-currently-statements becoming expressions that evaluate to a value.
If it got those, then it would pretty close to the perfect dynamic language for me.
The real benefit of pattern matching is the compiler checking at least that you have valid cases, and ideally that you haven't missed any cases.
Scala example:
myList match {
case Nil => 0
case first :: Nil => first * 3
case first :: second :: Nil => first - second
case first :: second :: rest => first * second + sum(rest)
} switch (mylist) {
| [“”, _, “”] => x
| _ => y
}
Moreover, pattern matching usually involves pulling information out of the object at the same time. In the above example, maybe you want to do something with the middle element: switch (mylist) {
| [“”, middle, “”] => middle ++ “!”
| [“foo”, x] => String.reverse(x)
| _ => “never mind”
}
All of this would be possible to do with a series of if conditions, but significantly harder to read and implement correctly.The power of pattern matching grows clearer as more complex data and rules are introduced. For example, it works just as well with nested lists:
switch (mylist) {
| [outer, [middle, [inner]]] => outer ++ middle ++ inner ++ “!”
| _ => “never mind”
}
While having a type system is helpful in all of the ways a type system is generally helpful, there’s nothing about this code which wouldn’t also be handy in a dynamic language like JavaScript. For further evidence of this see Erlang and Elixir, dynamic languages which make heavy use of pattern matching.(Code typed on my iPhone so forgive the dopey examples)
I agree that matching on multiple levels in one statement isn't common, if that's what you were talking about. But performing bindings in a match is very common, and not something that's part of a switch statement (except in those languages where a switch statement is doing pattern matching more generally anyway).
Nevertheless, if that was the only benefit of it, one might argue it’d be more of a party trick. But it’s simply one more facet of a very powerful, simple-to-use technique for expressing how your code reacts to the shape of your data.
I’m curious how much exposure you’ve had to languages which support it — you might find it more useful than you think.
I wouldn't call it a party trick, but I also wouldn't say it's the most common use of case by a long shot. I don't expect that you disagree.
I never thought I would understand point set topology, but for a few years I really did. (That was quite a few years ago, and since I haven't practiced... Well, it atrophied. Still remember my favorite proof, though! I also remember what a nightmare proof Stone–Weierstrass was :) ... amazing, but still a nightmare.)
Every time that functional programming patterns get discussed, though, there is a mad rush by functional programmers to impress upon everyone just how complicated they are.
It is the same impulse that keeps the mathematical entries in Wikipedia accurate but utterly worthless for most people.
> Every time that functional programming patterns get discussed, though, there is a mad rush by functional programmers to impress upon everyone just how complicated they are.
I... haven't observed this behavior personally. FWIW, I don't think that saying (for example)
> Hey, we can actually define what "|" and "(_,_)" means in a mathematical way
detracts in any way from a practical understanding. Can you elaborate on why you think it's detrimental? (I understand that it can come off as a little bit smug, but beyond that... meh?)
Then, to restore the reader's sense of mastery over the material, it's proposed that they grok a significantly more esoteric perspective.
(FWIW, I think it's absolutely right to call into question what the reader may have learned if an article is misleading. We can argue about wording, etc., but accuracy is important, I feel. Now, Lies-to-Children[0] certainly have their place, but they must be fundamentally accurate even if simplified.)
[0] I think I first heard of this concept in the Science of Discworld books...?
But yes, whether something is a sum type or a product type or whatever is not generally so interesting as a predicate about the type in itself. It's a relationship between this type and other types. The same way it is not interesting to say "7 is a sum" (everything is a sum); what's interesting is to observe "7 is the sum of 3 and 4".
[Disclaimer: In the same way that some topological spaces are connected and cannot be decomposed into separate disconnected components, one might sometimes reason that some types are connected and not reasonably represented as a disjoint unions over subtypes. So "Everything is a sum" is not always correct in all contexts. But let's ignore this sort of pedantry for now.]
I'm sure you're going to tell me that I'm not thinking abstractly enough...
So a “sum type” can be any type which is the sum of different values. On the other hand, Either is a canonical “sum type (constructor)” which sums two types together (via the summation of their vaues).
So if I'm in Haskell, say, and I've got a Maybe Integer, and I'm trying to walk through the options (pattern matching), I usually see the options as Nothing or Just Integer. Can I instead do Nothing or Just 0 or... "Just some other Integer"? That is, can I treat... um, not sure how to say this in light of your post... "individual values of a particular named type" the same way as I can treat "different named types"?
data MaybeInteger = Nothing | Just Integer
This defines the type MaybeInteger by saying how to construct the values that inhabit it. `Nothing` is a value that is immediately of type MaybeInteger. `Just` is a way to construct a value of type MaybeInteger by feeding it an Integer. You use `Just` to construct a value by applying it to an Integer: `Just 4`, for example.
There is an alternative syntax for defining sum types that might be clearer:
data MaybeInteger where
Nothing :: MaybeInteger
Just :: Integer -> MaybeInteger
When using sum types, we use a case statement: case x of
Nothing -> "nothing"
Just 1 -> "one"
Just _ -> "something else" data MaybeInteger where
Nothing :: MaybeInteger
Just :: Integer -> MaybeInteger
Doesn't this mean that Nothing (ie. MaybeInteger) is the same "type"/"supertype" however you call it, as a constructor from Integer to MaybeInteger?How does that work, e.g. how does MaybeInteger is equivalent to Integer -> MaybeInteger? I guess ending up at the same type is crucial, but one looks to be the type directly, while the other a constructor for the type.
And how can "Just" be a constructor for MaybeInteger here, but e.g. MaybeString in some other example? Is it specially blessed? What kind of thing it is in the language?
In this case Nothing is immediately a value of type MaybeInteger. Just on the other hand has the type of a function :: Integer -> MaybeInteger.
So they don't have the same type, but Nothing and Just 5 have the same type, MaybeInteger.
For pedagogical purposes I pretended to work with MaybeInteger instead of Maybe from Haskell. The definition of Maybe is:
data Maybe a = Nothing | Just a
or alternatively data Maybe a where
Nothing :: Maybe a
Just :: a -> Maybe a
I didn't want to simultaneously explain polymorphism and sum types, but the way you can combine them is vastly more useful than either feature alone.Nothing takes no parameters because there's no value there, so why would it?
Just takes an integer and gives you a MaybeInteger containing the integer you supplied to it.
There is only a single type here, namely MaybeInteger.
You can have a "type" for 4 in peano naturals and use that as a type guard on a function that can only physically take the number 4, and the Haskell type checking system will enforce that.
Here's one very clear implementation of Peano naturals as Haskell sum type: https://github.com/kenbot/church/blob/master/PeanoNat.hs
It's rare that you need that granular of a type, though, and as pointed out in sibling response it's easy enough to do with runtime pattern matching of Integer values. But it does illustrate that type abstraction "can go deeper".
data Zero = Zero data One = One data Two = Two ... data NaturalNumber = Zero | One | Two | Three | ...
Whatever you would say about sets, you might as well say the same thing about types; the distinction in terminology doesn't amount to much. The natural numbers are the disjoint union of {0}, {1}, {2}, {3}, ... . They're also the disjoint union of the even naturals and the odd naturals, or the disjoint union of {0}, {1}, primes, and composites. There are a million ways in which they are a disjoint union.
For a product type with no “or”, consider the JavaScript object {}. We see these as “atomic”.
Natural = Zero | Successor(Natural)
Bool = True | False
The mapping from Natural to Integer is standard.
data Int = Positive Nat | Negative Nat
data Nat = Zero | Succ Nat data Int = Zero | NonZero Sign PositiveInt
data PositiveInt = One | Succ PositiveInt
data Sign = Positive | NegativeBefore defining the integers, one must define the natural numbers. Natural numbers are defined as an inductive type, which is a little different from an ordinary sum type:
data Nat = Z | Suc Nat
Here is another way of defining them. Start by removing the self-reference: data NatF a = Z | Suc a
Notice that NatF is the same thing as the Maybe/Option type, AKA 1+a, AKA the sum of the unit type and a.Given a "fix" type:
data Fix f = MkFix (f (Fix f))
the definition of the natural numbers would be: type Nat = Fix NatF
An "F-algebra" is a way of defining some algebraic structure or data type as a map from a "sum of products" to the type. More precisely, an F-algebra consists of an "object" (type) A and a map F(A) -> A. For the natural numbers, the F is NatF and the map would be a function mkNat : NatF Nat -> Nat, AKA mkNat : (Maybe Nat) -> Nat or mkNat : (1 + Nat) -> Nat.Related links:
- https://ncatlab.org/nlab/show/inductive+type
- https://ncatlab.org/nlab/show/natural+numbers+object
- https://ncatlab.org/nlab/show/initial+algebra+of+an+endofunc...
- https://en.wikipedia.org/wiki/F-algebra
---
One way of defining the integers is as the product of the natural numbers with themselves, or Nat * Nat, equipped with an equivalence relation ~ where
(a, b) ~ (c, d) := a + d = b + c
(a, b) can be understood as a - b. Of course, a - b = c - d <-> a + d = b + c.
Another way of defining the integers is as:
data Int = Neg Nat | Zero | Pos Nat
where 0 = Zero, -1 = Neg Z, and +1 = Pos Z.---
Booleans can be defined as 1 + 1. Left () = True and Right () = False, or vice versa.
data Nat = Z | Suc Nat
^ What does this crazy construction mean syntactically?It looks like it is trying to do something like (from your link) Coq's
Inductive nat : Type :=
| zero : nat
| succ : nat -> nat.
But obviously defining natural numbers in terms of Z/the integers doesn't work because I could then call -1 a natural number. 0 == Z
1 == Suc Z
2 == Suc (Suc Z)
3 == Suc (Suc (Suc Z))
You can define some arithmetic operators for it: data Nat = Z | Suc Nat deriving (Show, Eq)
instance Num Nat where
(Suc x) + y = Suc (x + y)
Z + y = y
(Suc x) - (Suc y) = x - y
x - Z = x
Z - (Suc _) = undefined
Z * y = Z
(Suc x) * y = (x * y) + y
negate = undefined
abs x = x
signum (Suc _) = Suc Z
signum Z = Z
fromInteger 0 = Z
fromInteger x
| x < 0 = undefined
| otherwise = Suc (fromInteger (x - 1))
The definition `fromInteger` is used by GHC to implicitly convert decimal numbers in the code to our Nat. We can use it like this: ghci> 0 == Z
True
ghci> 1 == Suc Z
True
ghci> 2 == Suc (Suc Z)
True
ghci> 3 :: Nat
Suc (Suc (Suc Z))
ghci> 3 + 2 :: Nat
Suc (Suc (Suc (Suc (Suc Z))))
ghci> 3 - 2 :: Nat
Suc Z
ghci> 3 * 2 :: Nat
Suc (Suc (Suc (Suc (Suc (Suc Z)))))
ghci> 3 - 4 :: Nat
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
error, called at libraries/base/GHC/Err.hs:78:14 in base:GHC.Err
undefined, called at <interactive>:70:15 in interactive:Ghci8You can rote learn that, but the reason why it's intuitive to call them sum types is because you're adding the number of possible values. With product types, you multiply the number of possible values that the type represents. Consider these types:
-- 1 + 1 = 2 possible values
data Bool = True | False
-- 1 + 1 * a = 1 + a possible values
data Maybe a = Nothing | Just a
-- 1 * a + 1 * b = a + b possible values
data Either a b = Left a | Right b
-- 1 * a * b possible values
data Pair a b = Pair a b
-- 1 * a * b * c possible values
data Triple a b c = Triple a b c
`Maybe Bool` would have 1 + 2 = 3 possible values.`Either (Pair Bool Bool) (Maybe Bool)` would have (2 * 2) + (1 + 2) = 7 possible values.
Integers would be defined as:
data Int = ... | -2 | -1 | 0 | 1 | 2 | ...
if they were finite, but since they're not, they'd have to be defined recursively with something like: data PositiveInteger = One | Succ PositiveInteger
data Integer = Zero | NonZero Bool PositiveInteger
where the Bool represents the sign.Since 2 * x is the same as x + x, we can also define it as:
data Integer = Zero | NonZero (Either PositiveInteger PositiveInteger)
where Left of Either would represent negative integers, and Right of Either would represent positive integers.Instead, you can extend algebraic data types to combinatorial species, which do allow exactly the kind of quotienting you speak of. While they're unexplored in programming, they are believed to be programmable similarly to ADTs.
https://repository.upenn.edu/cgi/viewcontent.cgi?article=177...
I must take issue with the "objects are products" characterization you implied. Structures are products; objects need existential types: http://www.cis.upenn.edu/~bcpierce/papers/compobj.ps
(Now with optional laziness.)
Here the Rust book mentions it:
> async bodies and other futures are lazy: they do nothing until they are run.
https://rust-lang.github.io/async-book/03_async_await/01_cha...
I just read it, and thought it was odd. I don't understand how Rust's async relates to a laziness as in Haskell's laziness.
All of these things build up some form of computation that doesn’t execute until later. Until then, they’re represented by some kind of data structure.
Huh? They're part of Pascal (known as variant records) and plenty of other languages besides. It's mostly C that lacks them, and even then you're just expected to implement them yourself, for maximum efficiency.
"plenty of other languages besides" doesn't matter if those languages aren't being used in practice.
Let me list off the top 10 languages (incl markup) from the most recent Stack Overflow survey, specifically the Professional Developers tab. https://insights.stackoverflow.com/survey/2019#technology-_-...
* JavaScript
* HTML/CSS
* SQL
* Python
* Java
* Bash/Shell/PowerShell
* C#
* PHP
* TypeScript
* C++
You can argue about the demographics of Stack Overflow or the bias in users who actually respond on these surveys or a million other things. But based on job postings I've seen in the last several years, I'd wager this list is pretty accurate.
But it was a response to the following comment:
> > I could not believe they had not been part of any prior language I'd learned.
> Huh? They're part of Pascal (known as variant records) and plenty of other languages besides. It's mostly C that lacks them, and even then you're just expected to implement them yourself, for maximum efficiency.
With the context of that post, I hope you would understand that I am disagreeing with the insinuation that every programmer has used "Pascal [...] and plenty of other languages besides". You can easily learn five different languages and finally end up in Haskell without having encountered ADTs before.
It sounds like GP is saying they had never used of those languages beforehand. I'm not sure why would be so surprising given that it doesn't accompany any other information like "and I had used 20 other languages before it".
Just enable -Wincomplete-patterns or -Wall!
For example if you write this in Haskell
data PrimaryColor = Red | Green | Blue
colorToInt Red = 1
colorToInt Green = 2
colorToInt Blue = 3
it is closed because future code can't add more alternatives. Instead you must simulate it using type classes, with a wildly different syntax, and (some would say) a hack. Or define a different sum type that wraps the above.OCaml on the other hand supports the above perfectly with a slightly different syntax, but it also supports polymorphic variants (not to be confused with a Haskell sum type that's polymorphic because of a type variable like Maybe) which are lighter weight. For example you can write a function that takes different "constructors" without necessarily giving a definition for the type or listing all alternatives:
let color_to_int = function
| `Red -> 1 | `Green -> 2 | `Blue -> 3
Notice I never defined any type for the three colors! If you wish, you can mention the type: [< `Red | `Green | `Blue ]
or give it an alias to make it look more normal. But you can add (with some limitations) or remove more things to this variant later on.To me, though, the most intuitive way of understanding this is to think of the first as nominal typing, whereas the second is structural typing.
In particular, I wonder about their characterization of sum types. I had to search to make sure, but they never say the word "union". This is curious, because they do understand "tag" and "tagged" as words used to describe sum-type behavior, and because the classic way to explain sum types to "imperative" C programmers is as "tagged unions", or union types with a tag value that explains which of the union's constructors is present. C or Java programmers would instead prefer an analogy with subtyping, which the article proceeds to use, but by overlooking the tagged-union analogy, the author is actually missing out on both a useful analogy for reaching out to imperative programmers, and also how low-level implementations of functional programming languages usually implement sum types.
Pattern-matching is probably the killer part of ADTs, but only two paragraphs were spent upon the concept. Worse, it's implied that pattern-matching is tied deeply both to ADTs and to functional programming, when it occurs outside of those contexts. Languages like Python 3, Ruby, Racket, and Swift fall outside of the traditional "functional programming" traditions but still have interesting pattern-matching abilities.
The author still doesn't know Haskell. Want an effectful loop? Control.Monad.Loops [0] contains many prebuilt loops, and they're written in standard Haskell. The ST monad [1] provides imperative variables and mutation. So does IO [2], if one insists on avoiding GHC for some reason. I agree that it's good to learn an ML, and Haskell is a fine choice, but the author needs to realize how drenched in Haskell memes they currently are, and to either learn more of Haskell itself, or to take a break from it for a while.
[0] https://hackage.haskell.org/package/monad-loops
[1] https://wiki.haskell.org/Monad/ST
[2] https://hackage.haskell.org/package/base/docs/Data-IORef.htm...
Perhaps one day we will be able to have similar discussions about sum types without constant interjections by functional programmers.
(a, (b, c)) = foo()
(fst, *middle, last) = foo()
which is nice but i wouldn't really call it pattern matching because i can't do something like match xs:
when (): ...
when (x, *rest): ...
i.e. do different things based on the shape of the data.A class in Java for example is a product type. As it describes a group of fields and their types. Same for a struct in C++.
Where I disagree with the article is that JavaScript does not have product types, because you do not have a definition of such a group of values in advance. A JS object does not list what values it groups and what type they each will have. A JS class comes closer in listing what values it will group, but not their types. Also, product types must be closed, and a JS class defines an open group, at runtime the object could group more then what it specified, unless explicitly frozen. You could say a frozen class defines a product of types where all types are the Object type and thus can be of any value, but that's a stretch, because product types are only as useful as they define a constrained product of possible values for the type.
A sum type is just a known list of possible types a value can take. That's the one people aren't as familiar with as most popular languages don't have a construct like it. It let's you say, this variable can be a String or an Int, but not both.
Often people say product type is AND while sum type is XOR. This variable contains a String AND an Int. They will both be there. So it contains both. That's a product type. This variable can contain a String XOR an Int, they can't be both contained, it is only one or the other. That's a sum type.
You can think in Java that all types are a sum type, they are either null or of their defined type. But null isn't a real type, more that it is an allowable value of all types. So it's a bit of a stretch.
Like the article said, best way to mimic them in popular languages is by extending an abstract class. Each type the variable can be will be a child of the parent. Thus a variable of type parent can be one and only one of its children. It's not full featured, you can't make existing types extend from the parent, so you can't arbitrarily define sum types using any available type. You also often can't list what are all the possible types of parent, depending on the reflection capabilities, knowing what all extend a type isn't always feasible. Also, the static type checkers don't consider these like sum types, so it won't tell you that you forgot to handle cases where parent is of one of its child type. Etc.
There is a lot of scala reference material regarding ADTs
> Keep persevering. Don’t give up. If you find that a bunch of blog posts don’t explain things in a way that makes sense, skip them. Keep looking until you find some well-written ones. The same goes for courses and tutorials. Everyone comes from different backgrounds. What makes sense for someone else might not work for you. And that’s OK.
I feel like this is advice everyone should hear, especially junior or mid level developers
Something I've found confusing with algeabric data types is the fact that they are not mathematical concepts. They originate as a programming construct and were invented as part of a programming language. They were then given a name that makes it sounds mathematical, but it was just an attempt at giving some kind of metaphorical meaning to them. Same as if I name a search engine Odin, because Odin was the God of wisdom. But if you thought it had anything to do with Norse mythology you'd similarly get confused.
People tried to find correspondence for them with mathematical theories. Thus, in Set theory they could correspond to Disjoint Unions of Cartesian Products. Or in category theory, they could correspond to Coproducts of products.
I guess you could say they came out of type theory, but I'm not sure of the history here fully. Did the extensions to simply typed Lambda calculus first came out of the theory or did it retroactively built a theory from the programming construct?
At any rate, my point is, these mathematical correspondence are very confusing. And I think it's wrong to try and understand these things from the mathematical angle unless you're a mathematician trained in one of the corresponding mathematical theories.
As such I think the article does a good job.
1. https://codewords.recurse.com/issues/three/algebra-and-calcu... 2. http://www.math.lsa.umich.edu/~ablass/7trees.pdf
I'm sure there's quite many things in programming language where you can use such correspondence to your advantage, not just ADTs. But we don't teach it starting from the mathematical correspondence and then back.
To put it more simply, most programming isn't thought by first teaching learners about theoretical computer science. Generally it happens either in parallel or you learn the theory after the fact. But for some reason, when it comes to functional programming it seems most teachers want to start with teaching you the theory.
And my above point is that, even historically, it is not always true that theory came first. Often times, the construct were invented and used practically from finding solutions to concrete problems, and later a theory around it was developed. In the case of ADTs, I'm not 100% sure which came first, but I know they were first implemented in the programming language Hope. Not sure if the theory was there prior or not.
I'd be curious to know if there's an original paper on ADTs somewhere about it.
https://math.stackexchange.com/questions/50375/whats-the-mea...
It is not that there are no mathematical correspondence, but ADTs are not constructs of any mathematical theories. There's no ADT in set theory. There's no ADT in category theory. The only place you might find them is in type theory, but that's unclear to me if that is the theory that defined them, or simply the theory used to define them mathematically after their invention in computer science. And like Gilles answer suggests, ADTs brings some concepts that complicates the mathematical formalism for them.
This is why the whole sum type and product type thing is loosy washy. You can say Java classes are product types, but they are not quite like Haskell's product types, etc. They aren't an implementation of a very well defined mathematical theory. Unlike say how Java and Haskell both have support for set theory and algebra built in.
I wouldn't say that this is true. Category theory has a definition of product of objects, sum/coproduct of objects, terminal object (unit type), and initial object (empty type). There is a correspondence between type and category theory where a certain type theory is the internal language of a certain category: https://ncatlab.org/nlab/show/computational+trinitarianism
Product: https://ncatlab.org/nlab/show/cartesian+product
Coproduct: https://ncatlab.org/nlab/show/coproduct
Terminal object: https://ncatlab.org/nlab/show/terminal+object
Initial object: https://ncatlab.org/nlab/show/initial+object