矢の意味を学ぼうとしたのですが、わかりませんでした。
ウィキブックスのチュートリアルを使用しました。ウィキブックスの問題は、主にそのトピックをすでに理解している人のために書かれているように見えることだと思います。
誰かが矢印とは何か、そして私がそれらをどのように使うことができるかを説明できますか?
チュートリアルはわかりませんが、矢印は具体例を見ていただくと分かりやすいと思います。矢印の使い方を学ぶ上で私が抱えていた最大の問題は、チュートリアルや例のどれも実際に矢印の使い方を示しておらず、矢印の作成方法だけを示していることでした。それを念頭に置いて、ここに私のミニチュートリアルがあります。関数とユーザー定義の矢印タイプの 2 つの異なる矢印を調べますMyArr
。
-- type representing a computation
data MyArr b c = MyArr (b -> (c,MyArr b c))
1) アローは、指定されたタイプの入力から指定されたタイプの出力への計算です。矢印の型クラスは、矢印の型、入力の型、および出力の型の 3 つの型引数を取ります。矢印インスタンスのインスタンス ヘッドを見ると、次のことがわかります。
instance Arrow (->) b c where
instance Arrow MyArr b c where
矢印 ( または のいずれ(->)
かMyArr
) は、計算の抽象化です。
関数 の場合b -> c
、b
は入力で、c
は出力です。
a のMyArr b c
場合、b
は入力で、c
は出力です。
2) 実際に矢印の計算を実行するには、矢印の種類に固有の関数を使用します。関数の場合は、関数を引数に適用するだけです。他のアローの場合、別の関数が必要です (モナドの 、 などと同様) runIdentity
。runState
-- run a function arrow
runF :: (b -> c) -> b -> c
runF = id
-- run a MyArr arrow, discarding the remaining computation
runMyArr :: MyArr b c -> b -> c
runMyArr (MyArr step) = fst . step
3) 矢印は、入力のリストを処理するために頻繁に使用されます。関数の場合、これらは並行して実行できますが、特定のステップでの一部の矢印の出力は、以前の入力に依存します (たとえば、現在の入力の合計を維持します)。
-- run a function arrow over multiple inputs
runFList :: (b -> c) -> [b] -> [c]
runFList f = map f
-- run a MyArr over multiple inputs.
-- Each step of the computation gives the next step to use
runMyArrList :: MyArr b c -> [b] -> [c]
runMyArrList _ [] = []
runMyArrList (MyArr step) (b:bs) = let (this, step') = step b
in this : runMyArrList step' bs
これが、矢印が役立つ理由の 1 つです。それらは、状態をプログラマーに公開することなく、暗黙的に状態を利用できる計算モデルを提供します。プログラマーは、矢印化された計算を使用し、それらを組み合わせて洗練されたシステムを作成できます。
受信した入力の数をカウントする MyArr を次に示します。
-- count the number of inputs received:
count :: MyArr b Int
count = count' 0
where
count' n = MyArr (\_ -> (n+1, count' (n+1)))
これで、関数runMyArrList count
はリストの長さ n を入力として取り、1 から n までの Int のリストを返します。
「アロー」関数はまだ使用していないことに注意してください。これは、アロー クラス メソッドまたはそれらに関して記述された関数です。
4) 上記のコードのほとんどは、各 Arrow インスタンスに固有のものです [1]。Control.Arrow
(および)内のすべては、Control.Category
矢印を構成して新しい矢印を作成することです。Category が別のクラスではなく、Arrow の一部であると仮定すると、次のようになります。
-- combine two arrows in sequence
>>> :: Arrow a => a b c -> a c d -> a b d
-- the function arrow instance
-- >>> :: (b -> c) -> (c -> d) -> (b -> d)
-- this is just flip (.)
-- MyArr instance
-- >>> :: MyArr b c -> MyArr c d -> MyArr b d
この>>>
関数は 2 つの矢印を取り、最初の出力を 2 番目の入力として使用します。
一般に「ファンアウト」と呼ばれる別の演算子を次に示します。
-- &&& applies two arrows to a single input in parallel
&&& :: Arrow a => a b c -> a b c' -> a b (c,c')
-- function instance type
-- &&& :: (b -> c) -> (b -> c') -> (b -> (c,c'))
-- MyArr instance type
-- &&& :: MyArr b c -> MyArr b c' -> MyArr b (c,c')
-- first and second omitted for brevity, see the accepted answer from KennyTM's link
-- for further details.
Control.Arrow
計算を組み合わせる手段を提供するため、1 つの例を次に示します。
-- function that, given an input n, returns "n+1" and "n*2"
calc1 :: Int -> (Int,Int)
calc1 = (+1) &&& (*2)
calc1
複雑な折り畳みに役立つような関数や、たとえばポインターを操作する関数を頻繁に見つけました。
型クラスは、関数Monad
を使用してモナド計算を単一の新しいモナド計算に結合する手段を提供し>>=
ます。同様に、このクラスは、いくつかのプリミティブ関数 ( 、、および、およびControl.Category から)Arrow
を使用して、矢印化された計算を 1 つの新しい矢印化された計算に結合する手段を提供します。モナドと同様に、「矢印は何をするのか?」という質問も同様です。一概には答えられません。矢によります。first
arr
***
>>>
id
残念ながら、野生の矢印インスタンスの多くの例を知りません。機能とFRPが最も一般的なアプリケーションのようです。HXT は、頭に浮かぶ唯一の他の重要な使用法です。
[1] を除くcount
。の任意のインスタンスに対して同じことを行う count 関数を書くことができますArrowLoop
。
スタック オーバーフローに関するあなたの歴史をひと目見て、あなたが他の標準型クラスのいくつか、特にFunctor
とMonoid
に慣れていると仮定して、それらからの簡単な類推から始めます。
on リストの一般化されたバージョンとして機能するon の単一操作Functor
はです。これは、型クラスの目的のほとんどすべてです。「マッピングできるもの」を定義します。したがって、ある意味では、リストの特定の側面の一般化を表しています。fmap
map
Functor
の操作Monoid
は、空のリスト および の一般化されたバージョンであり、(++)
「識別値である特定のものと連想的に組み合わせることができるもの」を定義します。リストは、その説明に適合する最も単純なものでありMonoid
、リストのその側面の一般化を表しています。
上記の2つと同様に、Category
型クラスに対する操作は と の一般化版でid
あり(.)
、「2つの型を特定の方向に接続するもので、頭と尾をつなぐことができるもの」を定義します。したがって、これはfunctionsのその側面の一般化を表しています。一般化に含まれていないのは、カリー化または関数の適用です。
Arrow
型クラスは から構築されますがCategory
、基本的な概念は同じです。Arrow
s は、関数のように構成され、任意の型に対して定義された「識別矢印」を持つものです。クラス自体で定義された追加の操作はArrow
、任意の関数を にリフトするArrow
方法と、タプル間の単一の矢印として「並列に」2 つの矢印を結合する方法を定義するだけです。
したがって、ここで最初に覚えておくべきことは、式の構築Arrow
は本質的に複雑な関数合成であるということです。コンビネータは「ポイントフリー」スタイルを書くためのもの(***)
であり、表記法は、配線中に入力と出力に一時的な名前を割り当てる方法を提供します。(>>>)
proc
ここで注意すべきことは、Arrow
s が s の「次のステップ」であると説明されることもありますがMonad
、そこにはそれほど意味のある関係はないということです。Monad
のような型を持つ単なる関数である Kleisli の矢印を使用できますa -> m b
。(<=<)
演算子 inは、これらのControl.Monad
矢印合成です。一方、クラスも含めない限り、 Arrow
s は a を取得しません。したがって、直接的な関係はありません。Monad
ArrowApply
ここでの重要な違いは、Monad
s は計算を順序付けて段階的に実行するために使用できるのに対し、Arrow
s はある意味で通常の関数と同様に「時代を超越した」ものであるということです。によって接合される追加の機械や機能を含めることができますが、(.)
アクションの蓄積ではなく、パイプラインの構築に似ています。
他の関連する型クラスは、矢印を や と組み合わせることができるなど、追加の機能を矢印に追加Either
し(,)
ます。
の私のお気に入りの例は、Arrow
次のようなステートフル ストリーム トランスデューサです。
data StreamTrans a b = StreamTrans (a -> (b, StreamTrans a b))
StreamTrans
矢印は、入力値を出力とそれ自体の「更新された」バージョンに変換します。これがステートフルと異なる点を検討してMonad
ください。
上記の型のインスタンスArrow
とそれに関連する型クラスを記述することは、それらがどのように機能するかを理解するための良い練習になるかもしれません!
以前にも同様の回答を書きましたので、参考になるかもしれません。
Haskell の矢印は、文献に基づいて表示されるよりもはるかに単純であることを付け加えたいと思います。それらは単に関数の抽象化です。
これが実際にどのように役立つかを理解するために、構成したい関数がたくさんあると考えてください。そのうちのいくつかは純粋で、いくつかはモナドです。たとえば、f :: a -> b
、g :: b -> m1 c
、およびh :: c -> m2 d
。
関連する各型を知っていれば、合成を手作業で作成できますが、合成の出力型は中間のモナド型 (上記の場合はm1 (m2 d)
) を反映する必要があります。関数を , ,だけa -> b
であるかのように扱いたい場合はどうすればよいでしょうか? つまり、モナドの存在を抽象化し、基礎となる型についてのみ推論したいのです。まさにこれを行うために矢印を使用できます。b -> c
c -> d
これは、IO モナド内の関数の IO の存在を抽象化する矢印です。これにより、構成コードが IO が関与していることを知る必要なく、純粋な関数でそれらを構成できます。IO 関数をラップする IOArrow を定義することから始めます。
data IOArrow a b = IOArrow { runIOArrow :: a -> IO b }
instance Category IOArrow where
id = IOArrow return
IOArrow f . IOArrow g = IOArrow $ f <=< g
instance Arrow IOArrow where
arr f = IOArrow $ return . f
first (IOArrow f) = IOArrow $ \(a, c) -> do
x <- f a
return (x, c)
次に、作成したいいくつかの単純な関数を作成します。
foo :: Int -> String
foo = show
bar :: String -> IO Int
bar = return . read
そしてそれらを使用します:
main :: IO ()
main = do
let f = arr (++ "!") . arr foo . IOArrow bar . arr id
result <- runIOArrow f "123"
putStrLn result
ここでは IOArrow と runIOArrow を呼び出していますが、これらの矢印をポリモーフィック関数のライブラリで渡す場合、"Arrow a => ab c" 型の引数を受け入れるだけで済みます。モナドが関与していることをライブラリコードに知らせる必要はありません。矢印の作成者とエンド ユーザーのみが知る必要があります。
IOArrow を任意のモナドの関数で機能するように一般化することは、「Kleisli アロー」と呼ばれ、まさにそれを行うためのビルトイン アローが既に存在します。
main :: IO ()
main = do
let g = arr (++ "!") . arr foo . Kleisli bar . arr id
result <- runKleisli g "123"
putStrLn result
もちろん、矢印合成演算子と proc 構文を使用して、矢印が関係していることを少し明確にすることもできます。
arrowUser :: Arrow a => a String String -> a String String
arrowUser f = proc x -> do
y <- f -< x
returnA -< y
main :: IO ()
main = do
let h = arr (++ "!")
<<< arr foo
<<< Kleisli bar
<<< arr id
result <- runKleisli (arrowUser h) "123"
putStrLn result
ここでmain
、 IO モナドが関与していることはわかっていても、関与
arrowUser
していないことは明らかです。arrowUser
矢印なしでIO を「隠す」方法はありませんunsafePerformIO
。中間のモナド値を純粋な値に戻すことに頼る必要があります (したがって、そのコンテキストを永久に失います)。例えば:
arrowUser' :: (String -> String) -> String -> String
arrowUser' f x = f x
main' :: IO ()
main' = do
let h = (++ "!") . foo . unsafePerformIO . bar . id
result = arrowUser' h "123"
putStrLn result
を使わずに、 Monad 型の引数を扱わunsafePerformIO
ずに書いてみてください。arrowUser'
AFP (Advanced Functional Programming) ワークショップからの John Hughes の講義ノートがあります。Base ライブラリで Arrow クラスが変更される前に記述されていることに注意してください。