Why is liftA2 added to Applicative as a method? - haskell

I came across this discussion on the Haskell mailing list. From the discussion, there seems to be performance implications with adding liftA2 as a method of Applicative. Can you provide concrete examples why it is necessary to add liftA2 to Applicative methods?

The email is written in 2017. At that time the Applicative typeclass looked like:
class Functor f => Applicative f where
-- | Lift a value.
pure :: a -> f a
-- | Sequential application.
(<*>) :: f (a -> b) -> f a -> f b
-- | Sequence actions, discarding the value of the first argument.
(*>) :: f a -> f b -> f b
a1 *> a2 = (id <$ a1) <*> a2
-- This is essentially the same as liftA2 (const id), but if the
-- Functor instance has an optimized (<$), we want to use that instead.
-- | Sequence actions, discarding the value of the second argument.
(<*) :: f a -> f b -> f a
(<*) = liftA2 const
So without the liftA2 as part of the Applicative typeclass. It was defined as [src]:
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 f a b = fmap f a <*> b
so one could not make a special implementation in the typeclass. This means that sometimes liftA2 can be implemented more efficiently, but one can not define that.
For example the Maybe functor and Applicative are implemented as:
instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap _ Nothing = Nothing
instance Applicative Maybe where
pure = Just
Just f <*> Just x = Just (f x)
_ <*> _ = Nothing
This thus means that the liftA2 for a Maybe is implemented similar to:
liftA2Maybe :: (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c
liftA2Maybe f x y = apMaybe (fmapMaybe f x) y
where fmapMaybe f (Just x) = Just (f x)
fmapMaybe _ Nothing = Nothing
apMaybe (Just f) (Just x) = Just (f x)
apMaybe _ _ = Nothing
But this is not optimal. It means that fmapMaybe will inspect if the parameter is a Just x, or Nothing, and then return a Just (f x) or a Nothing. But regardless, apMaybe will again inspect that, whereas we can already know that in advance. We can make a more efficient implementation with:
liftA2Maybe :: (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c
liftA2Maybe f (Just x) (Just y) = Just (f x y)
liftA2Maybe _ _ _ = Nothing
here we avoid extra unpacking of data constructors. This is not that problematic however. For certain data structures like a ZipList the overhead will be more severe because the number of objects is larger.
On June 23, 2017, a new base library was published where the liftA2 function was added as a method to the Applicative type class.

Related

Does the Applicative interface provide power beyond the ability to lift multi-argument functions (in curried form) into a Functor?

Applicatives are often presented as a way to lift multi-argument functions
into a functor and apply functor values to it. But I wonder if there is some
subtle additional power stemming from the fact that it can do so by lifting
functions that return a function and applying the function arguments one at
a time.
Imagine instead we define an interface based on lifting functions whose argument is a tuple of arguments:
# from Functor
fmap :: (a -> b) -> Fa -> Fb
# from Applicative
pure :: a -> Fa
# combine multiple functor values into a functor of a tuple
tuple1 :: Fa -> F(a)
tuple2 :: Fa -> Fb -> F(a,b)
tuple3 :: Fa -> Fb -> Fc -> F(a,b,c)
(etc ...)
# lift multi-argument functions (that take a tuple as input)
ap_tuple1 :: ((a) -> b) -> F(a) -> Fb
ap_tuple2 :: ((a,b) -> c) -> F(a,b) -> Fc
ap_tuple3 :: ((a,b,c) -> d) -> F(a,b,c) -> Fd
(etc ..)
Assume we had the corresponding tuple function defined for every sized tuple we may encounter.
Would this interface be equally as powerful as the Applicative interface, given it allows for
lifting/applying-to multi-argument functions BUT doesn't allow for lifting/applying-to functions
that return a function? Obviously one can curry functions that take a tuple as an argument
so they can be lifted in an applicative and one can uncurry functions that return a function
in order to lift them into hypothetical implementation above. But to my mind there is a subtle
difference in power. Is there any difference? (Assuming the question even makes sense)
You've rediscovered the monoidal presentation of Applicative. It looks like this:
class Functor f => Monoidal f where
(>*<) :: f a -> f b -> f (a, b)
unit :: f ()
It's isomorphic to Applicative via:
(>*<) = liftA2 (,)
unit = pure ()
pure x = x <$ unit
f <*> x = fmap (uncurry ($)) (f >*< x)
By the way, your ap_tuple functions are all just fmap. The "hard" part with multiple values is combining them together. Splitting them back into pieces is "easy".
Yes, this is equally as powerful. Notice that pure and tuple1 are the same. Further, everything higher than tuple2 is recovered from tuple2 and fmap:
tuple3 x y z = repair <$> tuple2 (tuple2 x y) z
where repair ((a, b), c) = (a, b, c)
tuple4 w x y z = repair <$> tuple2 (tuple2 x y) (tuple2 x y)
where repair ((a, b), (c, d)) = (a, b, c, d)
-- etc.
Also, all of the ap_tuples are just fmap:
ap_tuple1 = fmap
ap_tuple2 = fmap
ap_tuple3 = fmap
-- ...
Renaming prod = tuple2, your question boils down to
Is
class Functor f => Applicative f where
pure :: a -> f a
prod :: f a -> f b -> f (a, b)
equivalent to
class Functor f => Applicative f where
pure :: a -> f a
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
?
And you might already see that the answer is yes. prod is just a specialization of liftA2
prod = liftA2 (,)
But (,) is "natural" in the sense that it doesn't "delete" anything, so you can recover liftA2 just by destructuring the data back out:
liftA2 f x y = f' <$> prod x y
where f' (a, b) = f a b

Looking for a Haskell function related to liftA2, but works like <|> from Alternative

Consider this liftA2 function:
liftA2 :: (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c
liftA2 f Nothing Nothing = Nothing
liftA2 f (Just x) Nothing = Nothing
liftA2 f Nothing (Just y) = Nothing
liftA2 f (Just x) (Just y) = f x y
This is equivalent to the real liftA2 function from Control.Applicative, but specialized for Maybe. (and also liftM2 from Control.Monad)
I'm looking for a cousin of that function, that works like this:
mystery :: (a -> a -> a) -> Maybe a -> Maybe b -> Maybe c
mystery f Nothing Nothing = Nothing
mystery f (Just x) Nothing = Just x
mystery f Nothing (Just y) = Just y
mystery f (Just x) (Just y) = Just (f x y)
The closest concept I'm aware of is <|>, but that discards the second value if there are two, whereas I would rather pass a function to combine them.
What is this mystery function called? What type class does it operate on? What terms can I google to learn more? Thank you!
If you are willing to accept a different type signature, working with a Semigroup instance instead of with an arbitrary function f, then what you are looking for is the Option newtype from Data.Semigroup:
Prelude Data.Semigroup> Option Nothing <> Option Nothing
Option {getOption = Nothing}
Prelude Data.Semigroup> Option (Just [1]) <> Option Nothing
Option {getOption = Just [1]}
Prelude Data.Semigroup> Option Nothing <> Option (Just [2])
Option {getOption = Just [2]}
Prelude Data.Semigroup> Option (Just [1]) <> Option (Just [2])
Option {getOption = Just [1,2]}
For an arbitrary function you need something that is pretty specialized to Maybe - I don't really see how it could work for an arbitrary Applicative, or Alternative.
If I understand you correctly, you may be interested in the Semialign class from Data.Align, which offers zip-like operations that don't drop missing elements:
class Functor f => Semialign f where
align :: f a -> f b -> f (These a b)
align = alignWith id
alignWith :: (These a b -> c) -> f a -> f b -> f c
alignWith f as bs = f <$> align as bs
You can write
alignBasic :: Semialign f => (a -> a -> a) -> f a -> f a -> f a
alignBasic f = alignWith $ \case
This a -> a
That a -> a
These a1 a2 -> f a1 a2
-- or just
alignBasic f = alignWith (mergeThese f)
Since Maybe is an instance of Semialign, alignBasic can be used at type
alignBasic :: (a -> a -> a) -> Maybe a -> Maybe a -> Maybe a
Here goes an extra argument in support of alignWith being the liftA2 analogue you are looking for, as dfeuer suggests.
Using the monoidal presentation of Applicative...
fzip :: Applicative f => f a -> f b -> f (a, b)
fzip u v = (,) <$> u <*> v -- Think of this as a default implementation.
... we can do a subtle tweak on liftA2:
liftA2' :: Applicative f => ((a, b) -> c) -> f a -> f b -> f c
liftA2' f u v = f <$> fzip u v
This variation is relevant here because it translates straightforwardly to Alternative, under the products-to-sums monoidal functor interpretation:
-- fzip analogue. Name borrowed from protolude.
eitherA :: Alternative f => f a -> f b -> f (Either a b)
eitherA u v = (Left <$> u) <|> (Right <$> v)
-- Made-up name.
plusWith :: Alternative f => (Either a b -> c) -> f a -> f b -> f c
plusWith f u v = f <$> eitherA u v
plusWith, however, isn't helpful in your case. An Either a b -> c can't produce a c by combining an a with a b, except by discarding one of them. You'd rather have something that takes a These a b -> c argument, as using These a b can express the both-a-and-b case. It happens that a plusWith that uses These instead of Either is very much like alignWith:
-- Taken from the class definitions:
class Functor f => Semialign f where
align :: f a -> f b -> f (These a b)
align = alignWith id
alignWith :: (These a b -> c) -> f a -> f b -> f c
alignWith f a b = f <$> align a b
class Semialign f => Align f where
nil :: f a
Alternative is a class for monoidal functors from Hask-with-(,) to Hask-with-Either. Similarly, Align is a class for monoidal functors from Hask-with-(,) to Hask-with-These, only it also has extra idempotency and commutativity laws that ensure the unbiased behaviour you are looking for.
One aspect of Align worth noting is that it is closer to Alternative than to Applicative. In particular, the identity of the These tensor product is Void rather than (), and accordingly nil, the identity of align, is an empty structure rather than a singleton. That might come as a surprise, given that align and friends offer us greedy zips and zips with padding, and that zip is often thought of as an applicative operation. In this context, I feel it might help to mention a fact about ZipList. ZipList offers a zippy Applicative instance for lists in lieu of the default, cartesian product one:
GHCi> fzip [1,2] [3,9,27]
[(1,3),(1,9),(1,27),(2,3),(2,9),(2,27)]
GHCi> getZipList $ fzip (ZipList [1,2]) (ZipList [3,9,27])
[(1,3),(2,9)]
What is less widely known is that ZipList also has a different Alternative instance:
GHCi> [1,2] <|> [3,9,27]
[1,2,3,9,27]
GHCi> [3,9,27] <|> [1,2]
[3,9,27,1,2]
GHCi> getZipList $ ZipList [1,2] <|> ZipList [3,9,27]
[1,2,27]
GHCi> getZipList $ ZipList [3,9,27] <|> ZipList [1,2]
[3,9,27]
Instead of concatenating the lists, it pads the first list with a suffix of the second one to the larger of the two lengths. (It is a fitting instance for the type because it follows the left distribution law .) align for lists is somewhat similar to (<|>) #ZipList, except that it isn't biased towards either list:
GHCi> align [1,2] [3,9,27]
[These 1 3,These 2 9,That 27]
GHCi> align [3,9,27] [1,2]
[These 3 1,These 9 2,This 27]

`(<*>)` definition for the Applicative functor?

Some Haskell source code (see ref):
-- | Sequential application.
--
-- A few functors support an implementation of '<*>' that is more
-- efficient than the default one.
(<*>) :: f (a -> b) -> f a -> f b
(<*>) = liftA2 id
-- | Lift a binary function to actions.
--
-- Some functors support an implementation of 'liftA2' that is more
-- efficient than the default one. In particular, if 'fmap' is an
-- expensive operation, it is likely better to use 'liftA2' than to
-- 'fmap' over the structure and then use '<*>'.
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
liftA2 f x = (<*>) (fmap f x)
Three things seem to be quite confusing to me:
1) (<*>) is defined in terms of liftA2, where liftA2 is defined in terms of (<*>). How does it work? I see no obvious "recursion-break" case...
2) id is an a -> a function. Why is it passed into liftA2 as an (a -> b -> c) function?
3) fmap id x always equals x, since functors must preserve appropriate identities. Thus (<*>) (fmap id x) = (<*>) (x) where x = f a - an a-typed functor itself (by the way, how can a-typifying of the functor can be explained from the pure category theory's point of view? functor is just a mapping between categories, it has no further "typification"... seems like it is better to say - "a container of type a with an (endo)functor defined for each instance of asummed category Hask of well-defined Haskell types). So (<*>) (f a) while by definition (<*>) expects f(a' -> b'): thus, the only way to make it work is to deliberately bound a to be an (a' -> b'). However when I run :t \x -> (<*>) (fmap id x) in the gchi, it spits out something mind-blowing: f (a -> b) -> f a -> f b - which I fail to explain.
Can someone step by step explain how does that work and why it even compiles?
P.S. Category theory terms, if needed, are welcome.
For question 1, you left out a very important piece of context.
class Functor f => Applicative f where
{-# MINIMAL pure, ((<*>) | liftA2) #-}
Those definitions you quoted belong to a class. That means instances can override them. Furthermore, the MINIMAL pragma says that in order to work, at least one of them must be overridden in the instance. So the breaking of the recursion happens whenever one is overridden in a particular instance. This is just like how the Eq class defines (==) and (/=) in terms of each other so that you only need to provide a definition for one in a hand-written instance.
For question two, a -> b -> c is shorthand for a -> (b -> c). So it unifies with (let's rename variables to avoid collision) d -> d as (b -> c) -> (b ->c). (tangentially, that's also the type of ($).)
For three - you're absolutely right. Keep simplifying!
\x -> (<*>) (fmap id x)
\x -> (<*>) x
(<*>)
So it shouldn't really be a surprise ghci gave you the type of (<*>) back, should it?
1) (<*>) is defined in terms of liftA2, where liftA2 is defined in terms of (<*>). How does it work? I see no obvious "recursion-break" case...
It's not recursion. In your instance of Applicative you can either define both of them or just one. If you define just (<*>) then liftA2 is defined from (<*>), and vice versa.
2) id is an a -> a function. Why is it passed into liftA2 as an (a -> b -> c) function?
Unification works as follows,
(<*>) :: f (a -> b) -> f a -> f b
(<*>) = liftA2 id
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
id : u -> u
liftA2 : (a -> (b -> c) -> f a -> f b -> f c
------------------------------------------------------
u = a
u = b->c
id : (b->c) -> (b->c)
liftA2 : ((b->c) -> (b->c)) -> f (b->c) -> f b -> f c
------------------------------------------------------
liftA2 id : f (b->c) -> f b -> f c
3.
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
liftA2 h x = (<*>) (fmap h x)
Renamed the first argument from f to h, to prevent confusion since f also shows in the type
h :: a -> (b -> c)
x :: f a
fmap :: (a -> d) -> f a -> f d
------------------------------
d = b -> c
h :: a -> (b->c)
x :: f a
fmap :: (a -> (b->c)) -> f a -> f (b->c)
----------------------------------------
fmap h x :: f (b -> c)
fmap h x :: f (b -> c)
(<*>) :: f (b -> c) -> f b -> f c
-------------------------------------
(<*>) fmap h x :: f b -> f c
Edit:
Consistency
To show the consistency of both formulas, first lets first rewrite liftA2 into something simpler. We can use the formula below to get rid of fmap and use only pure and <*>
fmap h x = pure h <*> x
and it's best to put all points in the definition. So we get,
liftA2 h u v
= (<*>) (fmap h u) v
= fmap h u <*> v
= pure h <*> u <*> v
So we want to check the consistency of,
u <*> v = liftA2 id u v
liftA2 h u v = pure h <*> u <*> v
For the first we need the property that pure id <*> u = u
u <*> v
= liftA2 id u v
= pure id <*> u <*> v
= u <*> v
For the second we need a property of liftA2. Properties of applicative are usually given in terms of pure and <*> so we need to derive it first. The required formula is derived from pure h <*> pure x = pure (h x).
liftA2 h (pure x) v
= pure h <*> pure x <*> v
= pure (h x) <*> v
= liftA2 (h x) v
This requires h : t -> a -> b -> c. The proof of consistency becomes,
liftA2 h u v
= pure h <*> u <*> v
= pure h `liftA2 id` u `liftA2 id` v
= liftA2 id (liftA2 id (pure h) u) v
= liftA2 id (liftA2 h u) v
= liftA2 h u v
1) (<*>) is defined in terms of liftA2, where liftA2 is defined in terms of (<*>). How does it work? I see no obvious "recursion-break" case...
Each instance is responsible for overriding at least one of the two. This is documented in a machine-readable way in the pragma at the top of the class:
{-# MINIMAL pure, ((<*>) | liftA2) #-}
This pragma announces that instance writers must define at least the pure function and at least one of the other two.
id is an a -> a function. Why is it passed into liftA2 as an (a -> b -> c) function?
If id :: a -> a, we can choose a ~ d -> e to get id :: (d -> e) -> d -> e. Traditionally, this particular specialization of id is spelled ($) -- maybe you've seen that one before!
3) ...
I don't... actually see any contradiction set up in the facts you state. So I'm not sure how to explain away the contradiction for you. However, you have a few infelicities in your notation that might be related to mistakes in your thinking, so let's talk about them briefly.
You write
Thus (<*>) (fmap id x) = (<*>) (x) where x = f a.
This is not quite right; the type of x is f a for some Functor f, but it is not necessarily equal to f a.
by the way, how can a-typifying of the functor can be explained from the pure category theory's point of view? functor is just a mapping between categories, it has no further "typification"... seems like it is better to say - "a container of type a with an (endo)functor defined for each instance of assumed category Hask of well-defined Haskell types
A functor constitutes two things: a mapping from objects to objects, and a mapping from arrows to arrows that is compatible with the object mapping. In a Haskell Functor instance declaration like
instance Functor F where fmap = fmapForF
the F is the mapping from objects to objects (objects in both the source and target categories are types, and F is a thing which takes a type and produces a type) and the fmapForF is the mapping from arrows to arrows.
I run :t \x -> (<*>) (fmap id x) in the gchi, it spits out something mind-blowing: f (a -> b) -> f a -> f b - which I fail to explain.
Well, you already observed that fmap id x = x, which means \x -> (<*>) (fmap id x) = \x -> (<*>) x. And for any function f, f = \x -> f x (up to some niggles that aren't important right now), so in particular \x -> (<*>) (fmap id x) = (<*>). So ghci gives you the type of (<*>), as it should.
Here I have to disagree with the GHC devs on their coding style :)
I would like to argue that one should never write
ap = liftA2 id
but, instead, use the equivalent
ap = liftA2 ($)
since the latter makes it clear that we are lifting the application operation.
(Actually, for very technical reasons GHC devs can not use $ here in this internal module, as pointed out below in the comments. So, at least they have a very good reason for their choice.)
Now, you might wonder why id can be used instead of $. Formally, we have
($) f x
= f x
= (id f) x
= id f x
hence, eta-contracting x then f, we get ($) = id.
Indeed, ($) is a "special case" of id.
id :: a -> a
-- choose a = (b -> c) as a special case
id :: (b -> c) -> (b -> c)
id :: (b -> c) -> b -> c
($):: (b -> c) -> b -> c
Hence, the main difference is: id is the identity on any type a, while ($) is the "identity" on any functional type b -> c. The latter is best visualized as a binary function (application), but it can equivalently be considered a unary function (identity) on a function type.

How are monoid and applicative connected?

I am reading in the haskellbook about applicative and trying to understand it.
In the book, the author mentioned:
So, with Applicative, we have a Monoid for our structure and function
application for our values!
How is monoid connected to applicative?
Remark: I don't own the book (yet), and IIRC, at least one of the authors is active on SO and should be able to answer this question. That being said, the idea behind a monoid (or rather a semigroup) is that you have a way to create another object from two objects in that monoid1:
mappend :: Monoid m => m -> m -> m
So how is Applicative a monoid? Well, it's a monoid in terms of its structure, as your quote says. That is, we start with an f something, continue with f anotherthing, and we get, you've guessed it a f resulthing:
amappend :: f (a -> b) -> f a -> f b
Before we continue, for a short, a very short time, let's forget that f has kind * -> *. What do we end up with?
amappend :: f -> f -> f
That's the "monodial structure" part. And that's the difference between Applicative and Functor in Haskell, since with Functor we don't have that property:
fmap :: (a -> b) -> f a -> f b
-- ^
-- no f here
That's also the reason we get into trouble if we try to use (+) or other functions with fmap only: after a single fmap we're stuck, unless we can somehow apply our new function in that new structure. Which brings us to the second part of your question:
So, with Applicative, we have [...] function application for our values!
Function application is ($). And if we have a look at <*>, we can immediately see that they are similar:
($) :: (a -> b) -> a -> b
(<*>) :: f (a -> b) -> f a -> f b
If we forget the f in (<*>), we just end up with ($). So (<*>) is just function application in the context of our structure:
increase :: Int -> Int
increase x = x + 1
five :: Int
five = 5
increaseA :: Applicative f => f (Int -> Int)
increaseA = pure increase
fiveA :: Applicative f => f Int
fiveA = pure 5
normalIncrease = increase $ five
applicativeIncrease = increaseA <*> fiveA
And that's, I guessed, what the author meant with "function application". We suddenly can take those functions that are hidden away in our structure and apply them on other values in our structure. And due to the monodial nature, we stay in that structure.
That being said, I personally would never call that monodial, since <*> does not operate on two arguments of the same type, and an applicative is missing the empty element.
1 For a real semigroup/monoid that operation should be associative, but that's not important here
Although this question got a great answer long ago, I would like to add a bit.
Take a look at the following class:
class Functor f => Monoidal f where
unit :: f ()
(**) :: f a -> f b -> f (a, b)
Before explaining why we need some Monoidal class for a question about Applicatives, let us first take a look at its laws, abiding by which gives us a monoid:
f a (x) is isomorphic to f ((), a) (unit ** x), which gives us the left identity. (** unit) :: f a -> f ((), a), fmap snd :: f ((), a) -> f a.
f a (x) is also isomorphic f (a, ()) (x ** unit), which gives us the right identity. (unit **) :: f a -> f (a, ()), fmap fst :: f (a, ()) -> f a.
f ((a, b), c) ((x ** y) ** z) is isomorphic to f (a, (b, c)) (x ** (y ** z)), which gives us the associativity. fmap assoc :: f ((a, b), c) -> f (a, (b, c)), fmap assoc' :: f (a, (b, c)) -> f ((a, b), c).
As you might have guessed, one can write down Applicative's methods with Monoidal's and the other way around:
unit = pure ()
f ** g = (,) <$> f <*> g = liftA2 (,) f g
pure x = const x <$> unit
f <*> g = uncurry id <$> (f ** g)
liftA2 f x y = uncurry f <$> (x ** y)
Moreover, one can prove that Monoidal and Applicative laws are telling us the same thing. I asked a question about this a while ago.

Implementing Monoidal in terms of Applicative

Typeclassopedia presents the following exercise:
Implement pure and (<*>) in terms of unit and (**), and vice versa.
Here's Monoidal and MyApplicative:
class Functor f => Monoidal f where
u :: f () -- using `u` rather than `unit`
dotdot :: f a -> f b -> f (a,b) -- using instead of `(**)`
class Functor f => MyApplicative f where
p :: a -> f a -- using instead of `pure`
apply :: f (a -> b) -> f a -> f b -- using instead of `(<**>)`
First, let me show the Maybe-like data type:
data Option a = Some a
| None deriving Show
Then, I defined instance MyApplicative Option:
instance MyApplicative Option where
p = Some
apply None _ = None
apply _ None = None
apply (Some g) f = fmap g f
Finally, my attempt at implementing Monoidal Option in terms of p and apply of MyApplicative:
instance Monoidal Option where
u = p ()
dotdot None _ = None
dotdot _ None = None
dotdot (Some x) (Some y) = Some id <*> Some (x, y)
Is this right? My implementation of dotdot with apply doesn't seem
instance Monoidal Option where
u = p ()
dotdot None _ = None
dotdot _ None = None
dotdot (Some x) (Some y) = apply (Some id) (Some (x, y))
In particular, I'm curious about how to properly implement dotdot :: f a -> f b -> f (a, b) with Applicative's (<*>) - in my case it's apply.
Applicative is a neat alternative presentation of Monoidal. Both typeclasses are equivalent, and you can convert between the two without considering a specific data type like Option. The "neat alternative presentation" for Applicative is based on the following two equivalencies
pure a = fmap (const a) unit
unit = pure ()
ff <*> fa = fmap (\(f,a) -> f a) $ ff ** fa
fa ** fb = pure (,) <*> fa <*> fb
The trick to get this "neat alternative presentation" for Applicative is the same as the trick for zipWith - replace explicit types and constructors in the interface with things that the type or constructor can be passed into to recover what the original interface was.
unit :: f ()
is replaced with pure which we can substitute the type () and the constructor () :: () into to recover unit.
pure :: a -> f a
pure () :: f ()
And similarly (though not as straightforward) for substituting the type (a,b) and the constructor (,) :: a -> b -> (a,b) into liftA2 to recover **.
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
liftA2 (,) :: f a -> f b -> f (a,b)
Applicative then gets the nice <*> operator by lifting function application ($) :: (a -> b) -> a -> b into the functor.
(<*>) :: f (a -> b) -> f a -> f b
(<*>) = liftA2 ($)
Getting from <*> back to liftA2 is common enough that liftA2 is included in Control.Applicative. The <$> is infix fmap.
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 f a b = f <$> a <*> b

Resources