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

Очень удивительно (я бы даже сказал — внезапно!), но кортеж-пара в 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) :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) :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) :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 Логика тут в том, что мы используем не одну функцию, а две, и каждая из них применяется к своему параметру функтора. В таком случае пару можно было бы определить как экземпляр двухпараметрического, а тройку — как экземпляр трехпараметрического функтора. Теперь, если мы примем такое определение, мы будем четко представлять себе, как обрабатывается каждый элемент кортежа, и даже сможем менять это поведение при необходимости. Правда, неясно, насколько полезны такие абстракции, и есть ли в этих классах другие полезные типы, кроме кортежей.Остается только сказать, что и это не совсем то, что мы хотели получить. На самом деле мы начали с того, что воспользовались функтором, как хорошей моделью для однородных вычислений. В нашем случае, даже несмотря на то, что элементы пары имеют разные типы, все они принадлежат к одному и тому же классу (инкрементальных типов, при более абстрактном подходе — числовых типов вообще), а функция, которую мы применяем — полиморфная. Поэтому мы вполне могли бы обойтись обычным функтором, если бы могли подсказать компилятору эти особенности нашей предметной области. Увы, в настоящее время я не совсем представляю себе, как это можно сделать.

© Habrahabr.ru