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