Explanation of <$> and <*> when used with functions

Viewed 143

I am learning haskell currently and I am having a really hard time wrapping my head around how to explain <$> and <*>'s behavior.

For some context this all came from searching how to use an or operation when using takeWhile and the answer I found was this

takeWhile ((||) <$> isDigit <*> (=='.'))

In most of the documentation I have seen, <*> is used with a container type.

show <*> Maybe 10

By looking at

(<$>) :: Functor f => (a -> b) -> f a -> f b

It tells me that <*> keeps the outer container if its contents and applies the right to the inside, then wraps it back into the container

   a       b          f       a        f       b
([Int] -> String) -> [Just]([Int]) -> [Just]([String])

This makes sense to me, in my mind the f a is essentially happening inside the container, but when I try the same logic, I can make sense to me but I cant correlate the logic

f = (+) <$> (read)

so for f it becomes

   a           b              f           a          f            b
([Int] -> [Int -> Int]) -> ([String] -> [Int]) -> ([String] -> [Int -> Int])

So f being the container really confuses me when I try and work out what this code is going to do. I understand when I write it out like this, I can work it out and see its basically equivalent to the .

(.) :: (b -> c) -> (a -> b) -> a -> c

  b           c               a           b          a            c
([Int] -> [Int -> Int]) -> ([String] -> [Int]) -> ([String] -> [Int -> Int])

so it can be written as

f = (+) . read

Why not just write it as just that? Why wasn't the original snippet just written as

takeWhile ((||) . isDigit <*> (=='.'))

or does <$> imply something in this context that . des not?


Now looking at <*>, it seems like it is basicly exactly the same as the <$> except it takes two containers, uses the inner of both, then puts it pack in the container

(<*>) :: Applicative f => f (a -> b) -> f a -> f b

so

Just show <*> Just 10

   f      a         b           f       a             f     b
[Just]([Int->Int]->[Int]) -> [Just]([Int->Int]) -> [Just]([Int])

However with functions, it becomes murky how things are being passed around to each other. Looking at the original snippit and breaking it up

f1 :: Char -> Bool -> Bool
f1 = (||) . isDigit 

f2 :: Char -> Bool
f2 = f1 <*> (== '.')

<*> behavior in f2 is

   f        a          b          f          a           f       b
([Char] -> [Bool] -> [Bool]) -> ([Char] -> [Bool]) -> ([Char] -> [Bool])

So using previous logic, I see it as Char -> is the container, but its not very useful for me when working out what's happening.

It looks to me as if <*> is passing the function parameter into right side, then passing the same function parameter, and the return value into the left? So to me, it looks equivalent to

f2 :: Char -> Bool
f2 x = f1 x (x=='_')

Its a bit of mental gymnastics for me to work out where the data is flowing when I see <*> and <$>. I guess im just looking for how an experienced haskell-er would read these operations in their head.

1 Answers

The applicative instance for functions is quite simple:

f <*> g = \x -> f x (g x)

You can verify for yourself that the types match up. And as you said,

(<$>) = (.)

(Ignoring fixity)

So you can rewrite your function:

(||) <$> isDigit <*> (=='.')
(||) . isDigit   <*> (=='.')
\x -> ((||) . isDigit) x ((=='.') x)

-- Which can simply be rewritten as:
\x -> isDigit x || x == '.'

But it's important to understand why the function instance is as it is and how it works. Let's begin with Maybe:

instance Applicative Maybe where
    pure :: a -> Maybe a
    pure x = Just x

    (<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b    
    Nothing  <*> _        = Nothing
    _        <*> Nothing  = Nothing
    (Just f) <*> (Just x) = Just (f x)

Ignore the implementation here and just look at the types. First, notice that we've made Maybe an instance of Applicative. What exactly is Maybe? You might say that it's a type, but that isn't true - I can't write something like

x :: Maybe

- that doesn't make sense. Instead, I need to write

x :: Maybe Int
-- Or
x :: Maybe Char

or any other type after Maybe. So we give Maybe a type like Int or Char, and it suddenly becomes a type itself! That's why Maybe is what's known as a type constructor.
And that's exactly what the Applicative typeclass expects - a type constructor, which you can put any other type inside. So, using your analogy, we can think of giving Applicative a container type.

Now, what do I mean by

a -> b

?

We can rewrite it using prefix notation (the same way 1 + 2 = (+) 1 2)

(->) a b

And here we see that the arrow (->) itself is also just a type constructor - but unlike Maybe, it takes two types. But Applicative only wants a type constructor which takes one type. So we give it this:

instance Applicative ((->) r)

Which means that for any r, (->) r is an Applicative. Continuing the container analogy, (->) r is now a container for any type b such that the resulting type is r -> b. What that means is that the contained type is actually the future result of the function on giving it an r.

Now for the actual instance:

(<*>) :: Applicative f => f (a -> b) -> f a -> f b

Substituting (->) r as the applicative,

(<*>) :: ((->) r (a -> b)) -> ((->) r a) ((->) r b)
-- Rewriting it in infix notation:
(<*>) :: (r -> (a -> b)) -> (r -> a) -> (r -> b)

How would we go about writing the instance? Well, we need a way to get the contained type out of the container - but we can't use pattern matching like we did with Maybe. So, we use a lambda:

(f :: r -> (a -> b)) <*> (g :: r -> a) = \(x :: r) -> f x (g x)

And the type of f x (g x) is b, so the entire lambda has type r -> b, which is exactly what we were looking for!

EDIT: I noticed that I didn't talk about the implementation of pure for functions - I could update the answer, but try seeing if you can use the type signature to work it out yourself!

Related