Scalaでの関数型プログラミング を実行しているときに、次の質問に遭遇しました。
FoldRightの観点からfoldLeftを正しくすることはできますか?逆はどうですか?
著者によって提供されたソリューションでは、次のような実装を提供しています。
def foldRightViaFoldLeft_1[A,B](l: List[A], z: B)(f: (A,B) => B): B =
foldLeft(l, (b:B) => b)((g,a) => b => g(f(a,b)))(z)
def foldLeftViaFoldRight[A,B](l: List[A], z: B)(f: (B,A) => B): B =
foldRight(l, (b:B) => b)((a,g) => b => g(f(b,a)))(z)
誰かが私がこのソリューションをたどって、これが実際にどのようにfoldlをfoldrの観点から実装するのか、またその逆を理解するのを手伝ってくれるでしょうか?
ありがとう
見てみましょう
_def foldLeftViaFoldRight[A,B](l: List[A], z: B)(f: (B,A) => B): B =
foldRight(l, (b:B) => b)((a,g) => b => g(f(b,a)))(z)
_
(他の折り目も同様です)。秘訣は、右折り操作中に、型B
の最終値を作成しないことです。代わりに、B
からB
までの関数を作成します。フォールドステップは、タイプ_a: A
_の値と関数_g: B => B
_を取り、新しい関数_(b => g(f(b,a))): B => B
_を生成します。この関数は、g
とf(_, a)
の合成として表すことができます。
_ l.foldRight(identity _)((a,g) => g compose (b => f(b,a)))(z);
_
プロセスは次のように見ることができます。a
の各要素l
について、関数_B => B
_である部分適用b => f(b,a)
を使用します。次に、 compose これらすべての関数を、右端の要素(トラバーサルを開始する要素)に対応する関数が構成チェーンの左端にくるようにします。最後に、z
に大きな合成関数を適用します。これにより、左端の要素(コンポジションチェーンの右端)から始まり、右端の要素で終わる一連の操作が行われます。
更新:例として、この定義が2要素リストでどのように機能するかを調べてみましょう。まず、関数を次のように書き直します。
_def foldLeftViaFoldRight[A,B](l: List[A], z: B)
(f: (B,A) => B): B =
{
def h(a: A, g: B => B): (B => B) =
g compose ((x: B) => f(x,a));
l.foldRight(identity[B] _)(h _)(z);
}
_
それを渡したときに何が起こるかを計算してみましょうList(1,2)
:
_List(1,2).foldRight(identity[B] _)(h _)
= // by the definition of the right fold
h(1, h(2, identity([B])))
= // expand the inner `h`
h(1, identity[B] compose ((x: B) => f(x, 2)))
=
h(1, ((x: B) => f(x, 2)))
= // expand the other `h`
((x: B) => f(x, 2)) compose ((x: B) => f(x, 1))
= // by the definition of function composition
(y: B) => f(f(y, 1), 2)
_
この関数をz
に適用すると
_f(f(z, 1), 2)
_
要求に応じ。
私はこの演習を行ったばかりで、誰かに役立つことを願って、どのようにして答えにたどり着いたかを共有したいと思います(基本的に質問の内容と同じですが、文字だけが異なります)。
背景として、foldLeft
とfoldRight
が行うことから始めましょう。たとえば、リスト[1、2、3]のfoldLeftの結果は、演算_*
_で開始値z
は値_((z * 1) * 2) * 3
_です
FoldLeftは、リストの値を左から右に段階的に消費するものと考えることができます。つまり、最初は値z
(リストが空の場合の結果)から開始し、次にfoldLeft
に、リストが1で始まり、値が_z * 1
_になることを明らかにし、foldLeft
が表示します。次のリストには_2
_があり、値は_(z * 1) * 2
_になり、最後に3に作用した後、値_((z * 1) * 2) * 3
_になります。
_ 1 2 3
Initially: z
After consuming 1: (z * 1)
After consuming 2: ((z * 1) * 2
After consuming 3: (((z * 1) * 2) * 3
_
この最終値は、(演習で求められているように)代わりにfoldRight
を使用することを除いて、達成したい値です。ここで、foldLeft
がリストの値を左から右に消費するのと同じように、foldRight
がリストの値を右から左に消費することに注意してください。したがって、リスト[1、2、3]では、
(((z * 1) * 2) * 3
_にします。言い換えると、foldRight
を使用して、最初にリストが空の場合の結果、次にリストに[3]のみが含まれている場合の結果、次にリストが[2、3]の場合の結果、最後にリストの結果は[1、2、3]です。
つまり、これらはfoldRight
を使用して到達したい値です。
_ 1 2 3
Initially: z
After consuming 3: z * 3
After consuming 2: (z * 2) * 3
After consuming 1: ((z * 1) * 2) * 3
_
したがって、z
から_(z * 3)
_、_(z * 2) * 3
_、_((z * 1) * 2) * 3
_に移動する必要があります。
valuesであるため、これを行うことはできません。任意の操作_(z * 3)
_に対して、値_(z * 2) * 3
_から値_*
_に移動する自然な方法はありません。 (可換で結合法則であるため乗算がありますが、任意の演算を表すために_*
_のみを使用しています。)
しかし、functionsとして、これを実行できる可能性があります。 「プレースホルダー」または「穴」を持つ関数が必要です。これは、z
を取り、適切な場所に配置するものです。
z => (z * 3)
があります。むしろ、関数は任意の値を取る必要があり、特定の値にz
を使用しているので、これをt => (t * 3)
と記述しましょう。 (入力z
に適用されるこの関数は、値_(z * 3)
_を提供します。)t => (t * 2) * 3
がありますか?最初のプレースホルダー関数から次の関数に移動できますか?しましょう
_ f1(t) = t * 3
and f2(t) = (t * 2) * 3
_
_f2
_に関して_f1
_とは何ですか?
_f2(t) = f1(t * 2)
_
はい、できます!したがって、必要な関数は_2
_と_f1
_を取り、_f2
_を与えます。これをg
と呼びましょう。 g(2, f1) = f2
があります。ここでf2(t) = f1(t * 2)
、つまり
_g(2, f1) =
t => f1(t * 2)
_
これを進めた場合にこれが機能するかどうかを見てみましょう。次のステップはg(1, f2) = (t => f2(t * 1))
で、RHSはt => f1((t * 1) * 2))
またはt => (((t * 1) * 2) * 3)
と同じです。
うまくいくようです!そして最後に、この結果にz
を適用します。
最初のステップはどうあるべきですか? _3
_と_f0
_にg
を適用して_f1
_を取得します。ここで、f1(t) = t * 3
は上記で定義したとおりですが、g
の定義からもf1(t) = f0(t * 3)
を取得します。したがって、恒等関数として_f0
_が必要なようです。
新たに始めましょう。
_Our foldLeft(List(1, 2, 3), z)(*) is ((z * 1) * 2) * 3
Types here: List(1, 2, 3) is type List[A]
z is of type B
* is of type (B, A) -> B
Result is of type B
We want to express that in terms of foldRight
As above:
f0 = identity. f0(t) = t.
f1 = g(3, f0). So f1(t) = f0(t * 3) = t * 3
f2 = g(2, f1). So f2(t) = f1(t * 2) = (t * 2) * 3
f3 = g(1, f2). So f3(t) = f2(t * 1) = ((t * 1) * 2) * 3
_
そして最後に、zにf3を適用して、必要な式を取得します。すべてがうまくいきます。そう
_f3 = g(1, g(2, g(3, f0)))
_
これは、f3 = foldRight(xs, f0)(g)
を意味します
今度は_x * y
_の代わりに任意の関数s(x, y)
を使用してg
を定義しましょう:
g
への最初の引数はタイプA
ですg
の2番目の引数は、これらのf
のタイプであり、_B => B
_です。g
のタイプは_(A, (B=>B)) => (B=>B)
_です。したがって、g
は次のとおりです。
_def g(a: A, f: B=>B): B=>B =
(t: B) => f(s(t, a))
_
これらすべてをまとめる
_def foldLeft[A, B](xs: List[A], z: B)(s: (B, A) => B): B = {
val f0 = (b: B) => b
def g(a: A, f: B=>B): B=>B =
t => f(s(t, a))
foldRight(xs, f0)(g)(z)
}
_
この本を読み進めるレベルでは、より明確で理解しやすいので、実際にはこの形式を好みます。しかし、ソリューションの形式に近づくために、_f0
_とg
の定義をインライン化できます(g
への入力であり、コンパイラーが推測するため、foldRight
の型を宣言する必要はありません)。
_def foldLeft[A, B](xs: List[A], z: B)(s: (B, A) => B): B =
foldRight(xs, (b: B) => b)((a, f) => t => f(s(t, a)))(z)
_
シンボルが異なるだけで、まさに問題になっているものです。同様に、foldLeftに関してfoldRightについても同様です。
そのコードは、リスト内の要素ごとに1つの関数を使用して、複数の関数オブジェクトをチェーンします。これをより明確に示す例を次に示します。
val f = (a: Int, b: Int) => a+b
val list = List(2,3,4)
println(list.foldLeft(1)(f))
val f1 = (b: Int) => f(b, 2)
val f2 = (b: Int) => f(b, 3)
val f3 = (b: Int) => f(b, 4)
val f4 = (b: Int) => b
val ftotal = f1 andThen f2 andThen f3 andThen f4
println(ftotal(1))
あなたはそれを関数オブジェクトのリンクリストとして想像することができます。値を渡すと、すべての関数を「フロー」します。これは、データフロープログラミングに少し似ています。
この本の著者は、 github/fpinscala ページで適切な説明を提供しています。