このようなfoldlMの定義を書き留めるには、どのような知識やトレーニングが必要ですか?[閉まっている]


9

最近、私は実際のケース生産システムの一部でHaskellを使用しようとしています。Haskell型システムは本当に私に大きな助けを提供してくれます。たとえば、あるタイプの関数が必要だと気付いたとき

f :: (Foldable t, Monad m) => ( a-> b -> m b) -> b -> t a -> m b

そこのような機能は、実際にあるfoldMfoldlMfoldrM

ただし、本当にショックを受けたのは、次のようなこれらの関数の定義です。

foldlM :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b
foldlM f z0 xs = foldr f' return xs z0
  where f' x k z = f z x >>= k

したがって、関数f'は次のタイプでなければなりません:

f' :: a -> b -> b

必要に応じfoldr、その後、b種類のものでなければならない*-> m *ので、全体の定義は、foldlM意味を作ることができます。

別の例には、liftA2および<*>

(<*>) :: f (a -> b) -> f a -> f b
(<*>) = liftA2 id

liftA2 :: (a -> b -> c) -> f a -> f b -> f c
liftA2 f x = (<*>) (fmap f x)

ソースコードをのぞく前に、自分自身のソリューションのいくつかを試しました。しかし、そのギャップは非常に大きいため、今後何行のコードを記述しても、このソリューションを思いつくことはできません。

だから私の質問は、誰かがそのような高度に抽象化されたレベルで推論するために、どのような知識または特定の数学の分岐が必要であるということです。

カテゴリー理論が役立つかもしれないことは知っています。私はこの素晴らしい講義を長い間続けてきて、まだ取り組んでいます。


13
Haskellは言語です。多くの単語があり、それらの単語のほとんどはさまざまな方法で使用できます。あなたが新しい言語を学んでいるとき、何年もの間、多くの文章や慣用句は意味がありません。しかし、それを使用すればするほど、おなじみのパターンが見られるようになり、かつては脅迫的で高度なものだと思っていたことが自然に生まれてきます。リラックス。
ルキ

回答:


3

一般的に、ロジックなど、想像します。しかし、それを行うことによってそれを学ぶこともできます。:)時間が経つにつれて、いくつかのパターンに気づき、いくつかのトリックを取り上げます。

foldr余計なことを言ってこのように。一部の人はそれを関数に折りたたむと見なして、それらを.and で結合できるようにしますid(これは本当にandの場合もあります)、<=<return

foldr g z xs  =  foldr ($) z . map g $ xs
              =  foldr (.) id (map g xs) z
         ~/=  foldr (<=<) return (map g xs) z
{-
  (\f -> f . f) :: (a -> a) -> (a -> a)

  (\f -> f <=< f) :: Monad m => (a -> m a) -> (a -> m a)
                            (still just a type, not a kind)
-}

一部の人は、次のように、よりシンプルで構文的な用語でそれを理解する方が簡単だと感じています

foldr g z [a,b,c,...,n] s =
     g a (foldr g z [b,c,...,n]) s

そのためg、2番目の引数がstrictでない場合はs、1つの例として、右側を折りたたんでいても、左側から渡される状態として機能できます。


1
どうもありがとうございました。私はこの定義がユニークであるかどうかを解明しようとしていましたが、ここでのクライスリ合成の使用を期待していませんでした。この答えは本当に私の疑問を解決します。
Theodora

どういたしまして。:)
Will Ness、

4

したがって、それを理解する最良の方法は、それを行うことです。以下にの代わりにをfoldlM使用する実装がfoldlありfoldrます。それは良い練習問題です。試してみて、後で私が提案する解決策に進んでください。この例は、私がそれを達成するために行ったすべての推論を説明しています。これは、あなたの推論とは異なる場合があり、関数アキュムレータの使用についてすでに知っているため、バイアスになる可能性があります。

ステップ1:のfoldlM観点から書いてみましょうfoldl

-- this doesn't compile because f returning type is (m b) and not just (b) 
foldlM :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b
foldlM f z0 xs = foldl f z0 xs 

-- So let substitute f by some undefined f'
foldlM :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b
foldlM f z0 xs = foldl f' z0 xs
  where f' = undefined

-- cool, but f' should use f somehow in order to get the monadic behaviour
foldlM :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b
foldlM f z0 xs = foldl f' z0 xs
  where f' b a = f somethingIDontkNow 

ここでf'は、それが純粋でありf、型の一致の結果を抽出する必要があることに気付きます。モナディック値を「抽出」する唯一の方法は演算子を使用することですが>>=、そのような演算子は使用した直後にラップする必要があります。

結論として:あなたが私と一緒終わるたびに、このモナドを完全にアンラップしたいときは、あきらめてください。正しい方法ではありません

ステップ2:パターンマッチングが簡単なので(つまり、実際にを使用する必要がないため)、まず折りたたみ可能として使用して書いfoldlMてみましょう。foldl[]fold

-- This is not very hard. It is pretty standard recursion schema. :)
foldlM' :: (Monad m) => (b -> a -> m b) -> b -> [a] -> m b
foldlM' f z0 []     = return z0
foldlM' f z0 (x:xs) = f z0 x >>= \c -> foldlM' f c xs

それは簡単でした。foldl定義をリストの通常の定義と比較してみましょう

foldlM' :: (Monad m) => (b -> a -> m b) -> b -> [a] -> m b
foldlM' f z0 []     = return z0
foldlM' f z0 (x:xs) = f z0 x >>= \c -> foldlM' f c xs

myfoldl :: (b -> a -> b) -> b -> [a] -> b
myfoldl f z0 []     = z0
myfoldl f z0 (x:xs) = foldl f (f z0 x) xs

涼しい!!それらはほとんど同じです。取るに足らないケースは、まったく同じものです。再帰的なケースは少し異なります。次のようなものを書きたいと思いますfoldlM' f (f z0 x) xs。しかし、ステップ1のようにコンパイルされないので、大丈夫f>>= だと思うかもしれませんが、私はを適用したくありませんそのような計算を保持して、でそれを作成するだけです。foldlM' f (f z0 x >>=) xs理にかなっているようなものを書きたい ...

ステップ3累積したいのは関数の合成であり、結果ではないことを認識してください。(ここでは、あなたが投稿したので私がすでに知っているという事実におそらく偏っています)。

foldlM :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b
foldlM f z0 xs = foldl f' initFunc xs
  where initFunc = undefined :: b -> m b
        f'       = undefined :: (b -> m b) -> a -> (b -> m b) -- This type signature can be deduce because f' should be applied to initFunc and a's from t a. 

initFuncステップ2(再帰的な定義)からの知識のタイプと使用によって、それを推測できinitFunc = returnます。およびを使用する必要があることをf'知ってf'いるfと、の定義を完了することができます>>=

foldlM :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b
foldlM f z0 xs = foldl f' return xs z0
--                        ^^^^^^
--                        |- Initial value
  where f' b a = \bvalue -> b bvalue >>= \bresult -> f bresult a -- this is equivalent to (b >=> \result -> f result a) which captures the sequence behaviour of the implementation
--         ^      ^^^^^^                  ^^^^^^^
--         |      |                       |- This is the result of previous computation
--         |      |- f' should return a function b -> m b. Any time you have to return a function, start writing a lambda  
--         |- This b is the accumulated value and has type b -> m b
-- Following the types you can write this with enough practise

ご覧のとおり、それを行うことはそれほど難しくありません。それは練習が必要ですが、私はプロのHaskell開発者ではなく、自分でそれを行うことができました。それは練習の問題です


1
左の折り目が右の折り目よりもわかりやすいのは、ここではわかりません。正しい折り方は、無限の構造に対して有用な結果を生成し、典型的なMonadインスタンスに対して効率的である可能性がはるかに高くなります。
dfeuer

@dfeuerこれのポイントは、簡単な例を示すことではなく、OPに適切な演習を提案し、解決策の合理的な議論を明らかにすることです。そのような解決策。効率性についての余談は考慮されていません
lsmor

3

のような関数を書くのに、数学に関する特別な知識は必要ありませんfoldM。Haskellを本番環境で4年間使用していますが、このの定義を理解するのにも苦労していますfoldM。しかし、それは主にそれが不十分に書かれているためです。あいまいなコードを理解できない場合は、それを個人的な責任としてとらないでください。これはより読みやすいバージョンですfoldlM

foldlM
    :: forall t m a b .
       (Foldable t, Monad m)
    => (b -> a -> m b)  -- ^ Monadic action
    -> b                -- ^ Starting accumulator
    -> t a              -- ^ List of values
    -> m b              -- ^ Computation result inside a monad
foldlM f z xs = (foldr step pure xs) z
  where
    step :: a -> (b -> m b) -> b -> m b
    step cur next acc = do
        result <- f acc cur
        next result

この機能はまだ最も簡単なものではありません。主foldrに、中間アキュムレータが関数である場合の非標準的な使用法があるためです。しかし、そのような定義を読みやすくするいくつかの方法を見ることができます:

  1. 関数の引数に関するコメント。
  2. より良い引数名(まだ短くて慣用的ですが、少なくとももっと読みやすいです)。
  3. 内部の関数の明示的な型シグネチャwhere(引数の形がわかるように)。

このような関数を見た後、方程式推論技法を実行して、定義を段階的に展開し、それがどのように機能するかを確認できます。そのような機能を思い付く能力には、経験が伴います。私には数学者のスキルがありません。この関数は典型的なHaskell関数ではありません。しかし、あなたがより多くの練習をするほど、それはより良くなります:)

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.