有名な論文によると イディオムは気づかず、矢印は細心の注意を払い、モナドは無差別です 、矢印の表現力(追加の型クラスなし)は、厳密に適用可能な関数とモナドの間のどこかにあるはずです:モナドはArrowApply
と同等です、およびApplicative
は、論文で「静的矢印」と呼ばれるものと同等である必要があります。しかし、この「静的」性がどのような制限を意味するのかは私にはわかりません。
問題の3つの型クラスをいじってみると、適用可能なファンクターと矢印の間の同等性を構築することができました。これは、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
IOアプリケーションファンクターを、IOモナドのクライスリ矢印と比較してみましょう。
前の矢印で読み取った値を出力する矢印を作成できます。
runKleisli ((Kleisli $ \() -> getLine) >>> Kleisli putStrLn) ()
しかし、アプリケーションファンクターではそれを行うことはできません。適用可能なファンクターを使用すると、すべての効果が発生しますbefore function-in-the-functorをarguments-in-the-functorに適用します。関数インザファンクターは、引数インザファンクター内の値を使用して、いわばそれ自体の効果を「変調」することはできません。
すべてのアプリケーションは矢印を生成し、すべての矢印はアプリケーションを生成しますが、それらは同等ではありません。矢印arr
と射_arr a b
_がある場合、その機能を複製する射arr o (a \to b)
を生成できるとは限りません。したがって、アプリケーションを往復すると、いくつかの機能が失われます。
Applicativeはモノイダル関数です。矢印はプロ関数であり、プロ関数のカテゴリ、または同等にモノイドでもあります。これらの2つの概念の間に自然なつながりはありません。私の軽蔑を許せば:Haskでは、矢印のプロ関数のファンクター部分はモノイダルファンクターであることがわかりますが、その構造は必然的に「プロ」部分を忘れます。
矢印からApplicativeに移行するときは、入力を受け取る矢印の部分を無視し、出力を処理する部分のみを使用します。多くの興味深い矢印は、入力部分を何らかの形で使用しているため、それらをApplicativeに変えることで、有用なものをあきらめています。
とは言うものの、実際には、より優れた抽象化が適用可能であり、ほとんどの場合、私が望むことを実行します。理論的には矢印の方が強力ですが、実際に矢印を使用しているとは思えません。
(私は以下に投稿しました 私のブログ 拡張された紹介付き)
Tom Ellisは、ファイルI/Oを含む具体的な例について考えることを提案したので、3つの型クラスを使用して3つのアプローチを比較してみましょう。簡単にするために、ファイルから文字列を読み取る操作とファイルに文字列を書き込む操作の2つだけを考慮します。ファイルは、ファイルパスによって識別されます。
_type FilePath = String
_
最初のI/Oインターフェイスは次のように定義されています。
_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 α
_によって操作されるファイル名のセットを事前に知る方法がないことを意味します。 「前払い」とは、純粋関数_fileNames :: IOM α → [FilePath]
_を記述できることを意味します。
もちろん、非IOベースのモナド(ある種の抽出関数_μ α → α
_があるモナドなど)の場合、この区別は少し曖昧になりますが、情報を抽出しようとすることを考えるのは理にかなっています。モナドの効果を評価せずに(たとえば、「タイプ_Reader Γ α
_の値が手元になくても、_Γ
_について何を知ることができるか」と尋ねることができます)。
この意味でモナドで静的解析を実際に実行できない理由は、バインドの右側の関数がHaskell関数の空間にあるため、完全に不透明であるためです。
それでは、インターフェースを適用可能なファンクターだけに制限してみましょう。
_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"
_
まず、テーブルに新しいものをもたらさない(実際にはできない)2つの矢印ベースのI/Oインターフェイスを取得しましょう:_Kleisli IOM
_と_Applicarrow IOF
_。
IOM
のクライスリ矢印、モジュロカリー化は次のとおりです。
_readFile ∷ Kleisli IOM FilePath String
writeFile ∷ Kleisli IOM (FilePath, String) ()
_
writeFile
の入力にはファイル名と内容の両方が含まれているため、copyIndirect
と書くことができます(簡単にするために矢印表記を使用)。 _Kleisli IOM
_のArrowApply
インスタンスが使用されていないことに注意してください。
_copyIndirect ∷ Kleisli IOM (FilePath, FilePath) ()
copyIndirect = proc (index, target) → do
from ← readFile ↢ index
s ← readFile ↢ from
writeFile ↢ (to, s)
_
Applicarrow
の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
はthisArrowインターフェイスでは実装できません。
この引数を逆にすると、(完全なIOA
パイプライン自体を実行する前に)ファイルに何が書き込まれるかを事前に知ることはできませんが、セットを静的に決定できることも意味します。変更されるファイル名の数。
モナドは静的分析に対して不透明であり、適用可能なファンクターは動的時間データの依存関係を表現するのが苦手です。矢印は2つの間のスイートスポットを提供できることがわかりました。純粋に機能的な入力と矢印付きの入力を慎重に選択することで、動的な動作と静的分析への快適さの適切な相互作用を可能にするインターフェイスを作成できます。