(FP)すごいHaskell!の5.4でflip'が本当に読めなかった話

「すごいHaskellたのしく学ぼう!」の5章の「5.4: ラムダ式」において,標準関数flipの再実装であるflip'がまったくわからなかったのが,2時間かかってやっと理解できたので記録する.

まず前提として,第5章は以下のようにステップしてきた.

  • 5.1: カリー化関数
    • 部分適用
    • カリー化
  • 5.2: 高階実演
    • 高階関数
      • 関数は,関数の引数として渡せる
      • 関数は,関数の返り値にできる
  • 5.3: 関数プログラマの道具箱

そして,5.4はラムダ式がテーマである.

ラムダ式とは?

書籍より抜粋(5.4)すると,

ラムダ式とは,1回だけ必要な関数を作るときに使う無名関数です.

通常,ラムダ式高階関数に渡す関数を作るためだけに使われます.ラムダ式を宣言するには,バックスラッシュ(\)を書いて,それから関数の引数をスペース区切りで書きます.続けて->,最後に関数本体を書きます.・・・(略)・・・普通,ラムダ式は括弧で囲みます.

> (\x y -> x + y) 2 3 5

(>はghciで実行している意味)

無名関数はJavaScriptで多少慣れていたのですぐわかった.

(function(x, y) { return x + y; } )(2, 3); 5

flip関数とは?

flip関数の定義を書籍から抜粋(5.2.2)すると,

flip関数は,関数を引数に取り,元の関数と似ているけど最初の2つの引数が入れ替わった関数を返します.

例えば,文字列と文字列を繋ぎ合わせる関数の例で,flipを使うと順序を入れ替えられる.

> flip (++) "A" "B" "BA"

一回のみ使うとしたら大したことないが,リストなどの集合を扱う時にはやっぱりこんな関数を使い回すことだろう.

さて,flip関数は5.2.2で一度実装している.ラムダ式ではない版だ.

flip' :: (a -> b -> c) -> (b -> a -> c) flip' f x y = f y x

二番目の丸括弧は,可読性のためのもので必須ではない.
一番目の丸括弧は,第一引数として関数を引数として受け取る,という意味であり仕様だから必須だ.

5.4でflip関数を再定義 -> まったく読めない

5.4ではflip関数をラムダ式の練習として再定義している.

flip' :: (a -> b -> c) -> b -> a -> c flip' f = \x y -> f y x

これがまったく読めなかった.

理解した今となっては「とても頭が固かった」としか言いようがない.
理解できなかったのは,関数の型宣言(1行目)を必死になって読もうとしたことと,カリー化と部分適用の話と,ラムダの話のトリプルで混戦していたのだ;

型宣言から必死に読もうとしていた思考の様子は,

flip'関数は,

  1. 第一引数は,関数であり,
    • その関数は,
      • 第一引数が,a型で,
      • 第二引数が,b型で,
      • 返り値としてc型を返す
  2. 第二引数は,b型の値で,
  3. 第三引数は,a型の値で,
  4. 返り値として,c型の値を返す

のだよね?あれ?と.

一度こう考えてしまうと,関数の実装(2行目)が読めなくなる.

理解1: 括弧をつける

これを理解する簡単な道は,「必須ではない括弧」を付けてみることだ.
さっきのコードの1行目と2行目で対応する括弧を付けると,

flip' :: (a -> b -> c) -> (b -> a -> c) flip' f = (\x y -> f y x)

おもしろいもので,このように括弧をつけると別の読み方になる.

flip'関数は,

  1. 第一引数は,関数であり,(関数f)
    • その関数は,
      • 第一引数が,a型で,
      • 第二引数が,b型で,
      • 返り値としてc型を返す
  2. 返り値として,関数を返す
    • その関数は,
      • 第一引数を x ,
      • 第二引数を y として受け取り,
      • f関数を実行する(渡す引数は以下のようにxとyとを入れ替える)
        • 第一引数を y
        • 第二引数を x

じゃあ,「括弧は積極的に書こうよ!」というと,そうとも言い切れないと思う.部分適用,カリー化の妨げになると思うのだ.

(ただ,「ラムダ式とは」のセクションで抜粋した中に書いてあったように,ラムダ式については括弧で囲むべきだと感じた)

理解2: 部分適用,カリー化を考える

必須ではない括弧を付けて,

flip' :: (a -> b -> c) -> (b -> a -> c)

のように宣言文を書いてしまうと,部分適用,カリー化しないような意味合いにとれてしまう.

Haskellでは,部分適用,及び,カリー化は重要なはずなので,括弧は付けない方が良いと思うコード例が以下.

-- 標準のflip関数を使う(特に深い意味はない) > :t flip flip :: (a -> b -> c) -> b -> a -> c > let a = "A" > let b = "B" -- カリー化(部分適用)して,concatFlipAand変数に格納する(これは関数である) > let concatFlipAand = flip (++) a > concatFlipAand b "BA" -- concatFlipAandを再利用してみる > let c = "C" > concatFlipAand c "CA"

まとめ

「5.4: ラムダ式」は4ページと少ない(し,filp'については実質1ページだ!).
理解するのに2時間と,さらにこのエントリを書いた時間を合わせて3時間超かかってしまったが,非常に内容の濃いセクションだった.