Beginner Notes on PureScript Functors

Conceptually, a Functor is a container data type that you can map over.

Examples of Functors

Data.Array, Data.Maybe, Data.Either

Map Function Signature

-- The infix alias for map is <$>
forall a b f. (Functor f) => (a -> b) -> f a -> f b

PureScript Example:

import Prelude
import Data.Maybe
import Data.Either

arrayPeopleContainer = ["Obama", "Gordon", "Jill"]
maybeContainer = Just "Garry"

presidentStartDate :: String -> Either String String
presidentStartDate name = if (name == "Obama")
                          then (Right "Nov 4, 2008")
                          else (Left (name ++ " is not a president."))

showPerson name = "Only " ++ name

-- > map showPerson arrayPeopleContainer
-- ["Only Bill", "Only Fred", "Only Ted"]

-- > map showPerson maybeContainer
-- Just ("Only Garry")

-- > map showPerson Nothing
-- Nothing

-- > map (\date -> "Started on " ++ date)  (presidentStartDate "Obama")
-- Right ("Started on Nov 4, 2008")

-- > map (\date -> "Started on " ++ date)  (presidentStartDate "Garrison")
-- Left ("Garrison is not a President")

Key Points:

  1. The map function returns a Functor the same shape as the one passed in.

Lifting...

map is just one of the functions that can be run on Functors. There is also lift2, lift3, lift4, in the Control.Apply module. These functions are like map, but map functors to functions of multiple arguments, e.g.

actors = ["Tom Hanks", "Noomi Rapace"]
showTwoPeople name1 name2 = name1 ++ " and " ++ name2

-- Control.Apply.lift2 showTwoPeople actors arrayPeopleContainer
-- ["Tom Hanks and Obama","Tom Hanks and Gordon","Tom Hanks and Jill","Noomi Rapace and Obama","Noomi Rapace and Gordon","Noomi Rapace and Jill"]

The "Apply" Type Class

Related to Functor is the "Apply" type class. Types in Apply, can contain functions that map one type to another.

-- The apply function signature
-- The infix alias for apply is <*>
class (Functor f) <= Apply f where
  apply :: forall a b. f (a -> b) -> f a -> f b

An example of types in Apply would be Data.Array.

If we contrast map to apply, we see that map was able to run a single function on each item in the container. apply on the other hand is able to run multiple functions on the items in a container, and accumulate the new values. We can see that in this example...

enjoysRunning subject = subject ++ " enjoys running."
likesCats subject = subject ++ " likes cats."
eatsWithVigor subject = subject ++ " eats vigorously."

-- > apply [enjoysRunning, likesCats] actors
-- ["Tom Hanks enjoys running.","Noomi Rapace enjoys running.","Tom Hanks likes cats.","Noomi Rapace likes cats."]

-- > apply [enjoysRunning, likesCats, eatsWithVigor] arrayPeopleContainer
-- ["Obama enjoys running.","Gordon enjoys running.","Jill enjoys running.","Obama likes cats.","Gordon likes cats.","Jill likes cats.","Obama eats vigorously.","Gordon eats vigorously.","Jill eats vigorously."]

When used together map and apply form the basis of the lift functionality.

When we "lift" a function over a data structure, we're running that function on ever elements in the data structure, and returning a new data structure. This is what map does.

Similarly, we can "lift" (map) a function over the arguments of another function. In order to do that, we first map the function arguments of f, resulting in a new function, e.g. a -> (b -> c) becomes f a -> f (b -> c). If we partially apply the mapped function (that is, we bind an argument to the first parameter of the function), then we get a new function f (b -> c). Notice how f (b -> c) is structured the same as the first argument of apply. This is essentially a container of functions that goes from b to c. So we can call apply with the argument f (b -> c) and a value f b, and we'll get back an f c.

Basically, we map over the function, and then call apply a certain number of times depending on how many arguments the function has.

lift2 f a b = f <$> a <*> b
lift3 f a b c = f <$> a <*> b <*> c
lift4 f a b c d = f <$> a <*> b <*> c <*> d

The 'Applicative' Type Class

class (Apply f) <= Applicative f where
  pure :: forall a. a -> f a

Example:

import Prelude
import Data.Maybe

-- calculate the addition of 1 and 2 inside some unknown container
-- At this point the compiler doesn't know what the container is.
let myCalculation = add <$> pure 1 <*> pure 2

-- If we want to see the result of adding, we need to tell the compiler
-- what the container is.
> myCalculation :: Maybe Int
Just (3)

-- We can just as well make it an Array...
> myCalculation :: Array Int
[3]

-- Or an Either...
> import Data.Either
> myCalculation :: Either Unit Int
Right (3)

-- Note: The "Left" in Either is the failure case. "Right" is the success case.