『Scala 関数型デザイン&プログラミング』の『Exercise 3.13』のふりかえり

第5回 関数型プログラミング勉強会で
Scala 関数型デザイン&プログラミング』の『Exercise 3.13』を解いて実例を試した。

この時、なんとなく解いてしまったので、以下のリンクのような疑問が残った。
http://se-info-tech.hatenablog.com/entry/2015/10/04/124454

もう一度、やってみる。
以下、雑文。

定義は下記となる。

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)

実行例として次を考える。
・z = 0
・l = [1,2,3]
また、関数id = (b => b)を、下で使用する。

foldRightViaFoldLeft_1([1,2,3], 0)(+)
=> foldLeft([1,2,3], id)((g,a) => b => g(f(a,b))) (0)

ここで、foldLeft([1,2,3], id)((g,a) => b => g(f(a,b))))を簡約していく

foldLeftの初項目のアキュムレータ算出結果
  = ( (g,a) => b => g(f(a,b))) )(g = id,  a = 1)
  = ( b => id(1 + b) )
  = ( b => 1 + b )
  = <1+>
ただし、<1+> := x => 1 + x とする略記

foldLeftの第二項目のアキュムレータ算出結果
  = ( (g,a) => b => g(f(a,b))) )(g = <1+>,  a = 2)
  = ( b => <1+>(2 + b) )
  = <1+><2+>
ただし、<2+> := x => 2 + x とする略記
正確に記述すると
  <1+><2+> := (x => (y => 1 + y)(2 + x))
  (x => (y => 1 + y)(2 + x)) = (y => 1 + y) (x => 2 + x)
  {∵ (y => 1 + y)上では、xは自由変数。
      言い換えると、(y => 1 + y)は、xが何になろうと定義(動作仕様)が変わることは無い。}
となる。

foldLeftの第三項目のアキュムレータ算出結果
  = ( (g,a) => b => g(f(a,b))) )(g = <1+><2+>,  a = 3)
  = ( b => <1+><2+>(3 + b) )
  = <1+><2+><3+>
ただし、<3+> := x => 3 + x とする略記
正確に記述すると
  <1+><2+><3+> := (x => (w => w + 1)(y => 2 + y)(3 + x))
自由変数だから
  (x => (y => (w => w + 1)(2 + y))(3 + x)) = (w => w + 1)(y => 2 + y)(x => 3 + x)
となる。

(エレガントさの為に、演算子<n+>を導入したのに自由変数だからの件を書いたのは
 本末転倒のような気もするが、記号操作に慣れていない層が
 もしかしたらメジャー層とも、思えるので、迎合してみた。でもクドイですね。)

以上より、計算すると

foldLeft([1,2,3], id)((g,a) => b => g(f(a,b))))(0)
 = <1+><2+><3+>(0)
 = <1+><2+>(3)
 = <1+>(5)
 = 6

略記せずに記述すると

foldLeft([1,2,3], id)((g,a) => b => g(f(a,b))))(0)
 = (w => w + 1)(y => 2 + y)(x => 3 + x)(0)
 = (w => w + 1)(y => 2 + y)(0 => 3 + x)
 = (w => w + 1)(y => 2 + y)(3 + 0)
 = (w => w + 1)(y => 2 + y)(3)
 = (w => w + 1)(3 => 2 + y)
 = (w => w + 1)(2 + 3)
 = (w => w + 1)(5)
 = (5 => w + 1)
 = (5 + 1)
 = 6

となる。

どうせ、同値なんだから
とっちでも良い気がするけど
階層構造で頑なに記述してみると

foldLeft([1,2,3], id)((g,a) => b => g(f(a,b))))(0)
 = (x => (y => (w => w + 1)(2 + y))(3 + x))(0)
 = (0 => (y => (w => w + 1)(2 + y))(3 + x))
 = (y => (w => w + 1)(2 + y))(3 + 0)
 = (y => (w => w + 1)(2 + y))(3)
 = (3 => (w => w + 1)(2 + y))
 = (w => w + 1)(2 + 3)
 = (w => w + 1)(5)
 = (5 => w + 1)
 = (5 + 1)
 = 6

となる。

『SE情報技術研究会 管理者』さん、今度はどうでしょうか ?

下のページの方も、ここの所を書いていますね。
http://myutaro.blogspot.jp/2015/05/p51.html
http://nijojin.hatenadiary.jp/entry/2015/06/13/163814


作者の回答の訳

  /*
  The implementation of `foldRight` in terms of `reverse` and `foldLeft` is a common trick for avoiding stack overflows when implementing a strict `foldRight` function as we've done in this chapter. 
> reverse と foldLeft の観点での foldRightの実装は、この章で行っている様な、正格評価の
> foldRight関数実装時に起こる、stack overflowを避ける時に、良く使う方法である。
(We'll revisit this in a later chapter, when we discuss laziness).
> (後の章の遅延評価の議論で再度取り上げる)
  The other implementations build up a chain of functions which, when called, results in the operations being performed with the correct associativity.
> その他の実装は、関数の連鎖で構築しています。それが呼び出されると、正しい結合規則によって
> 実行される操作を、結果として生じさせます。
We are calling `foldRight` with the `B` type being instantiated to `B => B`, then calling
the built up function with the `z` argument.
> foldRight を呼び、B型(の引数)を、B => B にインスタンス化する。
> そして、z引数で、構築した関数の呼び出している
Try expanding the definitions by substituting equals for equals using a simple example, like `foldLeft(List(1,2,3), 0)(_ + _)` if this isn't clear.
> もし、よくわからないなら、foldLeft(List(1,2,3), 0)(_ + _)の様な簡単な例を用いて
> 同値から同値へ書き換えることによって、定義を展開してみてください
Note these implementations are more of theoretical interest - they aren't stack-safe and won't work for large lists.
> これらの実装は、それらはスタックセーフではなく、大きいリストでは働かないという、
> 理論的な関心のみということではないことに注意してください
  */
  def foldRightViaFoldLeft[A,B](l: List[A], z: B)(f: (A,B) => B): B =
    foldLeft(reverse(l), z)((b,a) => f(a,b))

  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)

関数プログラミング(Rバード・Pワドラー)を見るとfoldl、foldrの定義は下記の通り
(なお、引数の順序はscalaとは違し、本とも記述形式を変えて、中高数学っぽくした。
また、<+>は、丸に+の記号で書きたかったが機種依存文字のようなので、代用として使用した。)

右側畳み込み
 foldr(f, a, [x1,x2,...,xn]) = f(x1,f(x2, (... f(xn, a) ...)))

左側畳み込み
 foldl(f, a, [x1,x2,...,xn]) =  f((...f(f(x1, a),x2)...),xn)

読みやすい形式で書くと(f ≡ <+> で、前置記法から中置記法に変更)

 foldr(<+>, a, [x1,x2,...,xn]) = x1 <+> (x2 <+> (x3 <+> (... (xn <+> a) ...)))

 foldl(<+>, a, [x1,x2,...,xn]) = (... ((a <+> x1) <+> x2) ...) <+> xn

となる。
         foldr(<+>, a, []) = a
       foldr(<+>, a, [x1]) = x1 <+> a
    foldr(<+>, a, [x1,x2]) = x1 <+> (x2 <+> a)
 foldr(<+>, a, [x1,x2,x3]) = x1 <+> (x2 <+> (x3 <+> a))

括弧を常に右側にまとめていることから、「右側に畳込む(fold right)」という意味で
この演算をfoldrと呼ぶ。

         foldl(<+>, a, []) = a
       foldl(<+>, a, [x1]) = a <+> x1
    foldr(<+>, a, [x1,x2]) = (a <+> x1) <+> x2
 foldr(<+>, a, [x1,x2,x3]) = ((a <+> x1) <+> x2) <+> x3

同様に、「左側に畳込む(fold left)」という意味で
この演算をfoldlと呼ぶ。


後付の理屈でイメージをまとめて書く

(g,a) => b => g(f(a,b))を使用し、特に、g(f(a,b))でfの後にgを実行することによって

たとえば、上記例では、

『「(a <+> x1) <+> x2」の「(a <+> x1)」の演算<+>を「(...) <+> x2」の演算<+>の後に実行する』

となるように、実行順序を組み替えている。

一般化すると

『「(... <+> x[k-1]) <+> xk」の「(... <+> x[k-1])」の演算<+>を「(...) <+> x[k-1]」の
   演算<+>の後、に実行する』

となるように、実行順序を組み替えている。

なぜなら、gは、現在処理中のlistのheadである、aの左側にあった、リスト要素の演算を行う様に
構成された関数だからである。

変数名の省略を嫌うオブジェクト志向界隈風にかけば、leftsideOparationsという名前で

  def foldRightViaFoldLeft_1[A,B](l: List[A], z: B)(f: (A,B) => B): B =
    foldLeft(l, (b:B) => b)((leftsideOparations,a) => b => leftsideOparations(f(a,b)))(z)

とするかもしれない。
(ただし、leftsideOparationsの意味は、関数チェーン作成中で、リストl:[a,b,c,d,e,f] の要素cを処理している時に、aとbをcの左側にある要素として、leftsideとし、その操作だからOparationsとした。)

『b => g(f(a,b))』となるのは、その型が B => B つまり、初期値の(b:B) => bと
同じ型である必要があるから理解できる。

『(g,a) => 』は、
(1)このgは、演算の途中結果を蓄積するアキュームレータであり、初期値の(b:B) => bを
   受取る関数型。
(2)aはふつうのfoldと同じで、Listの要素(head)を受取るものだから
  こうなるのは、ふつうに当たり前ですね。

そう考えると、演算の順序を揃えることと、型を合わせることで
『(g,a) => b => g(f(a,b))』に決まってくる様な気がします。

次に

上でやった
  <1+><2+><3+> := (x => (w => w + 1)(y => 2 + y)(3 + x))
  (x => (w => w + 1)(y => 2 + y)(3 + x)) = (w => w + 1)(y => 2 + y)(x => 3 + x)
のように、階層構造で包まずに
 (g,a) => b => g(f(a,b))
を、haskellのcompose演算子を使用して
 (g,a) => g . (b =>f(a,b))
と書く。あるいはscalaの関数を使用して
 (g,a) => g compose (b =>f(a,b))
 (g,a) => (b =>f(a,b)) andThen g
と書けそう。

  def foldRightViaFoldLeft_2[A,B](l: List[A], z: B)(f: (A,B) => B): B =
    foldLeft(l, (b:B) => b)((g,a) => g compose (b =>f(a,b)))(z)   // --- (*)

あるいは、

  def foldRightViaFoldLeft_2[A,B](l: List[A], z: B)(f: (A,B) => B): B =
    foldLeft(l, (b:B) => b)((g,a) => ((b:B) =>f(a,b)) andThen g)(z)    // --- (**)

(*)の様に書けば、leftsideOparationsなどと書かなくても直感的になにをやっているのか
分りやすいのではないか。
冗長に書くと、小学生でもわかる風な、書き方になるかな ?

  def foldRightViaFoldLeft_2[A,B](l: List[A], z: B)(f: (A,B) => B): B =
    foldLeft(l, (b:B) => b)((leftsideOparations,a) => 
                              ((b:B) =>f(a,b)) andThen leftsideOparations)(z)

なお、(*)と(**)は、手元にscala実行環境が無いので確認していない。
後で確認して、この記述を残すか、削除するか決める。
(=>確認して動いた。ただ、bに型指定を追加する必要があった。)

この関数の連鎖で構築する方針の答えは、5章の遅延評価の議論への伏線で上げているのかな ?
reverseでの実装と、操作の回数は同じようだが、優劣はどうか ?

対比
  reverseの操作 <=> 関数チェーンの構築
  foldの実行    <=> 関数チェーンの簡約操作

個々の処理
  reverseの操作          : 結果として操作の対象列ができる
  関数チェーンの構築     : 操作の連鎖としての関数列ができる
  foldの実行             : 対象列の要素を順次とりながら計算を実行
  関数チェーンの簡約操作 : 最後の1引数関数から引数を与えて、得た返却値を
                           次の1引数関数に引数として与えて行く連鎖を繰返し
                           最終結果を得る。

『foldの実行』より『関数チェーンの簡約操作』の方がコストは低いかもしれない。
(実験してみれば良いかもしれない。)
ただ、普段の実装で、その微々たるコストを気にするレベルの人ならば
関数型言語は使用せずに、命令型言語のC言語などのコンパイル言語を
使用するべきかもしれない。
そのレベルの人ならば、その方が精神衛生上、楽なのでは ?

関数型が有利と言われている、並列処理もコンパイラやフレームワークの最適化に委ねないで
自ら考えた最適解をハードコーディングした方が、頭の良い人ならば
より良い解が求まるような気がする。

(でも、昔からある格言の、最適化は最後までやるな! とか 最適化は絶対するな!(大体は
 物事が見えていない分かっていない人の要らない思い込みで、別の方法が在るはずだから)
 とかありますが !"#$%&'?)

自分は頭が悪いし、このレベルの問題に時間を掛けて他のことができなくなるのは
嫌なので、気にせずに「common trick」と言っていたreverseを使う方針を採用するのが
現時点では、良いと思う。
別にみんなに使われる共通基盤を作る訳ではないし
仮にそうでもアルファバージョン位では、カリカリに最適化しなければいけない
理由もない様な ?

皆様は、どう考えるでしょうか ?