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:
- 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.