Is there a non cyclic definition for `liftA2` and `<*>` on stackage.org

222 views Asked by At

On stackage.org, the following cyclic declaration exists for liftA2 and <*> for the Applicative typeclass.

       (<*>) = liftA2 id
liftA2 f x y = f <$> x <*> y

Is a non-cyclic declaration available for liftA2 or <*> on the site. Are such completely cyclic references an oversight?

UPDATE:

The following (necessary) clarifying declarations seems to be missing from hoogle docs:

<*> :: Functor F => F (a -> b) -> F a -> F b

and by implication (due to cyclic declaration)

liftA2 :: Functor F => (a -> b -> c) -> F a -> F b -> F c
2

There are 2 answers

6
Jon Purdy On

[…] the following cyclic definition exists for liftA2 and <*> for the Applicative type class.

       (<*>) = liftA2 id
liftA2 f x y = f <$> x <*> y

Is a non-cyclic definition available for liftA2 or <*> on the site.

These aren’t definitions of the methods, exactly; they’re default definitions. A typeclass with one parameter is just a set of types, and an instance definition is the price of admission for membership in the set. The Minimal pragma for Applicative tells you that you must implement one of these two methods, and that information is displayed in Haddock documentation.

The actual definitions of liftA2, <*>, and pure are specific to the instances of Applicative. Generally speaking, if a typeclass contains methods that can be implemented using only the other methods of the typeclass, then that method doesn’t strictly need to be part of the class, since it could be a top-level definition with a constraint.

However, such a method may be included anyway. This may be just for convenience, when it’s easier to define an instance in terms of one function even though it’s not the “most fundamental” operation. It’s also often for performance reasons: redundant methods tend to be included when it’s possible to implement a method more efficiently than the default for a particular type. In this case, for example, liftA2 may be able to traverse two structures together more efficiently than traversing one with <$> and then the other separately with <*>.

GHC also offers DefaultSignatures as a way to add more specific defaults, typically defined in terms of Generic, but this only lets you add typeclass constraints, largely for convenience with deriving.

Are such completely cyclic references an oversight?

Not at all, they’re intentional. Cyclic definitions in default implementations of typeclass methods are quite common. For example, Eq is defined in the Haskell Report something like this:

class Eq a where
  (==) :: a -> a -> Bool
  x == y = not (x /= y)

  (/=) :: a -> a -> Bool
  x /= y = not (x == y)

It is possible to forget to implement one of these, so that they both use the default, and thus represent an infinite loop, however:

  • This generates a warning by default (-Wmissing-methods is enabled by -Wdefault).

  • If a Minimal pragma is not specified, all of the methods in the class are assumed to be required.

So really the only other options in this case are to remove one or the other from the class, or omit providing a default for one of them. If you want to know about how these methods are implemented for Applicative, the thing to look at is the instance implementations for concrete type constructors like [], ZipList, Maybe, StateT, and so on.

11
willeM_ Van Onsem On

Is a non-cyclic definition available for liftA2?

The implementations of (<*>) and liftA2 are specific to the instances of the Applicative typeclass. Indeed, for each Applicative instance you need to implement pure, and (<*>) or liftA2, or as specified by the MINIMAL pragma:

class Functor f => Applicative f where
    {-# MINIMAL pure, ((<*>) | liftA2) #-}
    -- …

For example for the Maybe instance of Applicative, this is implemented as:

instance Applicative Maybe where
    pure = Just

    Just f  <*> m       = fmap f m
    Nothing <*> _m      = Nothing

    liftA2 f (Just x) (Just y) = Just (f x y)
    liftA2 _ _ _ = Nothing

    Just _m1 *> m2      = m2
    Nothing  *> _m2     = Nothing

Simply implementing pure and (<*>) would here be sufficient, since then liftA2 is implemented in terms of (<*>). It is however often more efficient to implement the other methods as well.