...

понедельник, 1 июня 2015 г.

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

Очень удивительно (я бы даже сказал — внезапно!), но кортеж-пара в 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


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

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

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Комментариев нет:

Отправить комментарий