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