📜 ⬆️ ⬇️

Problems caused by defining tuples as functors

Very surprising (I would even say - all at once!), But a tuple-pair in GHC is a functor. It is difficult to understand, because the functor must have only one parameter, and the pair has two of them. One can admire how the developers of the standard GHC library have managed to provide such an abstraction, but it seems to me that the resulting solution should be considered unsuccessful.

To begin with, it is intuitively incomprehensible. Say, try to calculate manually, without using the GHC tools, the expression (1+) `fmap` (2, 3) . And after that, check the result obtained, for example, in ghci . For many, did the result of a manual calculation coincide with the one that the system issued? And if your results still coincided, I would really like to hear a good explanation of how you did it.

Suppose we define an Incable class of incremental types:

 class Incable t where inc :: t -> t 

Of course, it would be simpler and more logical to define instances of this class for any numeric types:
')
 instance (Num t) => Incable t where inc = (1+) 

However, such a definition would require us to include an extension of the UndecidableInstances language, and then completely confuse the situation. Therefore, we confine ourselves to the definitions only for the most basic representatives of the class Num - types Integer and Double .

 instance Incable Integer where inc = (1+) instance Incable Double where inc = (1+) 

Obviously, for any functor, you can define an instance of the class Incable :

 instance (Functor f, Incable t) => Incable (ft) where inc = fmap inc 

This is a pretty good general definition. It allows us to work with lists, arrays, Maybe structures and another set of types for which instances of the Functor class are defined. Among all this diversity will be a pair of incremental values. However, suppose (even if we have found a convincing explanation for the current behavior of pairs as functors) that in our case it is desirable that both values ​​in a pair increase at once. It would be great to define an instance of the class Incable for pairs as follows:

 instance (Incable t1, Incable t2) => Incable (t1, t2) where inc (x1, x2) = (inc x1, inc x2) 

Alas, it is impossible. Of course, the compilation of this fragment will be successful, but when we try to use this definition, we will receive a message about overlapping instances:

 ghci> inc (1, 2) <interactive>:105:1: Overlapping instances for Incable (t0, t1) arising from a use of `inc' Matching instances: instance (Functor f, Incable t) => Incable (ft) -- Defined at src/Main.hs:16:10 instance (Incable t1, Incable t2) => Incable (t1, t2) -- Defined at src/Main.hs:18:10 In the expression: inc (1, 2) In an equation for `it': it = inc (1, 2) 

Having defined instances of the class Incable for all functors, we immediately defined it for pairs. Add to this that in GHC the definition of a class instance cannot be hidden during import. That is, we get unnecessary behavior of the pair as a functor into the load to those capabilities that we really need.

The situation becomes even more complicated if we consider tuples in which there are more than two elements, for example, triples. What if we need to calculate something like inc (1.0, 2, 3) ? It would seem that here we definitely should have no problems - the triples were not defined as functors, which means we can implement instances of the Incable class for them as we see fit:

 instance (Incable t1, Incable t2, Incable t3) => Incable (t1, t2, t3) where inc (x1, x2, x3) = (inc x1, inc x2, inc x3) 

Again, the compilation is successful, and an error occurs during execution:

 ghci> inc (1.0, 2, 3) <interactive>:14:1: Overlapping instances for Incable (t0, t1, t2) arising from a use of `inc' Matching instances: instance (Functor f, Incable t) => Incable (ft) -- Defined at src/Main.hs:18:10 instance (Incable t1, Incable t2, Incable t3) => Incable (t1, t2, t3) -- Defined at src/Main.hs:20:10 In the expression: inc (1.0, 2, 3) In an equation for `it': it = inc (1.0, 2, 3) 

Well, it turns out that the triples, too, cannot be defined separately - they, like couples, are considered a kind of functors. Maybe our definition of an incremental triple is not needed? No matter how wrong! Remove this definition and try again:

 ghci> inc (1.0, 2, 3) <interactive>:12:1: No instance for (Functor ((,,) t0 t1)) arising from a use of `inc' Possible fix: add an instance declaration for (Functor ((,,) t0 t1)) In the expression: inc (1.0, 2, 3) In an equation for `it': it = inc (1.0, 2, 3) 

Now we are asked to define a triple as a functor. Maybe we can Functor to define an instance of the class Functor for the troika as we need it?

 instance Functor ((,,) t1 t2) where fmap f (x1, x2, x3) = (fmap f x1, fmap f x2, fmap f x3) 

In no way, we cannot even compile such a definition:

 [1 of 1] Compiling Main ( src/Main.hs, interpreted ) src/Main.hs:19:36: Couldn't match expected type `b' with actual type `f0 b' `b' is a rigid type variable bound by the type signature for fmap :: (a -> b) -> (t1, t2, a) -> (t1, t2, b) at src/Main.hs:19:5 In the return type of a call of `fmap' In the expression: fmap f x3 In the expression: (x1, x2, fmap f x3) src/Main.hs:19:43: Couldn't match expected type `a' with actual type `f0 a' `a' is a rigid type variable bound by the type signature for fmap :: (a -> b) -> (t1, t2, a) -> (t1, t2, b) at src/Main.hs:19:5 In the second argument of `fmap', namely `x3' In the expression: fmap f x3 In the expression: (x1, x2, fmap f x3) Failed, modules loaded: none. 

As a last chance, let's try to define a triple as a functor by analogy with a pair:

 instance Functor ((,,) t1 t2) where fmap f (x1, x2, x3) = (x1, x2, fmap f x3) 

Alas, this definition is not compiled for the same reason as the previous one:

 [1 of 1] Compiling Main ( src/Main.hs, interpreted ) src/Main.hs:19:36: Couldn't match expected type `b' with actual type `f0 b' `b' is a rigid type variable bound by the type signature for fmap :: (a -> b) -> (t1, t2, a) -> (t1, t2, b) at src/Main.hs:19:5 In the return type of a call of `fmap' In the expression: fmap f x3 In the expression: (x1, x2, fmap f x3) src/Main.hs:19:43: Couldn't match expected type `a' with actual type `f0 a' `a' is a rigid type variable bound by the type signature for fmap :: (a -> b) -> (t1, t2, a) -> (t1, t2, b) at src/Main.hs:19:5 In the second argument of `fmap', namely `x3' In the expression: fmap f x3 In the expression: (x1, x2, fmap f x3) Failed, modules loaded: none. 

Discussion


Generally speaking, we can say that the definition of a tuple-pair as a functor was not an entirely successful solution. In some cases, this creates serious difficulties that prevent us from using the abstraction of the functor. It is very important to answer two basic questions. First, why did you even need to define an instance of the class Functor for couples? Secondly, how is it possible (and is it even possible) to give a similar definition for tuples with more than two elements (for example, for a triple)?

In addition, the definition of a pair as a variety of functors is not entirely logical. It is considered that a functor is always a type that has only one parameter. At the same time, even the simplest tuple (pair) already has two parameters. At the same time, it is completely unclear why in the definition of an instance of the class Functor for a pair, only one parameter is indicated, and not two?

Of course, it would be logical (and even justified) to confine ourselves to defining instances of the class Functor for tuples of the same type, that is, tuples of the type (t, t) , (t, t, t) , etc. At first glance, such Tuples are no different from lists, but their important feature is that they are tough, at the compilation stage, they specify the number of their elements. Such types can represent, for example, pairs and triples of integers that must be processed simultaneously and in the same way. In the general case, it would be more correct to use multiparameter functors.

A multiparameter functor is a kind of generalization of the ordinary functor, but for types with several parameters. For example, a two-parameter functor can be defined as follows:

 class Functor2 f where fmap2 :: (a1 -> b1) -> (a2 -> b2) -> f a1 a2 -> f b1 b2 

The logic here is that we use not one function, but two, and each of them is applied to its own parameter of the functor. In this case, the pair could be defined as a two-parameter instance, and the triple could be defined as an instance of a three-parameter functor. Now, if we accept this definition, we will have a clear idea of ​​how each element of the tuple is processed, and we can even change this behavior if necessary. However, it is unclear how useful such abstractions are, and whether there are other useful types in these classes besides tuples.

It remains only to say that this is not exactly what we wanted to receive. In fact, we began by using the functor as a good model for homogeneous calculations. In our case, even though the elements of the pair are of different types, they all belong to the same class (incremental types, with a more abstract approach, numerical types in general), and the function we use is polymorphic. Therefore, we could well do the usual functor, if we could tell the compiler these features of our subject area. Alas, at the present time I do not quite imagine how this can be done.

findings


As a result of the discussion (thanks to him to all the readers who immediately responded to my problems) I managed to formulate certain results. As the author, I decided to slightly correct the article so that it does not leave the impression of incompleteness.

First of all, I want to apologize for a very annoying mistake that I made when defining triples as functors. Of course, the correct code should look like this:

 instance Functor ((,,) t1 t2) where fmap f (x1, x2, x3) = (x1, x2, f x3) 

This code is compiled and works as expected - the changes concern only the last element. Similarly, in our case, I would like to implement a code of the form:

 instance Functor ((,,) t1 t2) where fmap f (x1, x2, x3) = (f x1, f x2, f x3) 

But it does not compile, although for a slightly different reason:

  [1 of 1] Compiling Main (src / Main.hs, interpreted)

 src / Main.hs: 19: 28:
     Couldn't match type `b 'with` t2'
       `b 'is a rigid type variable bound by
           the type signature for
             fmap :: (a -> b) -> (t1, t2, a) -> (t1, t2, b)
           at src / Main.hs: 19: 5
       `t2 'is a rigid type variable bound by
            the instance declaration at src / Main.hs: 18: 10
     Expected type: t1
       Actual type: b
     In the return type of a call of `f '
     In the expression: f x1
     In the expression: (f x1, f x2, f x3)

 src / Main.hs: 19: 30:
     Couldn't match type `t1 'with` t2'
       `t1 'is a rigid type variable bound by
            the instance declaration at src / Main.hs: 18: 10
       `t2 'is a rigid type variable bound by
            the instance declaration at src / Main.hs: 18: 10
     Expected type: a
       Actual type: t1
     In the first argument of `f ', namely` x1'
     In the expression: f x1
     In the expression: (f x1, f x2, f x3)

 src / Main.hs: 19: 34:
     Couldn't match type `a 'with` t2'
       `a 'is a rigid type variable bound by
           the type signature for
             fmap :: (a -> b) -> (t1, t2, a) -> (t1, t2, b)
           at src / Main.hs: 19: 5
       `t2 'is a rigid type variable bound by
            the instance declaration at src / Main.hs: 18: 10
     Expected type: t2
       Actual type: b
     In the return type of a call of `f '
     In the expression: f x2
     In the expression: (f x1, f x2, f x3)
 Failed, modules loaded: none.

Now I understand exactly how the pairing trick was done. In fact, any multiparameter type can be transformed into a one-parameter one by partial application, and then a functor can be realized for it.

Nevertheless, I still insist that the definition of a pair as a functor at the system library level was a bad decision. This is supported by the fact that for other multiparameter types (for example, higher-order tuples, maps, etc.), instances of the Functor class are not provided in the system library. I suspect that the developers simply needed the behavior of the functor for pairs somewhere "in place", and then they found it useful to raise it to the system level. To test this version, the easiest way is to rebuild the system library without defining pairs as functors, but I don’t have time for such an experiment right now.

Obviously, at the system level, it is reasonable to leave only the definition of pairs as bifunctors , and even generalize this approach for higher order functors. Of course, it always remains possible, by partial application, to obtain a simplified definition of a functor of a lower order than the type order. In this case, the structure elements excluded by the partial application of types can no longer be processed. It is more logical to describe the same behavior by a partial application of the mapping function, in which the first arguments are the identical function of id . Unlike globally defined instances, such reduced display functions are easily hidden when necessary when importing the corresponding module.

Source: https://habr.com/ru/post/259275/


All Articles