Стрелки в точности эквивалентны аппликативным функторам?

Согласно знаменитой статье Идиомы не обращают внимания, стрелки являются дотошными, монады неразборчивы, выразительная сила стрел (без каких-либо дополнительных типов) должно быть где-то строго между аппликативными функторами и монадами: монады эквивалентны ArrowApply, а Applicative должны быть эквивалентны тому, что бумага называет "статическими стрелками". Однако мне непонятно, какое ограничение имеет это "статичность".

Играя с тремя рассматриваемыми типами стилей, я смог создать эквивалентность между аппликативными функторами и стрелками, которые я приводил ниже в контексте хорошо известной эквивалентности между Monad и ArrowApply. Правильно ли эта конструкция? (Я доказал большую часть законов стрелок, прежде чем его надоело). Не означает ли это, что Arrow и Applicative точно такие же?

{-# LANGUAGE TupleSections, NoImplicitPrelude #-}
import Prelude (($), const, uncurry)

-- In the red corner, we have arrows, from the land of * -> * -> *
import Control.Category
import Control.Arrow hiding (Kleisli)

-- In the blue corner, we have applicative functors and monads,
-- the pride of * -> *
import Control.Applicative
import Control.Monad

-- Recall the well-known result that every monad yields an ArrowApply:
newtype Kleisli m a b = Kleisli{ runKleisli :: a -> m b}

instance (Monad m) => Category (Kleisli m) where
    id = Kleisli return
    Kleisli g . Kleisli f = Kleisli $ g <=< f

instance (Monad m) => Arrow (Kleisli m) where
    arr = Kleisli . (return .)
    first (Kleisli f) = Kleisli $ \(x, y) -> liftM (,y) (f x)

instance (Monad m) => ArrowApply (Kleisli m) where
    app = Kleisli $ \(Kleisli f, x) -> f x

-- Every arrow arr can be turned into an applicative functor
-- for any choice of origin o
newtype Arrplicative arr o a = Arrplicative{ runArrplicative :: arr o a }

instance (Arrow arr) => Functor (Arrplicative arr o) where
    fmap f = Arrplicative . (arr f .) . runArrplicative

instance (Arrow arr) => Applicative (Arrplicative arr o) where
    pure = Arrplicative . arr . const

    Arrplicative af <*> Arrplicative ax = Arrplicative $
        arr (uncurry ($)) . (af &&& ax)

-- Arrplicatives over ArrowApply are monads, even
instance (ArrowApply arr) => Monad (Arrplicative arr o) where
    return = pure
    Arrplicative ax >>= f =
        Arrplicative $ (ax >>> arr (runArrplicative . f)) &&& id >>> app

-- Every applicative functor f can be turned into an arrow??
newtype Applicarrow f a b = Applicarrow{ runApplicarrow :: f (a -> b) }

instance (Applicative f) => Category (Applicarrow f) where
    id = Applicarrow $ pure id
    Applicarrow g . Applicarrow f = Applicarrow $ (.) <$> g <*> f

instance (Applicative f) => Arrow (Applicarrow f) where
    arr = Applicarrow . pure
    first (Applicarrow f) = Applicarrow $ first <$> f

Ответ 1

Сравним прикладной функционал IO с стрелками Клейсли монады IO.

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

runKleisli ((Kleisli $ \() -> getLine) >>> Kleisli putStrLn) ()

Но вы не можете сделать это с помощью аппликативных функторов. С помощью аппликативных функторов все эффекты имеют место перед тем, как применить функцию-в-функторе к аргументам-в-функторе. Функция-in-the-functor не может использовать значение внутри аргумента-в-функторе для "модуляции" своего собственного эффекта, так сказать.

Ответ 2

Каждое приложение дает стрелку, и каждая стрелка дает аппликативный, но они не эквивалентны. Если у вас есть стрелка arr и морфизм arr a b, из этого не следует, что вы можете сгенерировать морфизм arr o (a \to b), который реплицирует его функциональность. Таким образом, если вы совершите кругосветное путешествие, вы потеряете некоторые функции.

Применения являются моноидальными функторами. Стрелы - это профинансы, которые также являются категориями или, что то же самое, моноидами в категории профуклонов. Между этими двумя понятиями нет естественной связи. Если вы извините мое легкомысленность: в Hask оказывается, что часть функтора про-функтора в стрелке является моноидальным функтором, но эта конструкция обязательно забывает о "про".

Когда вы переходите от стрелок к аппликациям, вы игнорируете часть стрелки, которая принимает входные данные и использует только часть, которая имеет дело с выходом. Многие интересные стрелки используют входную часть так или иначе, и поэтому, превращая их в аппликативные средства, вы отказываетесь от полезных вещей.

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

Ответ 3

(Я разместил ниже мой блог с расширенным введением)

Том Эллис предложил подумать о конкретном примере, связанном с файловыми вводами/выводами, поэтому давайте сравним три подхода к нему с помощью трех типов. Чтобы все было просто, мы будем заботиться только о двух операциях: чтение строки из файла и запись строки в файл. Файлы будут идентифицироваться по пути к файлам:
type FilePath = String

Монадический ввод/вывод

Наш первый интерфейс ввода/вывода определяется следующим образом:

data IOM ∷ ⋆ → ⋆
instance Monad IOM
readFile ∷ FilePath → IOM String
writeFile ∷ FilePath → String → IOM ()

Используя этот интерфейс, мы можем, например, скопировать файл с одного пути на другой:

copy ∷ FilePath → FilePath → IOM ()
copy from to = readFile from >>= writeFile to

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

copyIndirect ∷ FilePath → FilePath → IOM ()
copyIndirect index target = do
    from ← readFile index
    copy from (target ⟨/⟩ to)

С другой стороны, это означает, что нет способа узнать заранее набор имен файлов, которые будут обрабатываться заданным значением action ∷ IOM α. Под "upfront" я имею в виду способность писать чистую функцию fileNames :: IOM α → [FilePath].

Конечно, для монад, не основанных на IO (таких как те, для которых у нас есть какая-то экстракторная функция μ α → α), это различие становится немного более нечетким, но все же имеет смысл подумать о попытке извлечь информацию, не оценивая влияния монады (так, например, мы могли бы спросить "что мы можем знать о Reader Γ α без значения типа Γ под рукой?" ).

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

Итак, попробуйте ограничить наш интерфейс только аппликативным функтором.

Аппликативный ввод/вывод

data IOF ∷ ⋆ → ⋆
instance Applicative IOF
readFile ∷ FilePath → IOF String
writeFile ∷ FilePath → String → IOF ()

Так как IOF не является монадой, не существует возможности компоновки readFile и writeFile, поэтому все, что мы можем сделать с этим интерфейсом, - это либо читать из файла, либо затем обрабатывать его содержимое чисто или писать файл; но нет способа записать содержимое файла в другой.

Как насчет изменения типа writeFile?

writeFile′ ∷ FilePath → IOF (String → ())

Основная проблема с этим интерфейсом заключается в том, что, хотя это позволит написать что-то вроде

copy ∷ FilePath → FilePath → IOF ()
copy from to = writeFile′ to ⟨*⟩ readFile from

это приводит ко всем неприятным проблемам, потому что String → () - такая ужасная модель записи строки в файл, так как она нарушает ссылочную прозрачность. Например, что вы ожидаете от содержимого out.txt после запуска этой программы?

(λ write → [write "foo", write "bar", write "foo"]) ⟨$⟩ writeFile′ "out.txt"

Два подхода для ввода/вывода со стрелками

Прежде всего, позвольте получить два интерфейса ввода-вывода на основе стрелок, которые не могут (фактически, не могут) вносить что-то новое в таблицу: Kleisli IOM и Applicarrow IOF.

Клейсли-стрелка IOM по модулю каррирования:

readFile ∷ Kleisli IOM FilePath String
writeFile ∷ Kleisli IOM (FilePath, String) ()

Поскольку вход writeFile по-прежнему содержит как имя файла, так и содержимое, мы все равно можем записать copyIndirect (используя для обозначения простоты обозначение стрелки). Обратите внимание, что экземпляр ArrowApply Kleisli IOM даже не используется.

copyIndirect ∷ Kleisli IOM (FilePath, FilePath) ()
copyIndirect = proc (index, target) → do
    from ← readFile ↢ index
    s ← readFile ↢ from
    writeFile ↢ (to, s)

Applicarrow of IOF будет:

readFile ∷ FilePath → Applicarrow IOF () String
writeFile ∷ FilePath → String → Applicarrow IOF () ()

который, конечно же, по-прежнему демонстрирует ту же проблему, что и неспособность составить readFile и writeFile.

Собственный интерфейс ввода-вывода со стрелками

Вместо преобразования IOM или IOF в стрелку, что, если мы начнем с нуля, и попытаемся создать что-то промежуточное, с точки зрения того, где мы используем функции Haskell и где делаем стрелку? Возьмите следующий интерфейс:

data IOA ∷ ⋆ → ⋆ → ⋆
instance Arrow IOA
readFile ∷ FilePath → IOA () String
writeFile ∷ FilePath → IOA String ()

Поскольку writeFile принимает содержимое со стороны ввода стрелки, мы все равно можем реализовать copy:

copy ∷ FilePath → FilePath → IOA () ()
copy from to = readFile from >>= writeFile to

Однако другой аргумент writeFile является чисто функциональным, и поэтому он не может зависеть от вывода, например. readFile; поэтому copyIndirect не может быть реализован с помощью этого интерфейса со стрелкой.

Если мы включим этот аргумент, это также означает, что, хотя мы не можем заранее знать, что будет записано в файл (перед запуском полного конвейера IOA), но мы можем статически определить набор имен файлов, которые будут изменены.

Заключение

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