2

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)

Dr DR
  • 667
  • 1
  • 5
  • 8
  • 1
    cf. [four types of generalized function application](https://stackoverflow.com/a/44118041/849891) (at the bottom of the post) – Will Ness Apr 28 '20 at 05:10
  • 1
    "application" is a distraction. we already can apply functions "inside" a mere *Functor*. so `mult :: Monoidal f => (f a, f b) -> f (a,b)` smashes two functorial values into one, first, and then we just apply the uncurried version of our function (`uncurry g (x,y) = g x y`) "inside" that one functorial value, by the power of Functor alone. some laws will ensure that nothing is lost by the smashing. it's the same as `x $ y == uncurry ($) (x,y)`, only "on the inside". – Will Ness Apr 28 '20 at 05:24

2 Answers2

6

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

  • or, perhaps prettier, `(>*<) :: (f a, f b) -> f (a, b)`. – Will Ness Apr 28 '20 at 04:57
  • @WillNess But then you have an operator-style function that can't be called in infix notation. – Joseph Sible-Reinstate Monica Apr 28 '20 at 04:58
  • 1
    ah, so, must give it some other name then. was thinking more about the type. so `>*<` is already some kind of a comma, alright. – Will Ness Apr 28 '20 at 04:59
  • Interesting! I think I prefer this view of Applicative over the one normally mentioned in articles as it focuses on the monoidal quality rather than the ability to "apply" a value to the embedded function value (which seemed obtuse when I first encountered applicatves). Similar to how the presentation of a Monad using Join instead of Bind better shows the essence of the power of a Monad above that of a Functor (i.e. the flattening of two Monad values). – Dr DR Apr 28 '20 at 07:27
  • *meant "seemed obscure" rather than "seemed obtuse" – Dr DR Apr 28 '20 at 07:39
  • @DrDR one could reasonably claim actually that Bind more immediately than Join represents the layering of the (potentially) impure and the (dependent on the previously "computed" value) pure, that is the essence of Monad. – Will Ness Apr 28 '20 at 08:56
  • @WillNess Good point. But I see the presence of Join as being the distinguishing feature of Monads (whereas Bind is in a sense less primitive since it can be implemented in terms the Fmap and Join) – Dr DR Apr 28 '20 at 10:19
  • there again, you could argue that because Join can be implemented in terms of Bind etc the opposite is true =P – Dr DR Apr 28 '20 at 10:21
  • @DrDR exactly. :) both ways of defining it are equally valid, but the relevance of Join is less immediately apparent, I think. after all, `fs <*> xs == join [ [f x | x <- xs] | f <- fs]` yet it's an *essentially* Applicative code, *not* [essentially Monadic](https://stackoverflow.com/search?tab=votes&q=user%3a849891%20%22essentially%20monadic%22%20%5bmonad%5d) one. – Will Ness Apr 28 '20 at 11:47
  • 1
    While `join` may be cleaner conceptually, `>>=` is more efficient as it fuses flattening and mapping into a single step. Put another way: `join m = m >>= id` traverses `m` once whereas `m >>= f = join (fmap f m)` traverses twice. – Lambda Fairy Apr 28 '20 at 13:34
3

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
Will Ness
  • 70,110
  • 9
  • 98
  • 181
HTNW
  • 27,182
  • 1
  • 32
  • 60