I instructed a functional programming course at the university and wrote this to answer a few questions preemptively.
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.
The very first obstacle is GHC, the primary implementation of Haskell. This section concerns its tools and libraries.
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"]
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
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]
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]
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
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.
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 #-}
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.
There are exceptions that are kind of strange.
please :t undefined
undefined :: a
please undefined
*** Exception: Prelude.undefined
These issues arise from the formal system beneath.
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
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.
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
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
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
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.
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
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
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
Characters are reserved roughly so that
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
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.