web-dev-qa-db-ja.com

誰かがHaskellのトラバース機能を説明できますか?

Data.Traversableからtraverse関数を理解しようとして失敗しています。その要点がわかりません。私は命令的な背景から来ているので、誰かが命令的なループの観点からそれを私に説明してもらえますか?擬似コードをいただければ幸いです。ありがとう。

90
Konan

traversefmapと同じですが、データ構造の再構築中にエフェクトを実行することもできます。

Data.Traversableドキュメントの例をご覧ください。

 data Tree a = Empty | Leaf a | Node (Tree a) a (Tree a)

FunctorTreeインスタンスは次のようになります。

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のモナドバージョンも含まれています。すべての意図と目的において、mapMtraverseと同じです-Applicativeは後でMonadのスーパークラスになったため、別のメソッドとして存在します。

これを不純な言語で実装する場合、副作用を防ぐ方法がないため、fmaptraverseと同じになります。データ構造を再帰的に走査する必要があるため、ループとして実装することはできません。 Javascriptでどのように行うかを示す小さな例を次に示します。

Node.prototype.traverse = function (f) {
  return new Node(this.l.traverse(f), f(this.k), this.r.traverse(f));
}

このように実装すると、言語で許可されている効果に制限されます。もしあなたが非決定性(Applicativeモデルのリストインスタンス)が必要であり、言語に組み込まれていない場合は、運が悪くなります。

109
Sjoerd Visscher

traverseは、Traversableをものから外す関数を指定すると、Traversable内の要素をApplicativeの「内部」のApplicativeに変換します。

MaybeApplicativeとして、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]]
53
Landei

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

FoldableTraversableの主な違いは、後者では構造の形状を保持できるのに対し、前者では結果を他の値に折り畳む必要があることです。 。


その使用法の簡単な例は、トラバース可能な構造としてリストを使用し、適用可能なものとして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が他のタイプのコンテナで使用されている場合、または他のアプリケーションを使用している場合は、より興味深いものになります。

41
hammar

これは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_これらの関数のバージョン。どちらも関数からの戻り値を無視し、わずかに高速です。

16
Kai Sellgren

traverseisループ。その実装は、トラバースするデータ構造に依存します。これは、リスト、ツリー、MaybeSeq(uence)、またはforループや再帰関数のようなものを介して走査される一般的な方法を持つものです。配列にはforループ、リストにwhileループ、再帰的なツリー、またはスタックとwhileループの組み合わせがあります。しかし、関数型言語では、これらの面倒なループコマンドは必要ありません。ループの内側の部分(関数の形)をデータ構造とより直接的に、より冗長に結合します。

Traversableタイプクラスを使用すると、おそらくより独立した汎用性の高いアルゴリズムを作成できます。しかし、私の経験によると、Traversableは通常、既存のデータ構造にアルゴリズムを接着するためにのみ使用されます。修飾された異なるデータ型に対して同様の関数を記述する必要がないことは非常に素晴らしいことです。

7
comonad