In a nutshell, one needs to construct and destruct values.
Values are constructed by taking a data constructor which is a (possibly null-ary) function, and applying the required arguments. So far, so good.
Random example (abusing GADTSyntax
)
data T where
A :: Int -> T
B :: T
C :: String -> Bool -> T
Destruction is more complex, since one needs to take a value of type T
and obtain information about 1) which constructor was used to craft such value, and 2) what are the arguments to said constructor.
Part 1) could be done through a function:
whichConsT :: T -> Int -- returns 0,1,2 for A,B,C
Part 2) is more tricky. A possible option is to use projections
projA :: T -> Int
-- projB not needed
projC1 :: T -> String
projC2 :: T -> Bool
so that e.g. they satisfy
projA (A n) = n
projC1 (C x y) = x
projC2 (C x y) = y
But wait! The types of the projections are of the form T -> ...
, which promises that such functions work on all values of type T
. So we can have
projA B = ??
projA (C x y) = ??
projC1 (A n) = ??
How to implement the above? There's no way to produce sensible results, so the best option is to trigger a runtime error.
projA B = error "not an A!"
projA (C x y) = error "not an A!"
projC1 (A n) = error "not a C!"
However, this puts a burden on the programmer! Now it is the programmer's responsibility to check that values which are passed to the projections have the right constructor. This can be done using whichConsT
. Many imperative programmers are used to this kind of interface (test & access, e.g. Java's hasNext(), next()
in iterators), but this is because most imperative languages have no really better option.
FP languages (and, nowadays, some imperative languages as well) also allow pattern matching. Using it has the following advantages over projections:
- no need to split the information: we get 1) and 2) at the same time
- no way to crash the program: we never use partial projection functions which can crash
- no burden on the programmer: corollary of the above
- if the exhaustiveness-checker is on, we are sure to handle all the possible cases
Now, on types having exactly one constructor (tuples, ()
, newtype
s), one can define total projections, which are perfectly fine (e.g. fst,snd
). Still, many prefer to stick with pattern matching, which can also handle the general case as well.
fst
andsnd
either to understand the function... – Random Devfst
andsnd
I can see that there might be a readability advantage. – Zelphir KaltstahladdVectors (x₁,y₁) (x₂,y₂) = (x₁+x₂, y₁+y₂)
. – leftaroundabout