Data.Traversable
からtraverse
関数を理解しようとして失敗しています。その要点がわかりません。私は命令的な背景から来ているので、誰かが命令的なループの観点からそれを私に説明してもらえますか?擬似コードをいただければ幸いです。ありがとう。
traverse
はfmap
と同じですが、データ構造の再構築中にエフェクトを実行することもできます。
Data.Traversable
ドキュメントの例をご覧ください。
data Tree a = Empty | Leaf a | Node (Tree a) a (Tree a)
Functor
のTree
インスタンスは次のようになります。
instance Functor Tree where
fmap f Empty = Empty
fmap f (Leaf x) = Leaf (f x)
fmap f (Node l k r) = Node (fmap f l) (f k) (fmap f r)
f
をすべての値に適用して、ツリー全体を再構築します。
instance Traversable Tree where
traverse f Empty = pure Empty
traverse f (Leaf x) = Leaf <$> f x
traverse f (Node l k r) = Node <$> traverse f l <*> f k <*> traverse f r
Traversable
インスタンスは、コンストラクターが適用スタイルで呼び出されることを除いて、ほぼ同じです。これは、ツリーの再構築中に(副作用)の影響があることを意味します。 Applicativeは、モナドとほとんど同じですが、効果は以前の結果に依存できないことを除きます。この例では、たとえば、左のブランチを再構築した結果に応じて、ノードの右のブランチとは異なることができなかったことを意味します。
歴史的な理由から、Traversable
クラスには、traverse
と呼ばれるmapM
のモナドバージョンも含まれています。すべての意図と目的において、mapM
はtraverse
と同じです-Applicative
は後でMonad
のスーパークラスになったため、別のメソッドとして存在します。
これを不純な言語で実装する場合、副作用を防ぐ方法がないため、fmap
はtraverse
と同じになります。データ構造を再帰的に走査する必要があるため、ループとして実装することはできません。 Javascriptでどのように行うかを示す小さな例を次に示します。
Node.prototype.traverse = function (f) {
return new Node(this.l.traverse(f), f(this.k), this.r.traverse(f));
}
このように実装すると、言語で許可されている効果に制限されます。もしあなたが非決定性(Applicativeモデルのリストインスタンス)が必要であり、言語に組み込まれていない場合は、運が悪くなります。
traverse
は、Traversable
をものから外す関数を指定すると、Traversable
内の要素をApplicative
の「内部」のApplicative
に変換します。
Maybe
をApplicative
として、listをTraversable
として使用しましょう。まず、変換関数が必要です。
half x = if even x then Just (x `div` 2) else Nothing
そのため、数値が偶数の場合、その半分(Just
内)を取得し、それ以外の場合はNothing
を取得します。すべてが「うまくいく」場合、次のようになります。
traverse half [2,4..10]
--Just [1,2,3,4,5]
だが...
traverse half [1..10]
-- Nothing
その理由は、<*>
関数を使用して結果を作成し、引数の1つがNothing
である場合、Nothing
が返されるためです。
もう一つの例:
rep x = replicate x x
この関数は、コンテンツx
を持つ長さx
のリストを生成します。 rep 3
= [3,3,3]
。 traverse rep [1..3]
の結果は何ですか?
rep
を使用して、[1]
、[2,2]
、および[3,3,3]
の部分的な結果を取得します。ここで、Applicatives
としてのリストのセマンティクスは、「すべての組み合わせを取る」です。 (+) <$> [10,20] <*> [3,4]
は[13,14,23,24]
です。
[1]
と[2,2]
の「すべての組み合わせ」は[1,2]
の2倍です。 2回の[1,2]
と[3,3,3]
のすべての組み合わせは、6回の[1,2,3]
です。だから私たちは:
traverse rep [1..3]
--[[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3]]
sequenceA
は次のように定義できるため、traverse
の観点から理解するのが最も簡単だと思います。
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
traverse f = sequenceA . fmap f
sequenceA
は、構造体の要素を左から右に並べ、結果を含む同じ形状の構造体を返します。
sequenceA :: (Traversable t, Applicative f) => t (f a) -> f (t a)
sequenceA = traverse id
sequenceA
は、2つのファンクターの順序を逆にしたものと考えることもできます。アクションのリストから結果のリストを返すアクションに移動します。
そのため、traverse
は何らかの構造を取り、f
を適用して構造内のすべての要素を何らかの適用可能要素に変換し、それらの適用可能要素の効果を左から右に並べ、同じ構造を持つ構造体を返します。結果を含む形状。
また、関連する関数traverse_
を定義するFoldable
と比較することもできます。
traverse_ :: (Foldable t, Applicative f) => (a -> f b) -> t a -> f ()
Foldable
とTraversable
の主な違いは、後者では構造の形状を保持できるのに対し、前者では結果を他の値に折り畳む必要があることです。 。
その使用法の簡単な例は、トラバース可能な構造としてリストを使用し、適用可能なものとしてIO
を使用することです。
λ> import Data.Traversable
λ> let qs = ["name", "quest", "favorite color"]
λ> traverse (\thing -> putStrLn ("What is your " ++ thing ++ "?") *> getLine) qs
What is your name?
Sir Lancelot
What is your quest?
to seek the holy grail
What is your favorite color?
blue
["Sir Lancelot","to seek the holy grail","blue"]
この例はかなり刺激的ではありませんが、traverse
が他のタイプのコンテナで使用されている場合、または他のアプリケーションを使用している場合は、より興味深いものになります。
これはfmap
に似ていますが、マッパー関数内でエフェクトを実行でき、結果タイプも変更される点が異なります。
データベース内のユーザーIDを表す整数のリストを想像してください:[1, 2, 3]
。これらのユーザーIDをユーザー名にfmap
したい場合は、従来のfmap
を使用できません。関数内では、ユーザー名を読み取るためにデータベースにアクセスする必要があるためです(効果が必要です- -この場合、IO
モナドを使用)。
traverse
の署名は次のとおりです。
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
traverse
を使用すると効果が得られるため、ユーザーIDをユーザー名にマッピングするコードは次のようになります。
mapUserIDsToUsernames :: (Num -> IO String) -> [Num] -> IO [String]
mapUserIDsToUsernames fn ids = traverse fn ids
mapM
という関数もあります。
mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)
mapM
の使用はすべてtraverse
に置き換えることができますが、その逆はできません。 mapM
はモナドに対してのみ機能しますが、traverse
はより一般的です。
効果を達成するだけで有用な値を返さない場合は、traverse_
およびmapM_
これらの関数のバージョン。どちらも関数からの戻り値を無視し、わずかに高速です。
traverse
isループ。その実装は、トラバースするデータ構造に依存します。これは、リスト、ツリー、Maybe
、Seq
(uence)、またはforループや再帰関数のようなものを介して走査される一般的な方法を持つものです。配列にはforループ、リストにwhileループ、再帰的なツリー、またはスタックとwhileループの組み合わせがあります。しかし、関数型言語では、これらの面倒なループコマンドは必要ありません。ループの内側の部分(関数の形)をデータ構造とより直接的に、より冗長に結合します。
Traversable
タイプクラスを使用すると、おそらくより独立した汎用性の高いアルゴリズムを作成できます。しかし、私の経験によると、Traversable
は通常、既存のデータ構造にアルゴリズムを接着するためにのみ使用されます。修飾された異なるデータ型に対して同様の関数を記述する必要がないことは非常に素晴らしいことです。