Learning Outcomes

  • Understand how the Either type handles values with two possibilities, typically used for error handling and success cases.
  • Apply the Functor, Applicative, and Monad type classesA way in Haskell to associate functions with types, similar to TypeScript interfaces. They define a set of functions that must be available for instances of those type classes. to the Either type, learning how to implement instances for each.
  • Recognize the power of monadic do blocks in simplifying code and handling complex workflows.

Introduction to Eithers

In Haskell, the Either type is used to represent values with two possibilities: a value of type Either a b is either Left a or Right b. By convention, Left is used to hold an error or exceptional value, while Right is used to hold a correct or expected value. This is particularly useful for error handling.

data Either a b = Left a | Right b

In Haskell’s Either type, convention (and the official documentation) says errors go on the Left and successes on the Right. Why? Because if it is not right (correct) it must be left. This can be considered another example of bias against the left-handed people around the world, but alas, it is a cruel world.

The Left/Right convention is also more general then a Success/Error naming, as Left does not always need to be an error, but it is the most common usage.

Usage of Either

We can use Either to help us with error catching, similar to a Maybe type. However, since the error case, has a value, rather than Nothing, allowing to store an error message to give information to the programmer/user.

divide :: Double -> Double -> Either String Double
divide _ 0 = Left "Division by zero error"
divide x y = Right (x / y)

Similar to Maybes, we can also use pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data. against Eithers in a function.

handleResult :: Either String Double -> String
handleResult (Left err) = "Error: " ++ err
handleResult (Right val) = "Success: " ++ show val

The Kind of Either

The Either type constructor has the kind * -> * -> *. This means that Either takes two type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code. and returns a concrete type.

Either String Int is a concrete type. It has the kind * because both type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code. (String and Int) are concrete types.

Recap: Kinds in Haskell

In Haskell, types are classified into different kinds. A kind can be thought of as a type of a type, describing the number of type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code. a type constructor takes and how they are applied. The Either type has an interesting kind, which we’ll explore in detail.

Before diving into the Either typeclass, let’s briefly recap what kinds are:

* (pronounced “star”) represents the kind of all concrete types. For example, Int and Bool have the kind *. * -> * represents the kind of type constructors that take one type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code. and return a concrete type. For example, Maybe and [] (the list type constructor) have the kind * -> *. * -> * -> * represents the kind of type constructors that take two type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code. and return a concrete type. For example, Either and (,) (the tuple type constructor) have the kind * -> * -> *.

Typeclasses: Functor, Applicative, and Monad

Functor

The Functor type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions. expects a type of kind * -> *. For Either, this means partially applying the first type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code. , e.g., instance Functor (Either a), where a will be the type of the Left.

We can then define, fmap over either, considering Left as the error case, and applying the function, when we have a correct (Right) case.

instance Functor (Either a) where
    fmap _ (Left x)  = Left x
    fmap f (Right y) = Right (f y)

An example of using this will be:

fmap (+1) (Right 2) -- Result: Right 3
(+1) <$> (Right 2) -- or using infix <$>
fmap (+1) (Left "Error") -- Result: Left "Error"

Applicative

The ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>). type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions. allows for function application lifted over wrapped values.

In this instance, pure wraps a value in Right, and <*> applies the function inside a Right to another Right, propagating Left values unchanged.

instance Applicative (Either a) where
    pure = Right
    Left x <*> _ = Left x
    Right f <*> r = fmap f r
pure (+1) <*> Right 2 -- Result: Right 3
pure (+1) <*> Left "Error" -- Result: Left "Error"
Right (+1) <*> Right 2 -- Result: Right 3
Right (+1) <*> Left "Error" -- Result: Left "Error"
Left "Error" <*> Right 2 -- Result: Left "Error"

Monad

The MonadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context. type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions. allows for chaining operations that produce wrapped values.

This involves defining the methods return (which should be identical to pure) and >>= (bindthe defining function which all monads must implement. ).

instance Monad (Either a) where
    return = Right
    (>>=) (Left x) _ = Left x
    (>>=) (Right y) f = f y
Right 3 >>= (\x -> Right (x + 1)) -- Result: Right 4
Left "Error" >>= (\x -> Right (x + 1)) -- Result: Left "Error"
Right 3 >>= (\x -> Left "Something went wrong") -- Result: Left "Something went wrong"

Example

First, we’ll define custom error types to represent possible failures at each stage.

data FileError = FileNotFound | FileReadError deriving (Show)
data ReadError = ReadError String deriving (Show)
data TransformError = TransformError String deriving (Show)

Define a function to read data from a file. If reading succeeds, it returns a Right with the file contents, otherwise, it returns a Left with a FileError.

import System.IO (readFile)
import Control.Exception (catch, IOException)

readFileSafe :: FilePath -> IO (Either FileError String)
-- catch any IOException, and use `handleError` on IOException
readFileSafe path = catch (Right <$> (readFile path)) handleError
  where
    handleError :: IOException -> IO (Either FileError String)
    handleError _ = return $ Left FileReadError

Define a function to split the file content in to separate lines, if it exists. It returns a Right with the read data or a Left with a ReadError.

readData :: String -> Either ReadError [String]
readData content
    | null content = Left $ ReadError "Empty file content"
    | otherwise = Right $ lines content

Define a function to transform the read data. It returns a Right with transformed data or a Left with a TransformError.

transformData :: [String] -> Either TransformError [String]
transformData lines
    | null lines = Left $ TransformError "No lines to transform"
    -- Simple transformation, where, we reverse each line.
    | otherwise = Right $ map reverse lines

The outer do block, is using the IO monadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context. , while the inner do block is using the Either monadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context. . This code looks very much like imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style. code, using the power of monadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context. to allow for sequencing of operations. However, this is powerful, as it will allow the Left error to be threaded through the monadic do block, with the user not needing to handle the threading of the error state.

main :: IO ()
main = do
    -- Attempt to read the file
    fileResult <- readFileSafe "example.txt"
    
    let result = do
            -- Use monad instance to compute sequential operations
            content <- fileResult
            readData <- readData content
            transformData readdData
    print result

Glossary

Functor: A type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure.

Applicative: A type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).

Monad: A type class in Haskell that represents computations as a series of steps. It provides the bind operation (>>=) to chain operations and the return (or pure) function to inject values into the monadic context.