5

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.

pigeonhands
  • 3,066
  • 15
  • 26
  • Difference between . and <$> https://stackoverflow.com/questions/27883414/difference-between-function-composition-operator-and-fmap/27883814 Also read functor and applicative sections from typeclassopedia https://wiki.haskell.org/Typeclassopedia –  Feb 16 '22 at 03:47
  • These `f`s in the types correspond to instances of the type classes [`Functor`](https://hackage.haskell.org/package/base/docs/Data-Functor.html#t:Functor) (in the case of `<$>`) and [`Applicative`](https://hackage.haskell.org/package/base/docs/Control-Applicative.html#t:Applicative) (in the case of `<*>`). Those links have instance lists. Examples of instances of these would be `Maybe` and `[]` and `Either a`. Now, the instance that is confusing here is `(->) r`. Note that `(->) r b` is the same as `r -> b`, so `(->) r` could be read as `(r ->)` (although this syntax is not allowed in Haskell) – David Young Feb 16 '22 at 03:50
  • 1
    On functions, there _is_ no difference between `.` and `<$>`. As to the gymnastics, after having worked out why it is that way, you just memorize `(f <*> g) x = f x $ g x`. And ``(h <$> f <*> g) x = (liftA2 h f g) x = f x `h` g x = (h . f) x (g x)``. – Will Ness Feb 16 '22 at 07:45
  • It's not exactly a duplicate - the question was about something much more specific - I think [this](https://stackoverflow.com/questions/54961639/applicative-functor-evaluation-is-not-clear-to-me/54962496#54962496) previous answer of mine might be helpful to you. No worries if it isn't! – Robin Zigmond Feb 16 '22 at 08:41
  • 1
    Just a comment. For every Applicative functor the following expresion `f <$> x <*> y` is equivalent to `liftA2 f x y`. Turns out that `liftA2` for function instance is very handy and woth to learn. Essentially `liftA2 c f g ==\x -> c (f x) (g x)`. For example `(||) <$> isDigit <*> (=='.') == liftA2 (||) isDigit (== '.') == \x -> isDigit x || x == '.'` – lsmor Feb 16 '22 at 09:38
  • 1
    A function is a container. It contains values indexed by the parameter. – n. m. could be an AI Feb 16 '22 at 22:06
  • 1
    @Ammar by "differences [in that link]" you mean, "_no_ differences [as shown in that link]". – Will Ness Feb 17 '22 at 13:32
  • As for “how an experienced haskell-er would read these operations”, I see `(||) <$> isDigit <*> (== '.')` as “is digit or equals dot”. This is essentially the same thing as `\ c -> isDigit c || c == '.'`, but without needing to worry about the variable. `f <$> g <*> h` or `liftA2 f g h` is just plumbing; the actual content is `isDigit`, `(== '.')`, and `||`, which tell me that I’m using some implicit `Char ->` context to produce a `Bool`. It could also be spelled `isDigit <||> (== '.')` using `(<||>) = liftA2 (||)` (or `x <||> y = x >>= \ case { True -> pure True; _ -> y }` for `Monad`). – Jon Purdy Feb 20 '22 at 22:50

1 Answers1

4

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!

Vikstapolis
  • 744
  • 3
  • 13