Понимание стрел в Хаскеле - PullRequest
53 голосов
/ 01 июля 2010

Я пытался справиться со стрелками, так как они являются основой большинства FRP реализаций.Я думаю, что понимаю основную идею - они связаны с монадами, но хранят статическую информацию по каждому оператору связывания, так что вы можете пройтись по цепочке стрелок и посмотреть на статическую информацию без необходимости оценивать всю стрелку.* Но я заблудился в тот момент, когда мы начинаем обсуждать первый, второй и своп.Какое отношение имеет 2-кортеж со стрелками?Учебники представляют материал кортежа, как будто это был очевидный следующий шаг, но я не вижу связи.

В таком случае, что означает синтаксис стрелки интуитивно?

1 Ответ

46 голосов
/ 01 июля 2010

Пожалуйста, посмотрите в http://www.cs.yale.edu/homes/hudak/CS429F04/AFPLectureNotes.pdf,, который объясняет, как работают стрелки в FRP.

2-кортежа используются при определении стрелок, потому что это необходимо для представления функции со стрелкой, принимающей 2 аргумента.

В FRP константы и переменные часто представлены в виде стрелок, которые игнорируют его «ввод», например,

twelve, eleven :: Arrow f => f p Int
twelve = arr (const 12)
eleven = arr (const 11)

Приложения функций затем превращаются в композиции (>>>):

# (6-) 12

arr (6-) <<< twelve

Теперь, как мы можем превратить функцию с двумя аргументами в стрелку? Например

(+) :: Num a => a -> a -> a

из-за карри мы можем рассматривать это как функцию, возвращающую функцию. Так

arr (+) :: (Arrow f, Num a) => f a (a -> a)

Теперь давайте применим его к константе

arr (+)             -- # f     a (a -> a)
  <<< twelve        -- # f b Int
                      :: f b     (Int -> Int)

+----------+      +-----+      +--------------+
| const 12 |----> | (+) |  ==  | const (+ 12) |
+----------+      +-----+      +--------------+

эй, подожди, это не работает. Результатом по-прежнему остается стрелка, которая возвращает функцию, но мы ожидаем что-то похожее на f Int Int. Мы замечаем, что в Arrow происходит сбой каррирования, потому что разрешена только композиция. Поэтому мы должны откатить сначала функцию

uncurry :: (a -> b -> c) -> ((a, b) -> c)

uncurry (+) :: Num a => (a, a) -> a

Тогда у нас есть стрелка

(arr.uncurry) (+) :: (Num a, Arrow f) => f (a, a) a

Из-за этого возникает 2-кортеж. Тогда для работы с этими 2-мя кортежами необходимы такие функции, как &&&.

(&&&) :: f a b -> f a d -> f a (b, d)

, тогда сложение может быть правильно выполнено.

(arr.uncurry) (+)        -- # f   (a,    a) a
  <<<     twelve         -- # f b  Int
      &&& eleven         -- # f b      Int
                           :: f b           a

+--------+
|const 12|-----.
+--------+     |       +-----+      +----------+
              &&&====> | (+) |  ==  | const 23 |
+--------+     |       +-----+      +----------+
|const 11|-----'
+--------+

(Теперь, почему нам не нужны такие вещи, как &&&& для трех кортежей для функций с тремя аргументами? Потому что вместо этого можно использовать ((a,b),c).)


Редактировать: Из оригинальной статьи Джона Хьюза Обобщая Монады в Стрелы , в ней указывается причина

4,1 Стрелы и пары

Однако, хотя в случае монад операторы return и >>= - это все, что нам нужно, чтобы начать писать полезный код, для стрелок аналогичных операторов arr и >>> недостаточно. Даже простая монадическая функция сложения, которую мы видели ранее

   add :: Monad m => m Int -> m Int -> m Int
   add x y = x >>= \u -> (y >>= \v -> return (u + v))

пока нельзя выразить в виде стрелки. Делая зависимость от входа явным, мы видим, что аналогичное определение должно иметь вид

   add :: Arrow a => a b Int -> a b Int -> a b Int
   add f g = ...

, где мы должны объединить f и g в последовательности. Единственный доступный оператор секвенирования - >>>, но f и g не имеют правильных типов для составления. Действительно, функция add должна сохранить входные данные типа b в расчете на f, чтобы иметь возможность предоставить тот же вход для g. Аналогично, результат f должен быть сохранен во время вычисления g, чтобы два результата в итоге можно было сложить и вернуть. Комбинации стрелок, представленные ранее, не дают нам способа сохранить значение в другом вычислении, и поэтому у нас нет другого выбора, кроме как ввести другой комбинатор.

...