4 ms·
I came to a similar insight by the corollary: a sound type system makes the edge cases obvious. When we make the type of a partial function total by introducing
by agentultra 7y ago
I came to a similar insight by the corollary: a sound type system makes the edge cases obvious. When we make the type of a partial function total by introducing an optional value as a result there's an opportunity to develop an intuition that this is going to make my code harder to reason about. Assuming I fix all the warnings about incomplete pattern matches I know that users of this code will have to handle all of the edge cases I'm introducing. Therefore I might take another approach and introduce a type as my pre-condition: the type of non-empty lists. You can't even call my function unless I can be assured there's at least one element in the input. I haven't even had to look at the implementation to reason about my code yet.
head :: [a] -> a
-- versus
saferHead :: [a] -> Maybe a
-- versus
safeHead :: NonEmpty a -> a
The first function is partial because we'll get an exception if we give it the empty list.
The second function is total but annoying to use because we're telling all the code that uses the result to check for two cases (it's an error not to).
The last one forces the responsibility on the caller to provide a non-empty input.
That was probably one of the more frustrating aspects about learning to program Haskell as someone who has been programming for more than fifteen years when I started. It revealed to me in stunning detail all of the edge cases that almost every other language I've used actively hides from me.
It doesn't absolve you of having to think about edge cases: even Haskell throws run-time exceptions. However it does give you tools to think about many of those edge cases up-front and in a direct way.
Update Added a trivial example to demonstrate "partial," etc.
- BoiledCabbage 7y ago100% that's a big part of it (although there are others). Although if someone doesn't know haskell, then the above notation for functions getting the first element of a list is probably not of much value to them. Clearer would be: char head(List<char> x); char? saferHead(List<char> x); char safeHead(NonEmptyList<char> x);
- lgas 7y agoThere's also another option in the middle which is a version that takes a value to return when the list is empty headOr :: a -> [a] -> a which is similar to the Maybe solution but forces the caller to discharge the Maybe immediately rather than letting them potentially clutter up the rest of their code by proliferating the Maybe.