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
Author: Cactus, 2014-07-10

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.

 25
Author: danidiaz,
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.

 29
Author: Philip JF,
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 copyIndirectnie 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ą.

 9
Author: Cactus,
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