
Algebraic Data Types Zoo
In this article we will try to consider the whole variety of Algebraic Data Types.

I must say, the task is quite heavy, and to understand a person if he had not dealt with Algebraic Types before is not very simple.
ADTs were first used in the Hope language, but they gained major popularity thanks to the ML languages such as Standart ML, OCaml, F #, and the Haskell language.
Today ADTs are supported in one way or another in a much larger number of languages: Scala, Rust, Nemerle, Racket, ...
ADT is a universal data type. Using it, you can imagine most data types.
ADTs are called algebraic, because they can be represented as a kind of algebraic composition of the types of its components. This knowledge gives its advantage: understanding the properties of algebraic composition, you can calculate which type is necessary for constructing certain data.
We will consider types based on the Haskell language, however, similar with slight changes can be achieved in other languages with support for ADT.
It should be noted that the type theories developed in the twentieth century by Russell, Church, Martin-Löf gained such popularity that now (already in the twenty-first century) the theory of Homotopy Type Theory appeared, which attempted to explain the foundation of mathematics.
Our task is to look at Algebraic Data Types. Often, ADT data is called boxed or wrapped. They are so named because they look like data packed in a box. The most interesting thing is that even the implementation does not mean storing data, but pointers. In addition to ease of implementation, this provides the effective mechanisms of laziness that Haskell has.
In fairness, I note that Haskell allows you to create non-lazy, including non-wrapped data, which helps in processing highly loaded data volumes.
In Haskell, there are 2 ways to create an ADT using the
In Haskell there are not many, first of all it is these types of data, such as
It's not that you can not make these types of this (via ATD), a computer is able to work with numbers, and sin not to use it.
I want to draw attention to the fact that all types are written with a capital letter, this is due to the fact that all type and data constructors in Haskell are written exclusively with a capital letter.
Most imperative languages are designed in such a way that it is convenient to create programs from bottom to top in hierarchical complexity.
In Haskell, it is very easy to write code from top to bottom. In this connection, it became necessary to have empty data types (without implementations), since it is known what this data type will do, but it is not yet decided how to fill it out. An extension has appeared that allows you to do this.
These are not real, pseudo-null, empty data types.
The syntax, as we see it, is simple to disgrace.
Here we created 3 types of data, the second of which is parametric. We see what
We also see that the constructor is at the beginning of the type, and then the data is, as it were, packed into a box.
You ask why they are needed?
For example, we can write the following valid function code:
Of course, in most cases it is impossible to write all the functions without internal knowledge of the data, but you can leave some of the functions for later, doing other tasks, such as this:
This program will be compiled and checked for errors, despite the fact that neither the data nor part of the functions are defined.
The true null data type looks unusual enough for the untrained eye, but we can figure it out.
It was found relatively recently, it is still not in the Platform (so that it can be used out of the box), and is still little used.
Single data types give nothing but knowledge of what it is.
Of the common data types used
hereinafter commentary
In almost all programs, the signature is used:
because the program must return something. Therefore, it
Or, here's another well-known unit type:
the same, only regular constructors are used.
I must say that in these types there is no recursion, that is, you can write:
the fact is that the type namespace does not intersect with the type constructor space, so the compiler understands where there is recursion and where not.
Types of works are named so that they can be represented as the product of several types that make up it.
Very popular data types are used universally.
The most famous of them are tuples or stupid. Haskell uses special syntactic sugar for this:
we can write the same thing using ordinary constructors:
Wherever you need to store more than one given in the data - this is the type of work.
Total data types are named because they summarize the data types that make up them.
For example, if you summarize single data types, an enumeration is obtained.
Presenting one of the most famous listings:
also a famous enumeration is the type
If we move on to more complex ADTs, we need to recall the zero-abelian type:
Data that has a field for the result when it is not, so to speak,
If you need to learn more about the error, an even more complex data type is used:
a data type where there is either a “left” result (error description or the error itself) or a “correct” result.
Also this data type is used for binary selection.
You can compose more exotic data types, for example:
here we either have no result, or 1 result, or 2nd, or both 1st and 2nd.
In Haskell, you can easily create recursive data types.
The best known syntax sugar type is lists:
However, it can be easily rewritten with the words:
I want to note that special characters can be used as constructors, where the first character is necessarily a colon
Want to build a tree? No problem.
For example, a simple binary tree:
Or else, natural numbers:
If you have not yet opened a spoiler about a null data type (or opened, but did not understand) - it's time to open and read and understand that an
Do not think that only data can be written to the ADT. Functions can also easily be written in the ADT: power types (the general type has the power of raising to the power of one data type the power of another):
If we use the ADT of works, where the data will be mixed with functions, this is nothing more than records, and very similar structures to objects.
Sometimes you have to create quite unusual behavior, and you have to invent types that do nothing, but pretend they do. For this, in particular, phantom types are used. One of the most famous types is:
As you can see, the type header says that the type is parametric, but in reality it is a dummy.
These data types are often used as universal gags, as they are easily converted from one data type to another.
The type of works is so often used that there is a special syntactic sugar that helps to work with it easier and more conveniently (with built-in getters and setters). These types are called records.
May we have a type
much more convenient to rewrite to:
Inserts are used where it is necessary either to hide the type implementation, to protect it from outside interference, or to give other properties than the mother type.
It is written in a similar way (with the syntax of the entries):
Existential data types are named so because of the existence quantifier ∃.
The paradox is that in Haskell there is no such quantifier.
But there is a quantifier of universality ∀. But these quantifiers can be easily transformed into each other.
To be completely precise, then every existentially-quantified type of rank
As you can see, they were able to create a heterogeneous list, despite the fact that it is homogeneous.
But something looks like an object:
With functions written with an underscore, we cannot work directly.
True, working with such objects is not at all like working with OOP:
Generalized ADTs differ from ordinary ones in that they trim and specialize the final type.
Haskell uses a “functional” record of this data. Let's rewrite a simple data type in the new syntax:
We see that the final data type is the same for all:
If we have an abstract tree:
it’s easy to build a function to calculate a similar tree:
Sometimes it really hurts to limit parametric data types. They are cut constraint type (kind) type.
View - this is essentially a type of type, for example
And here is the data with a restriction on the parameter:
Basically, these types are necessary for very high levels of abstraction.
Algebraic Data Types is a very simple, universal, elegant, easily extensible and powerful toolkit for creating custom data types for very many programmers needs.


I must say, the task is quite heavy, and to understand a person if he had not dealt with Algebraic Types before is not very simple.
ADTs were first used in the Hope language, but they gained major popularity thanks to the ML languages such as Standart ML, OCaml, F #, and the Haskell language.
Today ADTs are supported in one way or another in a much larger number of languages: Scala, Rust, Nemerle, Racket, ...
ADT is a universal data type. Using it, you can imagine most data types.
ADTs are called algebraic, because they can be represented as a kind of algebraic composition of the types of its components. This knowledge gives its advantage: understanding the properties of algebraic composition, you can calculate which type is necessary for constructing certain data.
We will consider types based on the Haskell language, however, similar with slight changes can be achieved in other languages with support for ADT.
It should be noted that the type theories developed in the twentieth century by Russell, Church, Martin-Löf gained such popularity that now (already in the twenty-first century) the theory of Homotopy Type Theory appeared, which attempted to explain the foundation of mathematics.
HoTT
If anyone is interested in theory, you can read for free fresh work in English Homotopy Type Theory: Univalent Foundations of Mathematics
Our task is to look at Algebraic Data Types. Often, ADT data is called boxed or wrapped. They are so named because they look like data packed in a box. The most interesting thing is that even the implementation does not mean storing data, but pointers. In addition to ease of implementation, this provides the effective mechanisms of laziness that Haskell has.
In fairness, I note that Haskell allows you to create non-lazy, including non-wrapped data, which helps in processing highly loaded data volumes.
In Haskell, there are 2 ways to create an ADT using the
data
or declaration newtype
. The difference between the two is thatnewtype
de facto not wrapped, and therefore cheaper (by machine resources), and therefore more profitable, but with it you can record only a narrow circle of ADTs, so it is not suitable in the general case.Data Types Implemented Through Primitives
In Haskell there are not many, first of all it is these types of data, such as
Int
, Integer,
Float
, Double
, Char
, ... It's not that you can not make these types of this (via ATD), a computer is able to work with numbers, and sin not to use it.
I want to draw attention to the fact that all types are written with a capital letter, this is due to the fact that all type and data constructors in Haskell are written exclusively with a capital letter.
Zero Data Types
Most imperative languages are designed in such a way that it is convenient to create programs from bottom to top in hierarchical complexity.
In Haskell, it is very easy to write code from top to bottom. In this connection, it became necessary to have empty data types (without implementations), since it is known what this data type will do, but it is not yet decided how to fill it out. An extension has appeared that allows you to do this.
These are not real, pseudo-null, empty data types.
data S
data T a
data K a b
The syntax, as we see it, is simple to disgrace.
Here we created 3 types of data, the second of which is parametric. We see what
a
is written with a lowercase letter, which means it cannot be a constructor. Conclusion - This option, which can substitute any type, it is possible to have the following types: T Int,
T Char
, T S
, T (T Double)
and even T (T ( T String))
. We also see that the constructor is at the beginning of the type, and then the data is, as it were, packed into a box.
You ask why they are needed?
For example, we can write the following valid function code:
doubleT :: T a -> (T a, T a)
doubleT t = (t, t)
Of course, in most cases it is impossible to write all the functions without internal knowledge of the data, but you can leave some of the functions for later, doing other tasks, such as this:
foo :: S -> Int -> T a
foo = undefined
nextT :: S -> Int -> (Int, T a)
nextT s i = (i+1, foo s (i+1))
This program will be compiled and checked for errors, despite the fact that neither the data nor part of the functions are defined.
The true null data type looks unusual enough for the untrained eye, but we can figure it out.
It was found relatively recently, it is still not in the Platform (so that it can be used out of the box), and is still little used.
Void
Here we see a recursive type (we will talk about this later), first comes the type name -
If we rename, this will not change anything except our understanding:
This data type is infinitely looped on itself.
newtype Void = Void Void
Here we see a recursive type (we will talk about this later), first comes the type name -
Void
, then =
, then the constructor of this type Void
and the parameter, which is the type Void
. If we rename, this will not change anything except our understanding:
newtype VoidData = Void VoidData
This data type is infinitely looped on itself.
Unit Data Types
Single data types give nothing but knowledge of what it is.
Of the common data types used
data () = () --X
hereinafter commentary
-- X
data that is so defined will be shown, but it cannot be determined by itself because invalid characters are used. In almost all programs, the signature is used:
main :: IO ()
because the program must return something. Therefore, it
()
is the most popular type when you need to return anything. Or, here's another well-known unit type:
data Unit = Unit
the same, only regular constructors are used.
I must say that in these types there is no recursion, that is, you can write:
data UnitData = Unit
the fact is that the type namespace does not intersect with the type constructor space, so the compiler understands where there is recursion and where not.
Types of works
Types of works are named so that they can be represented as the product of several types that make up it.
Very popular data types are used universally.
The most famous of them are tuples or stupid. Haskell uses special syntactic sugar for this:
data (,) a b = (,) a b --X (a, b)
data (,,) a b c = (,,) a b c --X (a, b, c)
data (,,,,) a b c d = (,,,,) a b c d --X (a, b, c, d)
we can write the same thing using ordinary constructors:
data Pair a b = Pair a b
data Triple a b c = Triple a b c
Wherever you need to store more than one given in the data - this is the type of work.
Total Data Types
Total data types are named because they summarize the data types that make up them.
For example, if you summarize single data types, an enumeration is obtained.
Presenting one of the most famous listings:
data Bool = False | True
also a famous enumeration is the type
Ordering
used in comparisonsdata Ordering = LT | EQ | GT
If we move on to more complex ADTs, we need to recall the zero-abelian type:
data Maybe a = Nothing | Just a
Data that has a field for the result when it is not, so to speak,
null
from many programming languages.() :: Int -> Int -> Maybe Int
_ 0 = Nothing
a b = Just (a `div` b)
If you need to learn more about the error, an even more complex data type is used:
data Either a b = Left a | Right b
a data type where there is either a “left” result (error description or the error itself) or a “correct” result.
Also this data type is used for binary selection.
type IdPerson = Int
type ErrorMsg = String
personChecker :: Person -> Either ErrorMsg IdPerson
personChecker p = if age p < 0 then Left "this guy is not born yet!"
else if null $ first_name p then Left "this guy is unnamed!"
else Right $ registeredId p
You can compose more exotic data types, for example:
data Both a b = None | First a | Second b | Both a b
here we either have no result, or 1 result, or 2nd, or both 1st and 2nd.
Recursive Data Types
In Haskell, you can easily create recursive data types.
The best known syntax sugar type is lists:
data [] a = [] | a : [a] --X
However, it can be easily rewritten with the words:
data List a = Nil | Cons a (List a)
I want to note that special characters can be used as constructors, where the first character is necessarily a colon
(:)
, then they use infix notation. Want to build a tree? No problem.
For example, a simple binary tree:
data Tree a = Leaf a | Branch (Tree a) (Tree a)
data Tree2 a = Empty | Branch2 (Tree2 a) a (Tree2 a)
Or else, natural numbers:
data Nat = Zero | Succ Nat
If you have not yet opened a spoiler about a null data type (or opened, but did not understand) - it's time to open and read and understand that an
Void
elementary infinitely recursive data type.Power Data Types
Do not think that only data can be written to the ADT. Functions can also easily be written in the ADT: power types (the general type has the power of raising to the power of one data type the power of another):
data Fun a b = Fun (a -> b)
newtype ContT r m a = Writer ((a -> m r) -> m r)
If we use the ADT of works, where the data will be mixed with functions, this is nothing more than records, and very similar structures to objects.
Phantom Data Types
Sometimes you have to create quite unusual behavior, and you have to invent types that do nothing, but pretend they do. For this, in particular, phantom types are used. One of the most famous types is:
data Proxy a = Proxy
As you can see, the type header says that the type is parametric, but in reality it is a dummy.
These data types are often used as universal gags, as they are easily converted from one data type to another.
proxyСonverter :: Proxy a -> Proxy b
proxyСonverter Proxy = Proxy
Posts
The type of works is so often used that there is a special syntactic sugar that helps to work with it easier and more conveniently (with built-in getters and setters). These types are called records.
May we have a type
data Sex = Female | Male
data Person = Person String String Int Int Sex (String -> String)
much more convenient to rewrite to:
data Sex = Female | Male
data Person = Person { lastName :: String
, firstName :: String
, age :: Int
, socialId :: Int
, sex :: Sex
, greeting :: String -> String
}
Inserts or Wrappers
Inserts are used where it is necessary either to hide the type implementation, to protect it from outside interference, or to give other properties than the mother type.
It is written in a similar way (with the syntax of the entries):
newtype Wrapper a = Wrapper { unwrapper :: a }
newtype Dollar = Dollar Int
Existential data types
Existential data types are named so because of the existence quantifier ∃.
The paradox is that in Haskell there is no such quantifier.
But there is a quantifier of universality ∀. But these quantifiers can be easily transformed into each other.
To be completely precise, then every existentially-quantified type of rank
n+1
can be transformed into a universally-quantified type of rank n
.data HeteroData = forall a. HeteroData a
heteroList :: [HeteroData]
heteroList = [HeteroData 3.7, HeteroData "message", HeteroData True]
As you can see, they were able to create a heterogeneous list, despite the fact that it is homogeneous.
But something looks like an object:
data Counter a = forall self. NewCounter
{ _this :: self
, _inc :: self -> self
, _display :: self -> IO ()
, tag :: a
}
With functions written with an underscore, we cannot work directly.
True, working with such objects is not at all like working with OOP:
Existential Variable Record
inc :: Counter a -> Counter a
inc (NewCounter x i d t) = NewCounter { _this = i x, _inc = i, _display = d, tag = t }
display :: Counter a -> IO ()
display NewCounter{ _this = x, _display = d } = d x
counterA :: Counter String
counterA = NewCounter { _this = 0, _inc = (1+), _display = print, tag = "A" }
counterB :: Counter String
counterB = NewCounter { _this = "", _inc = ('#':), _display = putStrLn, tag = "B" }
main = do
display (inc counterA) -- prints "1"
display (inc (inc counterB)) -- prints "##"
Generic Algebraic Data Types (GADTs)
Generalized ADTs differ from ordinary ones in that they trim and specialize the final type.
Haskell uses a “functional” record of this data. Let's rewrite a simple data type in the new syntax:
data Maybe a = Nothing | Just a
data Maybe a where
Nothing :: Maybe a
Just :: a -> Maybe a
-- --------------------
data List a = Nil | Cons a (List a)
data List a where
Nil :: List a
Cons :: a -> List a -> List a
We see that the final data type is the same for all:
Maybe a
(or List a
). OATD allow you to have different resulting data types. If we have an abstract tree:
data Term a where
Lit :: Int -> Term Int
Succ :: Term Int -> Term Int
IsZero :: Term Int -> Term Bool
If :: Term Bool -> Term a -> Term a -> Term a
Pair :: Term a -> Term b -> Term (a,b)
it’s easy to build a function to calculate a similar tree:
eval :: Term a -> a
eval (Lit i) = i
eval (Succ t) = 1 + eval t
eval (IsZero t) = eval t == 0
eval (If b e1 e2) = if eval b then eval e1 else eval e2
eval (Pair e1 e2) = (eval e1, eval e2)
Parametric data types with limited parameters
Sometimes it really hurts to limit parametric data types. They are cut constraint type (kind) type.
View - this is essentially a type of type, for example
Just 3 :: (Maybe Int :: *)
And here is the data with a restriction on the parameter:
data F (a :: * -> *) where ...
Basically, these types are necessary for very high levels of abstraction.
Vector type
Vector with natural numbers as length
data Ze
data Su n
data Vec :: * -> * -> * where
Nil :: Vec a Ze
Cons :: a -> Vec a n -> Vec a (Su n)
Conclusion
Algebraic Data Types is a very simple, universal, elegant, easily extensible and powerful toolkit for creating custom data types for very many programmers needs.
