読者です 読者をやめる 読者になる 読者になる

i remember nothing

文章の練習も兼ねています

Haskellの let と where

はじめに

HaskellOCaml などの言語を使っていると、let やwhereで関数内に関数や値を宣言することがあります。(OCamlにはwhereないですが) どちらも似たような役割を持っているので、どう違うねんとかどっち使えばいいねんみたいな疑問を持つ人もいるでしょう。 まずは let と where の例を見てみましょう。

Haskell のシンプルな例

  • let
hoge n = 
    let
        f x = x * x
    in
        f n
  • where
hoge n = f n
  where
    f x = x* x

OCaml のシンプルな例

標準の OCaml にはwhere syntax はありません

let hoge n = let f x = x * x in f n

Haskell の let と where、それぞれの利点

let のメリット

たとえば State Monad を用いて、こんな関数を書くとしましょう。 State Monad は s -> (a, s) のように引数と何らかの値ペアを(状態として)返すモナドです。

State Monad - HaskellWiki

f :: State s a
f = State $ \x -> y
   where y = ... x ...

この関数はコンパイルできません。なぜならば、ここで λx.y の x はwhereのスコープに入らないからです。

f :: State s a
f = State $ \x -> where y = ... x ...

もちろん、このように書いてもだめですね。y が先になんらかの形で宣言されていません。

where 節は構文規則上パターンマッチのような性質を持っていて、(x -> y) の x の部分だけを取り出して操作するということはできないのです。

こんなとき、let を使うと簡明に書けます。

f :: State s a
f = State $ \x ->
   let y = ... x ...
   in  y

let は単なる変数のバインディングなので、もらってきた x を y の中に束縛し、そのyを返すことで操作することができます。

where のメリット

先ほどのletのメリットを読むと一見 where にアドバンテージが無いように見えますが、実はそうではありません。

シンプルに関数定義をしたい場合であれば where のほうが簡明に書けることが多いでしょう。

where 節はコンストラクティブな構文ですが、 let 節は単なる関数表現です。なので、暗黙的なパターンマッチなどの中で用いたい場合は where 節が強力です。

f x
  | cond1 x   = a
  | cond2 x   = g a
  | otherwise = f (h x a)
  where
    a = w x

これを let で書こうとすると

f x
  = let a = w x
    in case () of
        _ | cond1 x   -> a
          | cond2 x   -> g a
          | otherwise -> f (h x a)

のように、明示的にあまり意味のないcase文を書くはめになってしまいます。

lambda lifting 、または let floating

where を使うか let を使うか問題への一つのアプローチとして、lambda lifting というテクニックがあります。 where や let の代わりに新しい関数を定義し、自由変数として引数に取るという手法です。

たとえば、以下の様な where 節を用いた関数があるとします。

f x
  | cond1 x   = a
  | cond2 x   = g a
  | otherwise = f (h x a)
  where
    a = w x

これを新しい関数 f' を定義し、相互に再帰させる形式に変更します。

f x = f' (w x) x
 
f' a x
  | cond1 x   = a
  | cond2 x   = g a
  | otherwise = f (h x a)

これである意味問題を避けることができます。

where 節における問題

例えば、フィボナッチ数を返す関数

fib = (map fib' [0 ..] !!)
    where
      fib' 0 = 0
      fib' 1 = 1
      fib' n = fib (n - 1) + fib (n - 2)
fib x = map fib' [0 ..] !! x
    where
      fib' 0 = 0
      fib' 1 = 1
      fib' n = fib (n - 1) + fib (n - 2)

この2つを同時に実行した時、2番目のfibのほうが少し遅いのです。 η-簡約をしているかしていないかだけで、あとは同値な関数に見えるのに、実際のパフォーマンスに違いが出るのが不思議に見えます。

  • η-簡約

η-簡約は、M に x が出現しない時、 λx.M x → M と変換できる規則のことです

let を用いてこの関数を書きなおしてみるとその理由がわかります。

fib =
    let fib' 0 = 0
        fib' 1 = 1
        fib' n = fib (n - 1) + fib (n - 2)
    in  (map fib' [0 ..] !!)
fib x =
    let fib' 0 = 0
        fib' 1 = 1
        fib' n = fib (n - 1) + fib (n - 2)
    in  map fib' [0 ..] !! x

この下のコードでは、fib' がそれぞれの x に関して再定義されているのがわかります。

このように、where を用いた場合、関数適用の構造が隠されてしまうため、素朴なη-簡約に見えてしまう場合があります。

おまけ

Camlp5を用いるとwhere節を拡張したOCamlが(比較的簡単に)作れます。個人的には、OCamlは拡張してなんぼの言語だと思います。標準のsyntaxはやや貧弱です。

Tutorial: How to customize the syntax of OCaml, using Camlp5

参考文献

この記事は以下の記事を参照しています。

Let vs. Where - HaskellWiki