From e3452ebe6f7dea059658a40cd7201d1f9c7f9813 Mon Sep 17 00:00:00 2001 From: Edoardo La Greca Date: Sat, 17 Jan 2026 19:30:35 +0100 Subject: add notes for lecture 10 --- lec10/notes.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 lec10/notes.md diff --git a/lec10/notes.md b/lec10/notes.md new file mode 100644 index 0000000..e65dbe9 --- /dev/null +++ b/lec10/notes.md @@ -0,0 +1,80 @@ +# Lecture 10: Applicative functors (part 1) + +## Motivation + +Let's suppose that we have the following `Employee` type with a single constructor: + + type Name = String + data Employee = Employee { name :: Name, phone :: String } + deriving Show + +The constructor for `Employee` is going to be a function with the following signature: + + Employee :: Name -> String -> Employee + +However, we might want to use this constructor in contexts where we don't know all fields, like in forms with optionally compilable parts. In that case, we can write a new function that turns the `Employee` constructor into something that returns a `Just Employee` only when both its fields have values, or `Nothing` otherwise. + + maybeEmployee :: (Name -> String -> Employee) -> (Maybe Name -> Maybe String -> Maybe Employee) + +What if we need to extract the fields of an `Employee` object from somewhere else, like a very large data structure or a remote database? + + extractedEmployee :: (Name -> String -> Employee) -> ((e -> Name) -> (e -> String) -> (e -> Employee)) + +We can write this function, too! As it turns out, there is only one way to write it. + +## Generalizing + +If we extract the common parts of this pattern and make it generic to any type, we should end up with something like this: + + (a -> b -> c) -> (f a -> f b -> f c) + +This type looks familiar. In fact, it's just the type of `fmap` from the `Functor` class with an extra argument in the two functions! It would be nice if only we could write this new function, which we're going to call `fmap2`, in terms of `fmap`. We might think this is possible, considering that Haskell allows us to partially apply functions. + +However, despite our best effort, we actually cannot write `fmap2` in terms of `fmap`. The reason is that, by applying `fmap` to the arguments below one by one, we end up with the type below, which is far from our desired `fmap2`. + + h :: a -> (b -> c) + fa :: f a + fb :: f b + + fmap h fa :: f (b -> c) + +Our applied version of `fmap` lets us apply functions to values in a Functor context. However, we need our functions to be in the Functor context as well! The kind of Functor we need are called **applicative functors**, defined as follows and available in the standard library as `Control.Applicative`. + + class Functor f => Applicative f where + pure :: a -> f a + (<*>) :: f (a -> b) -> f a -> f b + +`(<*>)` is pronounced "ap" or "apply" and represents our "contextual application". `pure`, on the other hand, "injects" a value of a type into a container. An interesting aspect of `pure` is that, while our `fmap2` is `fmap` with one more argument, `pure` is `fmap` with one *less* argument. Notice that, since Applicative requires its instances to be instances of Functor as well, we can always use `fmap` with instances of Applicative. + +Using `(<*>)` we can now implement `fmap2`, which is also available in the standard library as `liftA2`. The standard library also defines a synonym for `fmap`, `(<$>)`, available in `Control.Applicative`. As a result, `liftA2` is simply written as: + + liftA2 :: Applicative f => (a -> b -> c -> f a -> f b -> f c + liftA2 h fa fb = h <$> fa <*> fb + +We can write `liftA3` similarly, with the same operators: + + liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d + liftA3 h fa fb fc = ((h <$> fa) <*> fb) <*> fc + +Note how going from `fmap` to `liftA2` required to introduce Applicative, while going from `liftA2` to `liftA3` didn't require further generalization. + +On the other hand, `pure` is useful when we need to apply a function to its arguments in the context of some functor `f`, with one or more arguments not being in `f`. Those arguments are often called "pure". By using `pure`, we can lift the arguments not in `f` before applying. + +## Applicative laws + +An interesting law is the following: + + f `fmap` x === pure f <*> x + +In other words, applying a function `f` to a container `x` gives the same result as first injecting the function into the container and then applying it to `x` wih `<*>`. + +## Applicative examples + +An instance of `Applicative Maybe` could be written like this: + + instance Applicative Maybe where + pure = Just + Nothing <*> _ = Nothing + _ <*> Nothing = Nothing + Just f <*> Just x = Just (f x) + -- cgit v1.2.3