I'm interested in alternative Preludes. I understand there are many choices:
I understand one simple thing a lot of them fix is text, and another is in functions like head
that error pretty hard when you might prefer they are safer.
However, when I try to use these alternatives, the behavior in head
, hmm, just seems to break the function completely, and doesn't look like an improvement to me. Here are some examples:
Prelude> head [1]
1
Prelude> head []
*** Exception: Prelude.head: empty list
Foundation> head [1]
<interactive>:6:6: error:
• Couldn't match expected type ‘NonEmpty c’
with actual type ‘[Integer]’
• In the first argument of ‘head’, namely ‘[1]’
In the expression: head [1]
In an equation for ‘it’: it = head [1]
• Relevant bindings include
it :: foundation-0.0.21:Foundation.Collection.Element.Element c
(bound at <interactive>:6:1)
Foundation> head []
<interactive>:7:6: error:
• Couldn't match expected type ‘NonEmpty c’ with actual type ‘[a0]’
• In the first argument of ‘head’, namely ‘[]’
In the expression: head []
In an equation for ‘it’: it = head []
• Relevant bindings include
it :: foundation-0.0.21:Foundation.Collection.Element.Element c
(bound at <interactive>:7:1)
Safe> head []
<interactive>:22:1: error: Variable not in scope: head :: [a0] -> t
ClassyPrelude> head [1]
<interactive>:24:6: error:
• Couldn't match expected type ‘NonNull mono’
with actual type ‘[Integer]’
• In the first argument of ‘head’, namely ‘[1]’
In the expression: head [1]
In an equation for ‘it’: it = head [1]
• Relevant bindings include
it :: Element mono (bound at <interactive>:24:1)
Relude> head [1]
<interactive>:27:6: error:
• Couldn't match expected type ‘NonEmpty a’
with actual type ‘[Integer]’
• In the first argument of ‘head’, namely ‘[1]’
In the expression: head [1]
In an equation for ‘it’: it = head [1]
• Relevant bindings include it :: a (bound at <interactive>:27:1)
RIO> head [1]
<interactive>:7:1: error:
Variable not in scope: head :: [Integer] -> t
Protolude> head [1]
Just 1
Protolude> head []
Nothing
This looks good---it also works for tail, right?
Protolude> tail [1]
<interactive>:12:1: error:
• Variable not in scope: tail :: [Integer] -> t
• Perhaps you meant ‘tails’ (imported from Protolude)
Protolude> tails [1]
[[1],[]]
Protolude> tails []
[[]]
Well, that's not exactly a drop-in replacement.
What am I missing in why this is better, why these functions have been defined if they're just going to fail?
In most cases, they are being introduced because they fail at compile time instead of runtime.
The problem with Prelude.head
is not (only) that it can fail. It is that it has to, since there is no way to take a list [a]
and always produce an element a
, since the input list might be empty. There is no easy fix that is a drop-in replacement, a radical change is needed.
A safer, and arguably better prelude can address this issue in one of the following ways:
remove head
, so that the programmer won't use a dangerous tool. Any use of head
will fail, at compile time. Not great, but OK.
restrict the input type, e.g. head :: NonEmptyList a -> a
. This will be usable, but the programmer has to adapt the code so to guarantee that the input list is really non empty. Just passing a nonempty list won't do for the compiler -- the compiler wants a proof, and rightly so. The good news is that the previous code will be littered with compile errors, which will help the programmer spot the parts of the program which need to be fixed.
restrict the output type, e.g. head :: [a] -> Maybe a
. This can be used just fine, but the programmer will need to cope with the different result type, and handle all the potential Nothing
s. Again, the compile time errors will help the programmer to identify where some fixes are needed.
In any case, the programmer has to modify the code. There's no way around it. However, once the compile time errors are resolved, the program is guaranteed to never produce head: empty list
errors at runtime.