Learning Haskell: Signaling Adversity

posted 2 years ago

Stop Worrying and Love Nothing

Consider the definition of Maybe:

data Maybe a = Nothing | Just a

This is a very common datatype in Haskell because it allows you to return a default Nothing value when there isn't a sensible value to return for the intended type a.

Take the example:

ifEvenAdd2 :: Integer -> Integer
ifEvenAdd2 n =
  if even n then n + 2 else ??? -- what should we put here???

Instead of promising an Integer result, we can return Maybe Integer.

ifEvenAdd2 :: Integer -> Maybe Integer
ifEvenAdd2 n =
  if even n then Just (n + 2) else Nothing

Smart Constructors for Datatypes

Let's consider a Programmer type which keeps track of two things, their name and age:

type Name = String
type Age = Integer

data Programmer = Programmer Name Age deriving Show

There is a problem here. We could construct a Programmer with an empty String or make a Programmer with a negative age.

Let's fix it with Maybe:

type Name = String
type Age = Integer

data Programmer = data Programmer Name Age deriving Show

mkProgrammer :: Name -> Age -> Maybe Programmer
mkProgrammer name age
  | name /= "" && age >= 0 =
    Just $ Programmer name age
  | otherwise = Nothing

mkProgrammer is called a smart constructor. It allows us to construct values of a type only when they meet certain criteria.

Either

If we want to express why we didn't get a successful result back from our mkProgrammer constructor, we can use the Either datatype.

data Either a b = Left a | Right b

Let's make a sum type to enumerate our possible failure modes:

data ProgrammerInvalid = 
    NameEmpty
  | AgeTooLow
  deriving (Eq, Show)

The constructor type changes to:

mkProgrammer :: Name -> Age -> Either ProgrammerInvalid Programmer

Logic changes:

type Name = String
type Age = Integer

data Programmer = Programmer Name Age deriving SShow

data ProgrammerInvalid = 
    NameEmpty
  | AgeTooLow
  deriving (Eq, Show)
  
mkProgrammer :: Name -> Age -> Either ProgrammerInvalid Programmer
mkProgrammer name age
  | name /= "" && age >= 0 =
    Right $ Programmer name age
  | name == "" = Left NameEmpty
  | otherwise = Left AgeTooLow

Left is used as the invalid or error constructor for a couple of reasons.

For one, it is conventional to do so in Haskell.

Since you normally want to apply functions and map over the case that doesn't stop your program, it has become convention that the Left of Either is used for whatever case is going to cause the work to stop.

A Thousand Stars in Your Types

Haskell has higher-kinded types.

Type constructors (higher-kinded types) are types that take more types as arguments.

data Example a = Blah | Roof | Woot a

Example is a type construct because it takes a type argument a which is used with the Woot data constructor.

Querying kinds:

Prelude> :k Example
Example :: * -> *

Example has one parameter, so it must be applied to one type in order to become a concrete type represented by a single *.

The two-tuple takes two arguments, so it must be applied to two types to become a concrete type:

Prelude> :k (,)
(,) :: * -> * -> *

Data Constructors are Functions

Data constructors are curried just like functions are.

Nullary data constructors, which are values taking no arguments, are not like functions.

However, data constructors that do take arguments behave like functions.

Like functions, their arguments are type-checked against the specification in the type.

Sources

What's the benefit of the Nothing constructor in Haskell? - https://softwareengineering.stackexchange.com/questions/176576/whats-the-benefit-of-the-nothing-constructor-in-haskell

Data.Maybe - https://hackage.haskell.org/package/base-4.15.0.0/docs/Data-Maybe.html

Haskell Book - https://haskellbook.com/