web-dev-qa-db-ja.com

矢印とは何ですか?どのように使用できますか?

矢印の意味を学ぼうとしたが、理解できなかった。

Wikibooksチュートリアルを使用しました。ウィキブックの問題は、主にすでにトピックを理解している人のために書かれているように見えることだと思います。

誰かが矢印とは何か、そして私がそれらをどのように使用できるかを説明できますか?

64
fuz

チュートリアルはわかりませんが、具体的な例を見ると、矢印がわかりやすいと思います。矢印の使い方を学んだ最大の問題は、チュートリアルや例で実際にse矢印の方法を示しているのではなく、矢印を作成する方法だけでした。それで、それを念頭に置いて、これが私のミニチュートリアルです。関数とユーザー定義の矢印タイプMyArrの2つの異なる矢印を調べます。

-- type representing a computation
data MyArr b c = MyArr (b -> (c,MyArr b c))

1)矢印は、指定されたタイプの入力から指定されたタイプの出力までの計算です。矢印タイプクラスは、矢印タイプ、入力タイプ、出力タイプの3つのタイプ引数を取ります。見つかったarrowインスタンスのインスタンスヘッドを見てください。

instance Arrow (->) b c where
instance Arrow MyArr b c where

矢印((->)またはMyArr)は、計算を抽象化したものです。

関数b -> cの場合、bは入力で、cは出力です。
MyArr b cの場合、bは入力で、cは出力です。

2)実際に矢印計算を実行するには、矢印タイプに固有の関数を使用します。関数の場合、関数を引数に適用するだけです。他の矢印については、別の関数が必要です(モナドのrunIdentityrunStateなどのように)。

-- 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

これがArrowsが役立つ理由の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のリストを返します。

「矢印」関数はまだ使用していないことに注意してください。つまり、Arrowクラスのメソッドまたはそれらの関数で記述された関数です。

4)上記のコードのほとんどは、各Arrowインスタンスに固有です[1]。 Control.Arrow(およびControl.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型クラスは、モナド計算を>>=関数を使用して単一の新しいモナド計算に組み合わせる手段を提供します。同様に、Arrowクラスは、いくつかのプリミティブ関数(firstarr、および*** 、Control.Categoryの>>>およびidを使用)。モナドと同様に、「矢は何をするのか」という質問です。一般的に答えることはできません。矢次第です。

残念ながら、私は実際の矢の実例の多くを知りません。関数とFRPが最も一般的なアプリケーションのようです。 HXTは、頭に浮かぶ他の唯一の重要な使用法です。

[1] countを除きます。 ArrowLoopの任意のインスタンスに対して同じことを行うcount関数を書くことは可能です。

71
John L

Stack Overflowの履歴を一目で見ると、他の標準型クラスの一部、特にFunctorMonoidに慣れていると想定し、簡単な説明から始めますそれらからの類推。

Functorの単一の操作はfmapで、これはリストのmapの一般化バージョンとして機能します。これは、型クラスの目的のほとんどすべてです。 「マップできるもの」を定義します。したがって、ある意味でFunctorは、リストの特定の側面の一般化を表します。

Monoidの操作は、空のリストと(++)の一般化されたバージョンであり、「ID値である特定のものと連想的に組み合わせることができるもの」を定義します。リストはその説明に当てはまる最も単純なものであり、Monoidはリストのその側面の一般化を表しています。

上記の2つと同じように、Categoryタイプクラスの操作はidおよび(.)の一般化されたバージョンであり、「特定の方向で2つのタイプを接続するもの」を定義します、それは頭から尾に接続することができます。」したがって、これは関数のその側面の一般化を表しています。特にカレーや関数の適用は一般化に含まれていません。

Arrow型クラスはCategoryから構築されますが、基本的な概念は同じです:Arrowsは、関数のように構成され、任意の型に対して「識別矢印」が定義されているものです。 Arrowクラス自体で定義されている追加の操作は、任意の関数をArrowに引き上げる方法と、2つの矢印をタプル間の単一の矢印として「並行して」結合する方法を定義するだけです。

したがって、ここで最初に覚えておかなければならないのは、_Arrowsを構築する式は、本質的に複雑な関数構成であることです。 (***)(>>>)のようなコンビネータは「ポイントフリー」スタイルを記述するためのものであり、proc表記は、物事を配線しながら一時的な名前を入力と出力に割り当てる方法を提供します。

ここで注目すべき便利な点は、ArrowsがMonadsの「次のステップ」であると説明されている場合でも、そこにはそれほど意味のある関係はないということです。どのMonadでも、Kleisliの矢印を操作できます。これは、a -> m bのようなタイプの関数です。 (<=<)Control.Monad演算子は、これらの矢印構成です。一方、Arrowクラスを含めない限り、MonadArrowApplyを取得しません。そのため、そのような直接的な関係はありません。

ここでの主な違いは、Monadsを使用して計算を順番に実行し、ステップごとに処理を実行できるのに対し、Arrowsはある意味で通常の関数と同じように「タイムレス」です。 (.)によって接合される追加の機械や機能を含めることができますが、それはアクションを蓄積するのではなく、パイプラインを構築するようなものです。

他の関連する型クラスは、矢印をEitherおよび(,)と組み合わせることができるなど、矢印に追加の機能を追加します。


Arrowの私のお気に入りの例は、ステートフルストリームトランスデューサーです。これは次のようになります。

data StreamTrans a b = StreamTrans (a -> (b, StreamTrans a b))

StreamTrans矢印は、入力値を出力およびそれ自体の「更新」バージョンに変換します。これがステートフルMonadと異なる点を検討してください。

上記の型のArrowとそれに関連する型クラスのインスタンスを書くことは、それらがどのように機能するかを理解するための良い練習になるかもしれません!

私は 以前に似たような回答 も書いたので、参考になると思います。

34
C. A. McCann

Haskellの矢印は、文献に基づいて表示される矢印よりもはるかに単純であることを付け加えておきます。それらは単に機能を抽象化したものです。

これが実際にどのように役立つかを確認するために、いくつかの関数が純粋であり、一部がモナドである、構成する多数の関数があることを考慮してください。たとえば、_f :: a -> b_、_g :: b -> m1 c_、_h :: c -> m2 d_などです。

関係する各タイプを知っているので、手動でコンポジションを構築できますが、コンポジションの出力タイプは中間モナドタイプ(上記の場合はm1 (m2 d))を反映する必要があります。関数を単に_a -> b_、_b -> c_、および_c -> d_であるかのように扱いたい場合はどうなりますか?つまり、モナドの存在を抽象化して、基になる型についてのみ理由を示したいと思います。これを正確に行うために矢印を使用できます。

IOモナド内の関数の場合、IOモナドの存在を抽象化し、純粋な関数でそれらを作成できるようにwithout 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を呼び出していますが、これらの矢印を多態性関数のライブラリで渡している場合は、「矢印a => a b 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は関係していません。「非表示」にする方法がないことは明らかですIO from arrowUser矢印なし-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
_

unsafePerformIOなしで、そして_arrowUser'_なしでそれを書いてみてください。モナド型引数を処理する必要はありません。

30
John Wiegley

AFP(高度な関数型プログラミング)ワークショップからのジョン・ヒューズの講義ノートがあります。これらは、Arrowクラスがベースライブラリで変更される前に記述されたものです。

http://www.cse.chalmers.se/~rjmh/afp-arrows.pdf

2
stephen tetley

Arrowの構成(本質的にはモナド)の調査を始めたときの私のアプローチは、最も一般的に関連付けられている関数の構文と構成から抜け出し、より宣言的なアプローチを使用してその原理を理解することから始めました。これを念頭に置いて、次の内訳はより直感的です。

function(x) {
  func1result = func1(x)
  if(func1result == null) {
    return null
  } else {
    func2result = func2(func1result)
    if(func2result == null) {
      return null
    } else {
      func3(func2result)
    } 

したがって、基本的には、ある値xに対して、最初にnull(func1)を返す可能性がある関数を呼び出し、別の関数がnullを返すかnull交換可能に、最後に、nullも返す可能性のある3番目の関数。次に、値xを指定して、xをfunc3に渡します。そのときのみ、nullが返されない場合は、この値をfunc2に渡し、この値がnullでない場合にのみ、この値をfunc1。より確定的で、制御フローにより、より高度な例外処理を構築できます。

ここでは、矢印の構成を利用できます:(func3 <=< func2 <=< func1) x

0
kinokaf