In a nutshell, you need to build and destroy values.
Values ββare constructed by adopting a data constructor, which is a (possibly null) function and applies the required arguments. So far so good.
Random example (abuse of GADTSyntax )
data T where A :: Int -> T B :: T C :: String -> Bool -> T
Destruction is more complicated, because you need to take a value of type T and get information about 1) which constructor was used to create such a value, and 2) what arguments to the specified constructor.
Part 1) can be performed using the function:
whichConsT :: T -> Int -- returns 0,1,2 for A,B,C
Part 2) is more complicated. A possible option is to use forecasts
projA :: T -> Int -- projB not needed projC1 :: T -> String projC2 :: T -> Bool
so that, for example, they satisfy
projA (A n) = n projC1 (C xy) = x projC2 (C xy) = y
But wait! Projection types have the form T -> ... , which promises that such functions work on all values ββof type T So we can have
projA B = ?? projA (C xy) = ?? projC1 (A n) = ??
How to implement the above? There is no way to get reasonable results, so the best option is to run a runtime error.
projA B = error "not an A!" projA (C xy) = error "not an A!" projC1 (A n) = error "not a C!"
However, this puts a strain on the programmer! Now the programmer's responsibility is to verify that the values ββthat are passed to the projections have the correct constructor. This can be done using whichConsT . Many imperative programmers are used for this type of interface (testing and access, for example Java hasNext(), next() in iterators), but this is due to the fact that most imperative languages ββdo not have a really better option.
FP languages ββ(and, currently, some imperative languages) also allow you to map patterns. Using it has the following advantages over projections:
- No need to share information: we get 1) and 2) at the same time
- Program cannot be minimized: we never use partial projection functions that may be damaged
- no load on the programmer: a consequence of the above
- If integrity checking is enabled, we will certainly consider all possible cases.
Now that the types have exactly one constructor (tuples, () , newtype s), you can define general projections that work great (for example, fst,snd ). However, many prefer to stick to pattern matching, which can also handle the general case.