Strzałki są dokładnie równoważne funkcjom aplikacyjnym?
Według słynnego pisma idiomy są nieświadome, strzały skrupulatne, monady rozwiązłe, moc ekspresyjna strzałek (bez dodatkowych typeklas) powinna znajdować się gdzieś ściśle między funkcjami aplikacyjnymi a monadami: monady są równoważne ArrowApply
, A Applicative
powinny być równoważne czemuś, co Gazeta nazywa "strzałkami statycznymi". Nie jest jednak dla mnie jasne, jakie ograniczenie to "statyczne"oznacza.
Zabawa z trzema typami w pytanie, udało mi się zbudować równoważność między funkcjami aplikacyjnymi a strzałkami, którą przedstawiam poniżej w kontekście znanej równoważności między Monad
i ArrowApply
. Czy ta konstrukcja jest prawidłowa? (Udowodniłem większość prawa strzał zanim się nim znudzę). Czy to nie znaczy, że Arrow
i Applicative
są dokładnie takie same?
{-# 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 answers
Porównajmy funktor aplikacyjny IO ze strzałkami Kleisli z monady IO.
Możesz mieć strzałkę, która wyświetla wartość odczytaną przez poprzednią strzałkę:
runKleisli ((Kleisli $ \() -> getLine) >>> Kleisli putStrLn) ()
Ale nie możesz tego zrobić z funkcjami aplikacyjnymi. W przypadku funktorów aplikacyjnych wszystkie efekty zachodzą przed zastosowaniem functora do argumentów-in-the-functor. Funkcja-in-the-functor nie może użyć wartości wewnątrz argumentu-in-the-functor do "modulacji" własnego efektu, tak aby mów.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-10 06:18:54
Każdy aplikator daje strzałkę, a każda strzałka daje aplikator, ale nie są one równoważne. Jeśli masz strzałkę arr
i morfizm arr a b
nie wynika z tego, że możesz wygenerować morfizm arr o (a \to b)
, który replikuje jego funkcjonalność. Tak więc, jeśli podróż przez aplikatora tracisz niektóre funkcje.
Aplikatory są funktorami monoidalnymi. Strzałki są profunktorami, które są również kategoriami lub równoważnie monoidami w kategorii profunktorów. Nie ma naturalnego związek między tymi dwoma pojęciami. Jeśli wybaczycie moją lekkomyślność: w Hask okazuje się, że funktor części pro-funktora w strzałce jest funktorem monoidalnym, ale ta konstrukcja koniecznie zapomina o części "pro".
Przechodząc od strzałek do aplikacji ignorujesz część strzałki, która pobiera dane wejściowe i używasz tylko części, która zajmuje się danymi wyjściowymi. Wiele ciekawych strzałek używa części wejściowej w taki czy inny sposób, a więc zamieniając je w aplikatory, jesteś rezygnacja z przydatnych rzeczy.
To powiedziawszy, w praktyce uważam applicative za ładniejszą abstrakcję do pracy i taką, która prawie zawsze robi to, co chcę. W teorii strzałki są mocniejsze, ale nie znajduję siebie używającego ich w praktyce.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-29 15:39:36
[[43]} (zamieszczam poniżej na mój blog z rozszerzonym wprowadzeniem)
Tom Ellis zasugerował zastanowienie się nad konkretnym przykładem z udziałem we/wy plików, więc porównajmy trzy podejścia do tego za pomocą trzech typów. Aby wszystko było proste, będziemy dbać tylko o dwie operacje: odczyt łańcucha z pliku i zapis łańcucha do pliku. Pliki będą identyfikowane po ich ścieżce:
type FilePath = String
Monadic I / O
Nasz pierwszy interfejs I/O jest zdefiniowany jak następuje:
data IOM ∷ ⋆ → ⋆
instance Monad IOM
readFile ∷ FilePath → IOM String
writeFile ∷ FilePath → String → IOM ()
Używając tego interfejsu, możemy na przykład skopiować plik z jednej ścieżki do drugiej:
copy ∷ FilePath → FilePath → IOM ()
copy from to = readFile from >>= writeFile to
Możemy jednak zrobić znacznie więcej: wybór plików, którymi manipulujemy, może zależeć od efektów wyjściowych. Na przykład, poniższa funkcja pobiera plik indeksu zawierający nazwę pliku i kopiuje go do podanego katalogu docelowego:
copyIndirect ∷ FilePath → FilePath → IOM ()
copyIndirect index target = do
from ← readFile index
copy from (target ⟨/⟩ to)
Z drugiej strony oznacza to, że nie ma możliwości poznania z góry zestawu nazw plików, które będą manipulowane przez podaną wartość action ∷ IOM α
. Mówiąc "z góry", mam na myśli możliwość napisania czystej funkcji fileNames :: IOM α → [FilePath]
.
Oczywiście, dla monad Nie opartych na IO (takich jak te, dla których mamy jakąś funkcję ekstrakcyjną μ α → α
), rozróżnienie to staje się nieco bardziej rozmyte, ale nadal sensowne jest myślenie o próbie wyodrębnienia informacji bez oceny efektów monad (więc na przykład, możemy zapytać "co możemy wiedzieć o Reader Γ α
bez posiadania wartości typu Γ
w ręka?").
Powodem, dla którego nie możemy wykonać analizy statycznej w tym sensie na monadach, jest to, że funkcja po prawej stronie Binda znajduje się w przestrzeni funkcji Haskella i jako taka jest całkowicie nieprzezroczysta.
Spróbujmy więc ograniczyć nasz interfejs do funkcji aplikacyjnej.
Applicative I/O
data IOF ∷ ⋆ → ⋆
instance Applicative IOF
readFile ∷ FilePath → IOF String
writeFile ∷ FilePath → String → IOF ()
Ponieważ IOF
nie jest monadą, nie ma możliwości komponowania readFile
i writeFile
, więc wszystko, co możemy zrobić z tym interfejsem, to albo odczytać z pliku a następnie postprocesować jego zawartość czysto, lub zapisać do pliku; ale nie ma sposobu, aby zapisać zawartość pliku do innego.
Może zmienisz Typ writeFile
?
writeFile′ ∷ FilePath → IOF (String → ())
Głównym problemem z tym interfejsem jest to, że chociaż pozwala na pisanie czegoś takiego jak
copy ∷ FilePath → FilePath → IOF ()
copy from to = writeFile′ to ⟨*⟩ readFile from
Prowadzi to do wszelkiego rodzaju nieprzyjemnych problemów, ponieważ String → ()
jest tak okropnym modelem zapisu łańcucha znaków do pliku, ponieważ łamie on referencyjną przezroczystość. Na przykład, czego oczekujesz zawartość out.txt
być po uruchomieniu tego programu?
(λ write → [write "foo", write "bar", write "foo"]) ⟨$⟩ writeFile′ "out.txt"
Dwa podejścia do arrowized I / O
Po pierwsze, usuńmy dwa interfejsy I / O oparte na strzałkach, które nie wprowadzają (w rzeczywistości nie mogą) niczego nowego do tabeli: Kleisli IOM
i Applicarrow IOF
.
Strzałka Kleisli z IOM
, modulo currying, to:
readFile ∷ Kleisli IOM FilePath String
writeFile ∷ Kleisli IOM (FilePath, String) ()
Ponieważ wejście writeFile
nadal zawiera zarówno nazwę pliku, jak i zawartość, możemy nadal pisać copyIndirect
(używając notacji strzałek dla uproszczenia). Uwaga jak instancja ArrowApply
Kleisli IOM
nie jest nawet używana.
copyIndirect ∷ Kleisli IOM (FilePath, FilePath) ()
copyIndirect = proc (index, target) → do
from ← readFile ↢ index
s ← readFile ↢ from
writeFile ↢ (to, s)
Applicarrow
Z IOF
będzie:
readFile ∷ FilePath → Applicarrow IOF () String
writeFile ∷ FilePath → String → Applicarrow IOF () ()
Który oczywiście nadal wykazuje ten sam problem braku możliwości komponowania readFile
i writeFile
.
A proper arrowized I / O interface
Zamiast przekształcać IOM
lub IOF
w strzałkę, co jeśli zaczniemy od zera i spróbujemy stworzyć coś pomiędzy, w kategoriach gdzie używamy funkcji Haskella i gdzie robimy strzałkę? Weź następujące interfejs:
data IOA ∷ ⋆ → ⋆ → ⋆
instance Arrow IOA
readFile ∷ FilePath → IOA () String
writeFile ∷ FilePath → IOA String ()
Ponieważ writeFile
pobiera zawartość ze strony wejściowej strzałki, nadal możemy zaimplementować copy
:
copy ∷ FilePath → FilePath → IOA () ()
copy from to = readFile from >>> writeFile to
Jednak drugi argument writeFile
jest czysto funkcjonalny, a więc nie może zależeć od wyjścia np. readFile
; więc copyIndirect
nie może być zaimplementowany za pomocą tego {95]} interfejsu Arrow.
Jeśli odwrócimy ten argument, oznacza to również, że chociaż nie możemy wcześniej wiedzieć, co zostanie zapisane w pliku (przed uruchomieniem pełny IOA
pipeline), ale możemy statycznie określić zestaw nazw plików, które będą modyfikowane.
Podsumowanie
Monady są nieprzezroczyste dla analizy statycznej, a funkcje aplikacyjne są słabe w wyrażaniu zależności danych w czasie dynamicznym. Okazuje się, że strzałki mogą zapewnić słodki punkt między tymi dwoma: starannie wybierając czysto funkcjonalne i strzałkowe wejścia, możliwe jest stworzenie interfejsu, który pozwala na właściwe współdziałanie dynamicznych zachowań i podatność na analizę statyczną.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-04 01:16:45