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

согласно известной газете идиомы не обращают внимания, стрелки дотошны, монады неразборчивы, выразительная сила стрелы (без каких-либо дополнительных классов типов) должен быть где-то строго между аппликативных функторов и монад: монады эквивалентны 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

3 ответов


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

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

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

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


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

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

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

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


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

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

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)

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

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

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

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

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

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

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

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

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

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

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

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

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

два подхода к arrowized ввода/вывода

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

Клейсли-стрела IOM, по модулю currying, есть:

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.

правильный arrowized I / O интерфейс

вместо преобразования 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 сам конвейер), но мы можем статически определить набор имен файлов, которые будут изменены.

вывод

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