Проблемы, вызванные определением кортежей как функторов

Очень удивительно (я бы даже сказал — внезапно!), но кортеж-пара в GHC является функтором. Это сложно понять, ведь функтор должен иметь только один параметр, а у пары их два. Можно восхищаться тем, как разработчики стандартной библиотеки GHC умудрились предоставить такую абстракцию, но мне кажется, полученное решение все же следует признать неудачным.

Начнем с того, что оно интуитивно непонятно. Скажем, попробуйте вычислить вручную, не используя инструментов GHC, выражение (1+) `fmap` (2, 3). А после этого проверьте полученный результат, например, в ghci. У многих ли результат ручного вычисления совпал с тем, который выдала система? И если у вас результаты все же совпали, мне очень хотелось бы услышать хорошее объяснение того, как именно вам это удалось.

Допустим, мы определяем класс Incable инкрементальных типов:

class Incable t where     inc :: t -> t 

Конечно же, проще и логичнее было бы определить экземпляры этого класса для любых числовых типов:

instance (Num t) => Incable t where     inc = (1+) 

Однако такое определение потребует от нас включить расширение языка UndecidableInstances, а затем и вовсе запутает ситуацию. Поэтому ограничимся определениями только для самых основных представителей класса Num — типов Integer и Double.

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

Очевидно, что для любого функтора можно определить экземпляр класса Incable:

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

Это достаточно хорошее общее определение. Оно позволяет нам работать со списками, массивами, структурами Maybe и еще множеством типов, для которых определены экземпляры класса Functor. Среди всего этого разнообразия окажутся и пары инкрементальных значений. Однако, предположим (даже если мы нашли убедительное объяснение нынешнему поведению пар как функторов), что именно в нашем случае желательно, чтобы увеличивались сразу оба значения в паре. Было бы здорово определить экземпляр класса Incable для пар следующим образом:

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

Увы, это невозможно. Конечно же, компиляция этого фрагмента пройдет успешно, но при попытке воспользоваться таким определением мы получим сообщение о перекрытии экземпляров:

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 (f t)         -- 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) 

Определив экземпляры класса Incable для всех функторов мы сразу же определили его и для пар. К этому стоить добавить, что в GHC определение экземпляра класса нельзя скрыть при импорте. То есть, ненужное нам поведение пары как функтора мы получаем в нагрузку к тем возможностям, которые нам в самом деле необходимы.

Ситуация становится еще сложнее, если мы рассмотрим кортежи, в которых больше двух элементов, например, тройки. Что делать, если нам нужно вычислить что-то вроде inc (1.0, 2, 3)? Казалось бы, здесь у нас точно не должно возникнуть проблем — тройки не были определены как функторы, а значит, мы можем реализовать для них экземпляры класса Incable так, как считаем нужным:

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

И снова компиляция проходит успешно, а при выполнении возникает ошибка:

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 (f t)         -- 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) 

Ну вот, оказывается, что тройки тоже нельзя определить отдельно — они, как и пары, считаются разновидностью функторов. Может быть, наше определение инкрементальной тройки не нужно? Как бы не так! Уберем это определение и попробуем еще раз:

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) 

Теперь нас просят определить тройку как функтор. Может быть, у нас получиться определить экземпляр класса Functor для тройки так, как нам нужно?

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

Никоим образом, у нас не получиться даже скомпилировать такое определение:

[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. 

В качестве последнего шанса попробуем определить тройку как функтор по аналогии с парой:

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

Увы, это определение не компилируется по той же причине, что и предыдущее:

[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. 

Выводы

Вообще говоря, можно сказать, что определение кортежа-пары как функтора оказалось не совсем удачным решением. В некоторых случаях это создает серьезные сложности, которые мешают нам воспользоваться абстракцией функтора. Здесь очень важно ответить на два основных вопроса. Во-первых, зачем вообще понадобилось определять экземпляр класса Functor для пар? Во-вторых, как можно (и можно ли вообще) дать подобное определение для кортежей с числом элементов больше двух (например, для тройки)?

Кроме того, определение пары как разновидности функторов не совсем логично. Принято считать, что функтор — это всегда тип, у которого есть только один параметр. В то же время, даже у простейшего кортежа (пары) уже два параетра. При этом совсем непонятно, почему в определении экземпляра класса Functor для пары указывается только один параметр, а не два?

Конечно же, вполне логично (и даже оправдано) было бы ограничиться определением экземпляров класса Functor только для однотипных кортежей, то есть, кортежей типа (t, t), (t, t, t) и т. п. На первый взгляд, такие кортежи ничем не отличаются от списков, однако их важная особенность в том, что они жестко, уже на этапе компиляции задают количество своих элементов. Подобные типы могут представлять, например, пары и тройки целых чисел, которые должны обрабатываться одновременно и одинаковым образом. В общем случае было бы правильнее использовать многопараметрические функторы.

Многопараметрический функтор является своего рода обобщением обычного функтора, но для типов с несколькими параметрами. Например, двухпараметрический функтор можно определить следующим образом:

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

Логика тут в том, что мы используем не одну функцию, а две, и каждая из них применяется к своему параметру функтора. В таком случае пару можно было бы определить как экземпляр двухпараметрического, а тройку — как экземпляр трехпараметрического функтора. Теперь, если мы примем такое определение, мы будем четко представлять себе, как обрабатывается каждый элемент кортежа, и даже сможем менять это поведение при необходимости. Правда, неясно, насколько полезны такие абстракции, и есть ли в этих классах другие полезные типы, кроме кортежей.

Остается только сказать, что и это не совсем то, что мы хотели получить. На самом деле мы начали с того, что воспользовались функтором, как хорошей моделью для однородных вычислений. В нашем случае, даже несмотря на то, что элеметы пары имеют разные типы, все они принадлежат к одному и тому же классу (инкрементальных типов, при более абстрактном подходе — числовых типов вообще), а функция, которую мы применяем — полиморфная. Поэтому мы вполне могли бы обойтись обычным функтором, если бы могли подсказать компилятору эти особенности нашей предметной области. Увы, в настоящее время я не совсем представляю себе, как это можно сделать.

ссылка на оригинал статьи http://habrahabr.ru/post/259275/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *