简体   繁体   中英

Haskell : what is the sense in: instance Functor ((->) r)

I admit, that my question may stem from a lack of knowledge and be rather vague. But I try to understand, have some doubts and can't resolve them.

So GHC.Base have such definition, and what is the sense in it:

instance Functor ((->) r) where
    fmap = (.)
  1. From the viewpoint of programming language: We have really base construction (->), I think more base than anything, but maybe terms, and you describe it as a part of very derivative construction (instance Functor). What is the sense? (->) is (->). Functor have any sense as far as (->) described under Haskell hood meaningfully. But not vice versa: (->) have sense while Functor described in Haskell libraries correctly.

  2. From the viewpoint of lambda calculus: 2.1 If from "common sense" definition "(->) r" is a container around r (let's call it "Any_f"), then how function fmap shoul work? fmap should change value into container, but do not change container-structure, try to write it.

    fmap f (Any_f x) <=> Any_f (fx)

(yes, this is non-typed lambda calculus)

2.2. But let's look how Functor ((->) r) defined in Haskell:

instance Functor ((->) r) where
    fmap = (.)
    -- Or in other words (quotation marks intentionaly):
    -- fmap f (Any_f x) = f (Any_f x) 
    -- fmap :: forall a, b, c => (b -> c) -> (a -> b) -> (a -> c)

So:

  • "common sense" (not change container structure) tells us to write:

    fmap f (Any_f_as_container x) = Any_f_as_container (fx)

  • types requirements tell us to write:

    fmap f (any_f_as_container x) = f (Any_f_as_container x)

Doesn't this means that "instance Functor ((->) r)" is meaningless? And if not - what sense does it has when it changes outermost function (container itself, not container value)?

I will try to convince you that fmap = (.) really is a thing that leaves a container's shape the same, but applies a function to all the elements in the container. But before we do that for (->) , let's do it for some simpler types. Specifically, let's do it for types that are containers with a specific number of elements -- ie, a container with exactly two elements will be TwoF , while one with no elements will be ZeroF . Like this:

data ZeroF  a = ZeroF
data OneF   a = OneF   a
data TwoF   a = TwoF   a a
data ThreeF a = ThreeF a a a

What should the Functor instances for these look like? Well, the one for OneF looks exactly like in your question:

instance Functor OneF where
    fmap f (OneF x) = OneF (f x)

The other ones look pretty similar -- just applying f more (or fewer) times to account for the fact that there are more (or fewer) elements. Here they all are, with some creative whitespace to highlight the similarities/pattern:

instance Functor ZeroF  where fmap f (ZeroF          ) = ZeroF
instance Functor OneF   where fmap f (OneF   x0      ) = OneF   (f x0)
instance Functor TwoF   where fmap f (TwoF   x0 x1   ) = TwoF   (f x0) (f x1)
instance Functor ThreeF where fmap f (ThreeF x0 x1 x2) = ThreeF (f x0) (f x1) (f x2)

Hopefully for now you agree that this definitely has the flavor of Functor instance that you described in your question: keep the shape of the container the same, and apply the given function f to each element contained within.

So those are containers with a given number of elements. Now, let's write accessors for these containers -- ie we want the equivalent of (!!) for lists, where given a number, we pull out that field from the container. Since there's zero elements in a ZeroF , we'll need an indexing type with zero values; while for ThreeF we need an indexing type with three values.

data Zero
data One   = One0
data Two   = Two0   | Two1
data Three = Three0 | Three1 | Three2

The indexing functions have types that look like this:

indexZero  :: ZeroF  a -> Zero  -> a
indexOne   :: OneF   a -> One   -> a
indexTwo   :: TwoF   a -> Two   -> a
indexThree :: ThreeF a -> Three -> a

I won't implement them all -- they're pretty straightforward -- but here's one to give you the idea in case it's not immediately obvious.

indexTwo (TwoF x0 x1) Two0 = x0
indexTwo (TwoF x0 x1) Two1 = x1

It turns out that the indexing functions have an inverse -- if you give me a function which, when given an index, produces a value, then I can give you a container with those values in it. The types look like this:

tabulateZero  :: (Zero  -> a) -> ZeroF  a
tabulateOne   :: (One   -> a) -> OneF   a
tabulateTwo   :: (Two   -> a) -> TwoF   a
tabulateThree :: (Three -> a) -> ThreeF a

(Do you see why this is the right type for an inverse? Note that, say, TwoF a -> Two -> a is the same type as TwoF a -> (Two -> a) ,) Just to give you an idea of how these are implemented, in case it's not immediately obvious: we simply apply the indexing function to each index:

tabulateZero  ix = ZeroF
tabulateOne   ix = OneF   (ix One0  )
tabulateTwo   ix = TwoF   (ix Two0  ) (ix Two1  )
tabulateThree ix = ThreeF (ix Three0) (ix Three1) (ix Three2)

It's not too hard to prove that tabulateX. indexX = id tabulateX. indexX = id and indexX. tabulateX = id indexX. tabulateX = id for each X , ie that tabulation really is the inverse of indexing.

Okay, but hold up now and take a look at what we've just done: we have turned a function (like Two -> a ) into a container (like TwoF a ), and vice versa. The types Two -> a and TwoF a are, morally speaking, exactly the same thing . So it seems reasonable to think we could implement fmap for Two -> a -- for example, just by converting to TwoF a and back as appropriate!

twomap :: (a -> b) -> (Two -> a) -> (Two -> b)
twomap f = indexTwo . fmap f . tabulateTwo

Let's visualize what that's doing. We'll start with an arbitrary indexing function:

\case Two0 -> x0; Two1 -> x1

Now we go through the process:

               \case Two0 -> x0; Two1 -> x1
tabulateTwo
               TwoF x0 x1
fmap f
               TwoF (f x0) (f x1)
indexTwo
               \case Two0 -> f x0; Two1 -> f x1

Since f gets applied in both branches, we could pull that out of the case :

               f . (\case Two0 -> x0; Two1 -> x1)

That second term is exactly the indexing function we started out with. In other words, we have just determined another, simpler implementation for twomap :

twomap f ix = f . ix

If you work through similar reasoning for zeromap , onemap , and threemap , you'll discover they actually all have that same implementation; We can do this uniformly for all the various sizes of container just by going polymorphic; instead of having onemap for changing One -> a 's, etc., let's have an xmap for changing x -> a 's:

xmap :: (a -> b) -> (x -> a) -> (x -> b)
xmap f ix = f . ix

Of course, we don't have to name f and ix :

xmap = (.)

...and this is the Functor instance for (x -> _) .

(->) isn't just syntax. It's an operator like any other, but at the type level instead of the term level. It has a kind Type -> Type -> Type , which means if you apply it to a single type, you get back not a type, but another "function" of kind Type -> Type .

Type -> Type is the kind of all functors, so it's reasonable to think the partially applied (->) operator might be a functor as well, which is what

instance Functor ((->) r) where
    fmap = (.)

defines. That is, mapping a function over another function means to compose the two functions.

As a "container", think of a function (something of type r -> a ) as containing all possible values of type a that you can get by applying the function to an argument of type r . fmap will apply a function to whatever the other function returns. (Or in theory, apply it to every value that the other could return.)

So the answer is: functions can be represent either as (a -> b) or as Map ab - for function with finite count of possible arguments these are literally two equivalent representations.

So instance Functor (Map r) is meaningful and it would be implemented just as instance Functor ((->) r) implemented already.

And the answer above is confirmed by the implementation of instance Functor ((,) r) . Yes this is a bit different than Map r , but as close as possible.

PS @chepner: I can't mark your answer as "best answer" because I don't understand (and almost don't agree) with one word in one sentense:

(->) isn't just syntax. It's an operator like any other

Function is not "like any other" operation (I used notion "construction") function is magical- or under-hood-compiler- construction, on which all other fuctions are based.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM