Pitfalls in Haskell

Datetime:2016-08-23 04:40:48          Topic:          Share

This text demonstrates the difficulties people often face when learning Haskell. It is perhaps biased by my own experiences, so it may emphasize things that C and Scheme have taught me. Those would be paranoia and flexibility.

Table of Contents

    • 1.1   Confusing Prompt
    • 1.2   Interactive Context
    • 1.3   Shadowing Definitions
    • 1.4   Unloading Modules
    • 1.5   Specialized Functions
    • 1.6   Monad Type Class
    • 1.7   Language Extensions
    • 1.8   List Comprehensions
    • 2.1   Type Conversions
    • 2.2   No Instance for Show
    • 2.3   Monomorphism Restriction
    • 2.4   Type Defaulting
    • 2.5   Integer Overflows
    • 2.6   The Bottom Type
    • 3.3   Negative Numbers
    • 3.4   Special Characters
    • 3.5   Naming Conventions

1   System

The very first obstacle is GHC, the primary implementation of Haskell. This section concerns its tools and libraries.

1.1   Confusing Prompt

The prompt used in interactive sessions tends to grow quite long as more modules are imported, so it is typical to change it. Do not be confused by strange prefixes like λ> or politeness.

Prelude Control.Monad Data.Functor> :set prompt "please "
please :set prompt2 "       "
please let this = Just "an example"
           that = "good idea"
please return this :: [Maybe String]
[Just "an example"]
please sequence it
Just ["an example"]

1.2   Interactive Context

Definitions in source files are just like mathematical equations. They are statements of what is true, so it is not possible to change them, and they do not have effects, so it is not possible to observe them either. That makes them unfit for interactive use.

Interactive sessions get around this limitation by working in the IO monad. That means interactive commands are akin to do -blocks, with the exception of some special cases like imports or data type definitions. The most visible consequence is that definitions need to be prefixed with let .

please let x = 2
please x
2

1.3   Shadowing Definitions

Redefining things during interactive sessions can be confusing, because new names simply shadow the old ones. Both definitions coexist, but the other one is unreachable without indirection.

please let f x = 5
           g = f
please [f 2, g 2]
[5, 5]
please let f x = 2
please [f 2, g 2]
[2, 5]

1.4   Unloading Modules

There is an alternative syntax for loading modules in interactive sessions.

please map negate [1 .. 3]
[-1, -2, -3]
please :m + Data.List Data.Map

It is useful for loading multiple modules at the same time or unloading modules, both of which are normally impossible.

please map negate [1 .. 3]
<interactive>:2:1:
    Ambiguous occurrence `map'
    It could refer to either `Data.Map.map',
                             imported from `Data.Map'
                          or `Prelude.map',
                             imported from `Prelude'
please :m - Data.Map
please map negate [1 .. 3]
[-1, -2, -3]

1.5   Specialized Functions

Many functions are specialized versions of more general ones.

please :t map
map :: (a -> b) -> [a] -> [b]
please :t (<$>)
(<$>) :: Functor f => (a -> b) -> f a -> f b

They exist to help convey intent and sometimes to allow for performance optimizations. It is up to the user to decide the level of abstraction they want to work at.

please :t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
please :t (<<<)
(<<<) :: Category cat => cat b c -> cat a b -> cat a c

1.6   Monad Type Class

A constraint that relates monads and applicative functors is missing in the standard library, because Monad was created before Applicative .

class Applicative a => Monad a where
      (>>=) :: m a -> (a -> m b) -> m b
      (>>) :: m a -> m b -> m b
      return :: a -> m a
      fail :: String -> m a

It is also the reason why both pure and return exist.

1.7   Language Extensions

The best source for confusing features is language extensions. They are unofficial features that are not in the standard, but may one day be, if they survive their trial by fire (namely: users). Some of them are fun and some break on every update.

They have to be turned on explicitly.

please :set -XGADTs
please {-# LANGUAGE GADTs #-}

1.8   List Comprehensions

List comprehensions are essentially retarded monad comprehensions. They work like do -blocks or explicit binding,

please [x * y | x <- [1 .. 3], y <- [x .. 3]]
[1, 2, 3, 4, 6, 9]
please do x <- [1 .. 3]
          y <- [x .. 3]
          return $ x * y
[1, 2, 3, 4, 6, 9]
please [1 .. 3] >>= \ x -> [x .. 3] >>= \ y -> return $ x * y
[1, 2, 3, 4, 6, 9]

but only with lists.

please Just 2 >>= \ x -> Just x >>= \ y -> return $ x * y
Just 4
please do x <- Just 2
          y <- Just x
          return $ x * y
Just 4
please [x * y | x <- Just 2, y <- Just x]
<interactive>:6:15:
    Couldn't match expected type `[b]' with actual type `Maybe a'
    In the return type of a call of `Just'
    In the expression: Just 2
    In a stmt of a list comprehension: x <- Just 2
<interactive>:6:28:
    Couldn't match expected type `[b]' with actual type `Maybe b'
    In the return type of a call of `Just'
    In the expression: Just x
    In a stmt of a list comprehension: y <- Just x

Language extensions fix that.

please :set -XMonadComprehensions
please [x * y | x <- Just 2, y <- Just x]
Just 4

Do not be afraid to use language extensions when it is appropriate.

1.9   Exceptions

There are exceptions that are kind of strange.

please :t undefined
undefined :: a
please undefined
*** Exception: Prelude.undefined

2   Semantics

These issues arise from the formal system beneath.

2.1   Type Conversions

Some functions return concrete types.

please let f xs = sum xs / length xs
<interactive>:1:19:
    No instance for (Fractional Int) arising from a use of `/'
    Possible fix: add an instance declaration for (Fractional Int)
    In the expression: sum xs / length xs
    In an equation for `f': f xs = sum xs / length xs

They need to be manually promoted to more generic types, since there are no implicit type conversions.

please let f xs = sum xs / fromIntegral (length xs)
please import Data.List
please let f xs = sum xs / genericLength xs

2.2   No Instance for Show

It is not possible to print arbitrary data types by default.

please data Type a = T a
please T 2
<interactive>:2:1:
    No instance for (Show (Type a)) arising from a use of `print'
    Possible fix: add an instance declaration for (Show (Type a))
    In a stmt of an interactive GHCi command: print it

Doing so requires defining the show function from the Show type class. It is a fairly obvious function. The only unexpected thing is that the result should be valid code that can be fed to read . You can write it by hand

please instance Show a => Show (Type a) where
                show (T a) = "T " ++ show a
please T 2
T 2

or derive it automatically.

please data Type a = T a deriving Show
please T 2
T 2

Both have their uses, but the latter is more convenient.

2.3   Monomorphism Restriction

The Haskell 98 report mentions a strange type system restriction. It is called the monomorphism restriction and concerns making decisions about types during inference. It sounds like abstract nonsense, but makes sense in the context of category theory, where morphisms are a generalization of functions and monomorphisms in particular correspond to injective functions. Regardless of its cryptic name, it solves a practical problem: it prevents ambiguous types from appearing.

The restriction can cause seemingly nonsensical errors and is especially common in interactive sessions.

please let f x = return x
please :t f
f :: Monad m => a -> m a
please let f = return
<interactive>:3:9:
    No instance for (Monad m) arising from a use of `return'
    The type variable `m' is ambiguous
    Possible fix: add a type signature that fixes these type variable(s)
    Note: there are several potential instances:
      instance Monad ((->) a) -- Defined in `GHC.Base'
      instance Monad IO -- Defined in `GHC.Base'
      instance Monad [] -- Defined in `GHC.Base'
      ...plus six others
    In the expression: return
    In an equation for `f': f = return

The correct way to work around it is to explicitly specify the types of all top level symbols.

please let f :: Monad m => a -> m a
           f x = return x

The lazy way is to switch on a language extension that removes the restriction (and all of its benefits).

please :set -XNoMonomorphismRestriction
please let f = return
please :t f
f :: Monad m => a -> m a

2.4   Type Defaulting

Another type inference problem is type defaulting. The most generic type is not always the one that is chosen, so the outcome can be unexpected and therefore troublesome.

please :t 2
2 :: Num a => a
please let x = 2
please :t x
x :: Integer

The solution is, again, to use type signatures or hope for the best.

please let x :: Num a => a
           x = 2
please :t x
x :: Num a => a

2.5   Integer Overflows

Types defaulting to Integer instead of Int cause the illusion that integer overflows do not exist. Not having direct access to memory and lacking a special size type, like size_t in C, also contribute to it.

Integer overflows are real and dangerous.

please 42 ^ 13
1265437718438866624512
please :t it
it :: Integer
please length [1 .. 42] ^ 13
-7387622647092436992
please :t it
it :: Int

2.6   The Bottom Type

The monomorphism restriction takes care of ambiguous types, but there are other special cases the type system chokes on. Functions that do not terminate and those that throw exceptions are among them.

please f :: a -> b
       f x = f x

Their return types seem arbitrary, because a mathematical entity called the bottom type, often written or _|_ , is not a visible part of the type system.

Total languages like Coq do not have this problem, but they are not Turing complete either.

3   Syntax

3.1   Indentation

Statements need to be indented correctly to avoid ambiguous parsing. The if -condition is especially confusing to beginners, because it works until it is placed in another construct that causes a conflict.

if p
then c
else a

Luckily there are many ways to do it right.

if p
   then c
   else a
if p then
   c else
   a

The right ways are more obvious with other constructs.

case p of
     True -> c
     False -> a

3.2   Spaces

Sometimes spaces do not make a difference and sometimes they do.

please import Foreign as F
please (not.F.toBool) 1
False
please (not . F . toBool) 1
<interactive>:3:8: Not in scope: data constructor `F'

It is best to be careful and establish a consistent style.

please (not . F.toBool) 1
False

3.3   Negative Numbers

The operator - is difficult to apply partially as negative numbers may be written with a space between the sign and the magnitude.

please (/ 2) 5
2.5
please (- 2) 5
<interactive>:2:2:
    No instance for (Num (a -> b))
      arising from a use of syntactic negation
    Possible fix: add an instance declaration for (Num (a -> b))
    In the expression: - 2
    In the expression: (- 2) 5
    In an equation for `it': it = (- 2) 5

The subtract function is useful for working around the problem.

please (subtract 2) 5
3

3.4   Special Characters

Characters are reserved roughly so that

  • lowercase letters are for values,
  • uppercase letters are for types and
  • special characters are for operators.

There are a few exceptions that are surprising. For example : is used in operators for types

please let x + y = 5 in 2 + 2
5
please let x :+ y = 5 in 2 :+ 2
<interactive>:2:7: Not in scope: data constructor `:+'
<interactive>:2:21: Not in scope: data constructor `:+'
please data Type a b = a :+ b
please data Type a b = a + b
<interactive>:4:17: Not a data constructor: `a'

and ~ is used for controlling pattern matching.

please let x ~~ y = 5 in 2 ~~ 2
5
please let x ~ y = 5 in 2 ~ 2
<interactive>:6:20: Pattern syntax in expression context: ~2
please let x ~y = 5 in x 2
5

3.5   Naming Conventions

Some names are inconsistent. For example Functor is not called Mappable while Traversable is not named after abstract nonsense. It is best to focus on what things are instead of what they are called.