Haskell勉強会準備室5 (ラムダ式とレキシカルスコープ)

今回は高階関数をサポートするギミックであるラムダ式とwhere節を学んでいきます。
高階関数はλ式、where節とセットになることで真価を発揮します。

ラムダ式 -使い捨ての関数

ラムダ式は何か特別な面白い名前ですが、ただの関数です。使い捨ての関数というのが正解です。
無名関数(anonymous function)という訳語もあります。
さっきから高階関数で使うためにいろいろ関数を定義していました。
例えば、isNotTab、isNotSpecChar, divideAtTab, divideAtなどです。mapとかは使い勝手がすごく良い高階関数なので、
こういう小さい関数をたくさんつくることになります。これが問題を引き起こします。

あふれる名前

関数につける名前が足りなくなってしまいます。よくあるインターネットサービスでも、既に誰かにアカウント名が使われてしまっている、
というのは良くあります。それとおなじことです。lengthのような汎用性の高いよく使う関数ならわからんでもないですが、
使い捨ての関数にそれは勿体無いです。下のようなエラー文がいちいち出てきてしまいます。

multiple declaration of ...

長くなる関数名

名前がかぶるのを避けようとするとどうなるか、
恐らく名前が長くなっていきます。これはこれで憶えるのが大変ですし、
HIEのオートコンプリートの力を借りたとしても限度が出てきます。

ラムダ式による解決

ラムダ式を使うと名前をいちいち用意しなくても良くなります。

ss1 = ['今', '日', '\t', '名', '詞', ',', '副', '詞', '可', '能']
isNotTab :: Char -> Bool
isNotTab c = c /= '\t'
ss1Left = takeWhile isNotTab ss1

上をラムダ式で書き換えてみます。

ss1 = ['今', '日', '\t', '名', '詞', ',', '副', '詞', '可', '能']
ss1Left' = takeWhile (\c -> c /= '\t') ss1

(\c -> c /= '\t')の箇所がラムダ式です。これは型理論のそっち系の教科書であれば、
以下のように書かれます。
λc -> c /= '\t'
読み方というか解釈の仕方は、
以下cを変数とする関数である、どんな関数かというとcが'\t'かどうかを判定する。
辺りの感覚で読みます。
ピンとこない人は以下を考えてみてください。数学寄りの例です。

f x = x + 1
\x -> x + 1

上のfという名前をつけた関数と、下のラムダ式は同じものを表します。
別にxを使う必然性はないです。下の式はすべて同じ関数を表します。

f x = x + 1
f y = y + 1
\x -> x + 1
\pgr -> pgr + 1
\orz -> orz + 1
\hogehoge -> hogehoge + 1

\を使うのはλに似ているからです。日本語のキーボードだとこれが¥マークになってしまうので、
残念なところです。
そして、一文字だけで表現している辺りに、Haskellにとってλ式がいかに重要かがよく分かります。
よく使うということです。他言語だとラムダ式はlambdaと書いたりします。

where節(レキシカルスコープ)

さて、ラムダ式と並んでwhere節もよく使われます。
λ式が何故必要だったか、これは高階関数を使う際に使い捨ての関数が沢山出てきて、
それらすべてに名前をつけるのは経済的でないということでした。
使い捨てですよ、ということが表現できればいいです。
同様のことはwhere節(where clause)でも出来ます。

レキシカルスコープ

プログラミングでは変数が有効な領域をスコープと表現したりします。
where節はレキシカルスコープ(lexical scope)と呼ばれます。

静的と動的

レキシカルというのは語彙、言葉という意味です。whereを見ればそれがスコープを表すと分かる、
辺りの意です。静的スコープ(static scope)とも呼ばれます。
プログラミング言語の特徴として静的とか動的とか出てきます。
例えば、Haskellは静的型付(statically typed)言語として分類されます。
静的というのはコンパイル時に型付されるということです。対して動的型付言語もあり、
こちらは実行時(ランタイムと呼びます)に型付されるものになります。

レキシカルスコープによる解決

さて、戻りますとlambda式と同様のことはwhere節で達成できます。
上に出てきた例を書き換えてみます。

ss1 = ['今', '日', '\t', '名', '詞', ',', '副', '詞', '可', '能']
isNotTab :: char -> bool
isNotTab c = c /= '\t'
ss1Left = takeWhile isNotTab ss1
ss1 = ['今', '日', '\t', '名', '詞', ',', '副', '詞', '可', '能']
ss1Left' = takeWhile (\c -> c /= '\t') ss1
ss1 = ['今', '日', '\t', '名', '詞', ',', '副', '詞', '可', '能']
ss1left''' = takeWhile isNotTab2 ss1
  where
      isNotTab2 :: Char -> Bool
      isNotTab2 c = c /= '\t'

1つ目が普通に関数定義した場合(グローバルに関数を定義した場合と呼ばれます)、
2つ目はラムダ式、
3つ目がwhere節です。
上の3つはどれも同じ事をやっています。

whereの効果 -関数定義の封じ込め

2つ目のラムダ式は無視しておきます。
重要なのは、isNotTab2の定義がwhere節の外では
有効でないということです。
試しに上をMain.hsに定義してみましょう。
そのあとでisNotTabとisNotTab2の型をチェックしてみてください。

*Main Lib> :t isNotTab
isNotTab :: Char -> Bool
*Main Lib> :t isNotTab2

<interactive>:1:1: error:
    • Variable not in scope: isNotTab2
    • Perhaps you meant ‘isNotTab’ (line 144)

isNotTabについては型チェックが出来ますが、isNotTab2については、
Variable not in scope、つまり、スコープ中でそういう変数はありませんよ、
というエラー文が出てきます。余談ですが、ご丁寧にPerhaps you meantというサジェスチョンがなされるのが良い感じですね。

whereの読み方

whereというのは、「ゴニョゴニョが出てきたが、「ここにおいて」このゴニョゴニョは以下のとおりです。」という意味です。
英語の関係代名詞、関係副詞みたいなものです。実際whereは関係副詞ですね。

ss1left''' = takeWhile isNotTab2 ss1
  where
      isNotTab2 :: Char -> Bool
      isNotTab2 c = c /= '\t'

上のゴニョゴニョはisNotTab2に相当します。上から順番に読んでいった場合、こういう反応になります。すなわち、、、
ふむふむ、takeWhileか、第一引数はなんらかの判定を表す関数だろうな(a -> Bool)。で、それは、、isNotTab2?、そんなの見たこと無いぞ、何ぞ?ああ、whereがあるな、そっちに書かれているな、
こういう反応になってきます。

Mecabの出力結果の加工

前回の最後に、「せっかくなのでラムダ式あたりを」と言っていましたが、
ラムダ式とwhere節に熱を入れすぎて、ちょっと忘れていました(笑)。
やりたかったのは、
("今日","名詞,副詞可能,,,,,今日,キョウ,キョー")
について、
("今日","名詞")
と加工したかったのです。
今回の理解度テストとして、
"今日\t名詞,副詞可能,,,,,今日,キョウ,キョー"
から出発して、("今日","名詞")を出力する関数をwhere節とラムダを使って書いてみます。
皆さん、考えてみてください。正解は以下のような感じです。

getTokenTag :: String -> (String, String)
getTokenTag str = (token, tag)
  where
    token = takeWhile (\c -> c /= '\t') str
    tagPrim = tail (dropWhile (\c -> c /= '\t') str)
    tag = takeWhile (\c -> c /= ',') tagPrim

こんな感じですね。ラムダ式とwhere節ですっきりしています。
getTokenTag内で全て閉じているのが良いですね。
getMecabedした結果に、mapしてみましょう。

strs <- getMecabed "ラムダ式とwhere節ですっきりしています。"
results = map getTokenTag strs
cshowUI results
*Main Lib> strs <- getMecabed "ラムダ式とwhere節ですっきりしています。"
*Main Lib> results = map getTokenTag strs
*Main Lib> cshowUI results
(0,("ラムダ","名詞"))
(1,("式","名詞"))
(2,("と","助詞"))
(3,("where","名詞"))
(4,("節","名詞"))
(5,("で","助詞"))
(6,("すっきり","副詞"))
(7,("し","動詞"))
(8,("て","助詞"))
(9,("い","動詞"))
(10,("ます","助動詞"))
(11,("。","記号"))
(12,("EOS"*** Exception: Prelude.tail: empty list

だいぶすっきりして見えますね。
最後にExceptionというエラーが出ていますが、、
今回は盛り沢山だったので今は見なかったことにします、、、
やっぱり気になりますね。
どこがエラーしているかというと、getTokenTag中のtailがダメです。
一応解決法を書いておきます。これはguardというギミックを学ぶ際に何をやっているか説明します。
|とかotherwiseの箇所です。場合分けをしています。
興味深いのはwhere節が複数出てくるところです。
droppedは更に深いところに封じ込められています。

getTokenTag :: String -> (String, String)
getTokenTag str = (token, tag)
  where
    token = takeWhile (\c -> c /= '\t') str
    tagPrim
     | dropped == "" = ""
     | otherwise = tail dropped
      where
         dropped = (dropWhile (\c -> c /= '\t') str)
    tag = takeWhile (\c -> c /= ',') tagPrim

これでエラーは出ないはずです。試してみてください。

終わりに

今回は盛り沢山でした。
要点は、高階関数を有効に使うには、ラムダ式とwhere節が必要だということです。
これは本当に重要です。
上では具体例に即して説明しましたが、参考書の該当する箇所を必ず読み返して体系的知識をつけておいてください。
次回はファイル入出力と、ガード(上で出てきた)、高階関数fold, filterについてご説明します。foldはmapと対になる大事な関数です。
map, fold, filter, zip辺りの数学的に筋の良い高階関数が揃ってくると、
いかにも関数型プログラミングな感じがしてきます。
これらを組み合わせて複雑なことをやっていきます。
余裕があれば、こういう筋の良い高階関数で書いていった場合のご利益(ごりやく)についても触れたいです。
fusionというものです。
ラムダ式、where節も当然出てきますので、しっかり復習しておいてください。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする