0

Pure is used to transform normal function into function in Applicative container. With this, any multi-parameter operations become can be used on Applicative. In this context, pure is not desired to be a -> f a type, it is just desired to be (a -> b) -> f (a -> b) type. But type of pure is a -> f a. Why should normal values can be transformed into Applicative? Is there more purpose for use pure more than transforming functions?

lunuy lunuy
  • 429
  • 3
  • 7
  • 8
    `(a -> b) -> f (a -> b)` **is** a possible instantiation of `pure`'s type. And no, `pure` is not generally “used to transform normal function into function in `Applicative` container”, that sounds rather like you're describing `fmap :: (a -> b) -> f a -> f b`. I'd rather say `pure` is used to transform normal _values_ into values in an `Applicative` context. It so happens that functions are values as well, but that's a special case. – leftaroundabout Apr 17 '22 at 14:18
  • What you are probably thinking about is the `pure f` in `fmap f fx = pure f <*> fx`, but the `fx`, where would it come from? `pure x`, that's where, for one. – Will Ness Apr 18 '22 at 17:52

4 Answers4

6

I don't find the Applicative interface of applying lifted functions (namely, (<*>)) a good intuition. Functions are more complicated to conceptualize for various reasons.

I prefer thinking of Applicative as lifting an n-ary function

liftA0 :: Applicative f => (a)                -> (f a)
liftA  :: Functor     f => (a -> b)           -> (f a -> f b)
liftA2 :: Applicative f => (a -> b -> c)      -> (f a -> f b -> f c)
liftA3 :: Applicative f => (a -> b -> c -> d) -> (f a -> f b -> f c -> f d)

where liftA0 = pure and liftA already exists as fmap defined in terms of Applicative.

The thing is that 0-ary and 1-ary liftings

liftA0 @f @a    :: Applicative f => a -> f a
liftA  @f @a @b :: Applicative f => (a -> b) -> (f a -> f b)

can both take an a -> b function if we instantiate liftA0 = pure at a function type:

liftA0 @f @(a->b) :: Applicative f => (a -> b) -> f (a->b)
liftA  @f @a @b   :: Applicative f => (a -> b) -> (f a -> f b)

So pure @f @(a->b) already has that type.

And pure has plenty of purposes, theoretical which turn out to be practical in Haskell, it is the unit if Applicative is viewed as a Monoid in the category of natural transformations, with (Notions of Computation as Monoids) with Day

type Mempty :: Type -> Type
type Mempty = Identity

type Mappend :: (Type -> Type) -> (Type -> Type) -> (Type -> Type)
type Mappend = Day

mempty :: Applicative f => Mempty ~> f
mempty (Identity a) = pure a

mappend :: Mappend f f ~> f
mappend (LiftA2 (·) as bs) = liftA2 (·) as bs

I just released a library that works with Applicative homomorphisms, that are polymorphic functions that respect the applicative structure. It defines a type class for such structures

type  Idiom :: k -> (Type -> Type) -> (Type -> Type) -> Constraint
class (Applicative f, Applicative g) => Idiom tag f g where
  idiom :: f ~> g

where pure is the initial applicative morphism.

-- https://chrisdone.com/posts/haskell-constraint-trick/
instance (Identity ~ id, Applicative f) => Idiom Initial id f where
  idiom :: Identity ~> f
  idiom (Identity a) = pure a

pure is then frequently used, as a unit for a computation. It is the driving force in Traversable one of the success stories of Haskell

instance Traversable [] where
  traverse :: Applicative f => (a -> f b) -> ([a] -> f [b])
  traverse f []     = pure []
  traverse f (x:xs) = ...

we require pure because our only argument that produces an Applicative-action is f x but with an empty list we don't have an x :: a to feed it. Thus, we need 0-ary lifting.

Iceland_jack
  • 6,848
  • 7
  • 37
  • 46
5

You can define pure :: a -> f a in terms of lift :: (a -> b) -> f (a -> b) and <*>:

pure x = lift (const x) <*> lift (const ())

So it's equivalent either way, and usually simpler to write pure.

(This is in addition to Iceland_jack's excellent summary of the design reasons it should be this way.)

Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
2

Sometimes it's worth approaching questions from the opposite direction. What would you gain if the type of pure was (a -> b) -> f (a -> b)?

As someone calling pure, that's strictly a downgrade. As mentioned, (a -> b) -> f (a -> b) is an instantiation of the current type of pure already. So on the calling side, you only lose options.

The implementation side is the dual here, though. The more concrete a type is, the more options an implementation has. Requiring the argument to be a function means the implementation can take advantage of that to do function-specific things. Like... calling it. That's the only special thing you get to do with functions in Haskell. So to call it, you just need to provide it with a value of some type a that the caller of pure gets to choose. You can get one of those by.. uh.. you can't get one of those. The only option is using undefined or some other bottom value that has a universally-quantified type. What are you going to do? pure f = let x = f undefined in ...? How can that be helpful for implementing pure?

To return to the initial question, then: what would you gain if the type of pure was (a -> b) -> f (a -> b)? As a caller, it's strictly less useful. As an implementer, it provides additional power, but that additional power doesn't help you do anything useful. Where are the upsides to a more concrete type?

Carl
  • 26,500
  • 4
  • 65
  • 86
1

Yes, there is more purpose for pure than transforming functions. It is very common for do blocks to end with a call to pure. It's also handy for giving fallback values by combining its use with <|>.

Also, it meshes well with the underlying category theory; but I don't really consider that a motivating reason. Rather it's first useful and then discovered to coincide with a previously-known category theory concept. (Actually, historically I think it went "define something useful; realize it's the concept of monad; discover the related concept of applicative functors; realize they're useful. So it is in fact a mix of "useful first" and "theory first". But I would never defend its existence because it's there in theory -- only get excited that the theory was insightful.)

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380