Home > Enterprise >  How does the typeclass bimap not have a circular definition
How does the typeclass bimap not have a circular definition

Time:10-29

What key intuition am I missing to understand this code? I do not understand how these compose. first expects a function (a->c) and constructed type (f a b). Yet, second yields (f a d). Furthermore, first and second are defined in terms of bimap which seems circular.

class Bifunctor f where
  bimap :: (a -> c) -> (b -> d) -> f a b -> f c d
  bimap g h = first g . second h
  first :: (a -> c) -> f a b -> f c b
  first g = bimap g id
  second :: (b -> d) -> f a b -> f a d
  second = bimap id

CodePudding user response:

second is curried. You can write the implied parameter and it will be symmetrical with first.

first g = bimap g id
second g = bimap id g

As for your question about whether the definitions are circular, the answer is yes. They are circular. According to a strict reading of the Haskell standard, the following implementation is correct

data F a b = F a b

instance Bifunctor F -- No functions implemented; take all three defaults

That will compile, according to the Haskell standard. But any attempt to call first, second, or bimap will bottom out, since they'll just call each other forever.

However, if you look at the source code for Bifunctor, there's another little annotation.

{-# MINIMAL bimap | first, second #-}

This is not an ordinary comment. This is a special instruction directed at GHC (the most popular Haskell compiler). GHC in particular supports minimal directives. This declares that an instance of Bifunctor is only complete if it implements at least bimap or both first and second. So on GHC, if you fail to satisfy that condition in your own instance, you'll get a compiler error. This is also reflected in the Hackage docs

Minimal complete definition

bimap | first, second

The expectation is that you, as the programmer, override some of these defaults, and the remaining defaults can just silently fall into place.

You can see the same behavior more directly in the built-in typeclass Eq

class  Eq a  where
    (==), (/=)           :: a -> a -> Bool

    {-# INLINE (/=) #-}
    {-# INLINE (==) #-}
    x /= y               = not (x == y)
    x == y               = not (x /= y)
    {-# MINIMAL (==) | (/=) #-}

Both (==) and (/=) are implemented in terms of each other, and there's a MINIMAL directive at the bottom indicating that any instance must implement at least one of the two functions.

  • Related