Mittenchops
Mittenchops

Reputation: 19724

Using alternative preludes in haskell

I'm interested in alternative Preludes. I understand there are many choices:

  1. https://hackage.haskell.org/packages/#cat:Prelude
  2. https://guide.aelve.com/haskell/alternative-preludes-zr69k1hc

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

Prelude> head [1]
1
Prelude> head []
*** Exception: Prelude.head: empty list

Foundation

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

Safe> head []

<interactive>:22:1: error: Variable not in scope: head :: [a0] -> t

Classy Prelude

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

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

RIO> head [1]

<interactive>:7:1: error:
    Variable not in scope: head :: [Integer] -> t

Protolude

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?

Upvotes: 2

Views: 1382

Answers (2)

Shersh
Shersh

Reputation: 9179

I'm one of the relude authors and I can provide a bit motivation regarding why relude choose this behaviour for head, tail, last and init functions.

Standard Prelude defines head in the following way:

head :: [a] -> a

Alternative preludes often define head as follows:

head :: [a] -> Maybe a

However, relude implements it with the following type signature instead:

head :: NonEmpty a -> a

This design decision makes the library less beginner-friendly (people might not expect such type of the head function) but on the other hand it makes the interface more type-safe.

Another reason for this: if you have function of type head :: [a] -> Maybe a, you can't express head :: NonEmpty a -> a using your Maybeised version of head. But if you have head that works with NonEmpty it's quite easy to implement head that returns Maybe a. And relude even has the viaNonEmpty function for that:

viaNonEmpty :: (NonEmpty a -> b) -> ([a] -> Maybe b)

See the docs with examples here:

Upvotes: 2

chi
chi

Reputation: 116174

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 Nothings. 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.

Upvotes: 12

Related Questions