Haskell勉強会準備室4 (はじめての高階関数)

今回は、結構盛り沢山です。
高階関数(takeWhile, dropWhile, map, zip)、λ式(ラムダ式、lambda Expression)、とレキシカルスコープ(lexical scope)を学んでいきます(長くなったので、今回は高階関数までです)。
Haskellをやる上で最重要項目と言ってもいいです。
前回は下のsを\t(タブ)で分割するためにdivideAtTabという関数を作成しました。

s = "今日\t名詞,副詞可能,*,*,*,*,今日,キョウ,キョー"
take2 s = take 2 s
drop3 s = drop 3 s
divideAtTab :: String -> (String, String)
divideAtTab s = (take2 s, drop3 s)

前回行うのを忘れていました(笑)が、divideAtTabをsに適用してみます。

*Main Lib> s = "今日\t名詞,副詞可能,*,*,*,*,今日,キョウ,キョー"
*Main Lib> uprint (divideAtTab s)
("今日","名詞,副詞可能,*,*,*,*,今日,キョウ,キョー")

結果はタプルで得られています。"今日"と、"名詞,副詞可能,*,*,*,*,今日,キョウ,キョー"とで分かれています。
前回の終わりでこの関数divideAtTabはダメだと述べました。なぜダメなのかを確認してみます。
s以外には使えないということです。

ss <- getMecabed "今日はmap関数を勉強していきます。"
s = head ss
s = ss !! 0
s == s0
s1 = ss !! 1

上のsが"今日\t名詞,副詞可能,*,*,*,*,今日,キョウ,キョー"でした。
s1に適用してみてください。

*Main Lib> uprint (divideAtTab s1)
("は\t","詞,係助詞,*,*,*,*,は,ハ,ワ")

はい、"\t"(タブ)で切れていません。必ずしも、タブより前は2文字とは限らないので、だめです。
この辺り改善しておきます。ここで高階関数が出てきます。高階関数の意味は後で述べます。

初めての高階関数

takeWhileとdropWhile

まずは使ってみましょう。Data.ListのtakeWhileとdropWhileを使います。
復習ですが、文字列(String)は[Char]として表されます。

"今日\t名詞,副詞可能" == ['今', '日', '\t', '名', '詞', ',', '副', '詞', '可', '能']

Trueになっていると思います。
さて、上の'\t'で分割したいのでした。
takeWhileは英語のとおりによむとtakeWhile .. 「Whileって、どういう間takeし続けるの?」というリアクションになります。
そうです、いつまでtakeするんだというのが抜けています。例のごとくtakeWhileの型を見ましょう。

*Main Lib> :t takeWhile
takeWhile :: (a -> Bool) -> [a] -> [a]

第一引数が(a -> Bool)になっています。これはaという型を受け取り、Boolを返す関数になっています。
aは型ならなんでも良いよ、という意味です(補足ですが自分で関数定義する際には、aでなくてもbとかcとか書いてもいいです)。
上の例だとCharを考えています。
この(a -> Bool)が「Whileって、どういう間takeし続けるの?」に相当します。
課題に戻ります。'\t'より前を得たいので、「どういう間?」、というのは「'\t'でない間」と言えそうです。
'\n'でない間というのは以下のとおりに書けます。

isNotTab :: Char -> Bool
isNotTab c = not (c == '\t')

HIEは優秀ですね、not == でなく /=を使えと言ってきます。

isNotTab :: Char -> Bool
isNotTab c = c /= '\t'

はい、isNotTabの型を確認してください。(a -> Bool)みたいになっています。
これを使ってみます。Main.hsに上の関数を追記してstack ghciしましょう。

ss1 = ['今', '日', '\t', '名', '詞', ',', '副', '詞', '可', '能']
ss1Left = takeWhile isNotTab ss1

"今日"が得られているはずです。ではdropWhileも同様につくってみてください。
takeの逆になります。同じように実験してみてください。
dropWhileだと'\t'が先頭に残りますので、先頭より後ろという関数tailを使います。
これもData.Listの関数で前回の宿題で出てきました。

ss1 = ['今', '日', '\t', '名', '詞', ',', '副', '詞', '可', '能']
ss1Right = tail (dropWhile isNotTab ss1)

takeWhileとdropWhileを使って前回のdivideAtTabを書き換えます。

divideAtTab2 :: String -> (String, String)
divideAtTab2 s = (takeWhile isNotTab s, tail (dropWhile isNotTab s))

ss1に適用してみます。

*Main Lib> ss1 = ['今', '日', '\t', '名', '詞', ',', '副', '詞', '可', '能']
*Main Lib> ss1Divided = divideAtTab2 ss1
*Main Lib> uprint ss1Divided
("今日","名詞,副詞可能")

所望の結果が得られているのが分かります。

高階関数の意味

高階関数とは、「関数」を引数にとる「関数」のことです。takeWhileもdropWhileも(a -> Bool)という関数が引数の一つになっていました。
なので高階関数です。英語だとhigher Order Functionと呼ばれます。
0階が非関数(Int, String, Char, Boolなど)、1階が普通の関数(上だとisNotTabとか)、2階は1階の関数を引数にとる関数(上だとtakeWhile)、
3階は2階の関数を引数にとる関数(まだ出てきていない)、、、みたいに無限に続きます。なので高階と呼ばれます。
高階関数は関数型プログラミングで非常に重要な概念です。
上のisNotTabみたいな箇所を他の関数、例えば、isNotAlphabetみたいなの(ここでは定義しません)と簡単に差し替えられます。
Haskellは特殊ギミックをたくさん持っていますが、1つだけ選べと言われれば高階関数を自分は選びます。
ただし、この後で出てくるλ式とレキシカルスコープがなければ威力は半減です。
その前にmap関数を覚えていきます。これは高階関数の中でも最重要な関数になります(最も重要だらけですね(笑))。

map関数

リストのすべての要素にある関数を適用する高階関数です。何を言っているかはまずは具体的に使ってみましょう。
getMecabedの結果はリストで得られていました。それぞれの文字数をカウントしてみます。

ss <- getMecabed "今日はmap関数を勉強していきます。"
cshowUI ss
lens = map length ss
cshowUI lens
以下のような結果が出てきているはずです。
*Main Lib> cshowUI ss
(0,"今日\t名詞,副詞可能,*,*,*,*,今日,キョウ,キョー")
(1,"は\t助詞,係助詞,*,*,*,*,は,ハ,ワ")
(2,"map\t名詞,固有名詞,組織,*,*,*,*")
(3,"関数\t名詞,一般,*,*,*,*,関数,カンスウ,カンスー")
(4,"を\t助詞,格助詞,一般,*,*,*,を,ヲ,ヲ")
(5,"勉強\t名詞,サ変接続,*,*,*,*,勉強,ベンキョウ,ベンキョー")
(6,"し\t動詞,自立,*,*,サ変・スル,連用形,する,シ,シ")
(7,"て\t助詞,接続助詞,*,*,*,*,て,テ,テ")
(8,"いき\t動詞,非自立,*,*,五段・カ行促音便,連用形,いく,イキ,イキ")
(9,"ます\t助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス")
(10,"。\t記号,句点,*,*,*,*,。,。,。")
(11,"EOS")
[(),(),(),(),(),(),(),(),(),(),(),()]
*Main Lib> lens = map length ss
*Main Lib> cshowUI lens
(0,29)
(1,22)
(2,22)
(3,29)
(4,23)
(5,33)
(6,28)
(7,23)
(8,35)
(9,31)
(10,21)
(11,3)

lengthはリストの長さを返す関数です。
ssは11個の要素を持っているリストですが、それぞれ長さが得られていると思います。
ちょっと結果が見えにくいです。それぞれの長さについて何番目の要素かをいちいち確認しないといけません。
zip関数を使ってみます。これもいかにも関数型プログラミングっぽい高階関数です。

ss <- getMecabed "今日はmap関数を勉強していきます。"
cshowUI ss
lens = map length ss
nsAndss = zip lens ss
cshowUI nsAndss

以下の通り出力されます。

*Main Lib> lens = map length ss
*Main Lib> nsAndss = zip lens ss
*Main Lib> cshowUI nsAndss
(0,(29,"今日\t名詞,副詞可能,*,*,*,*,今日,キョウ,キョー"))
(1,(22,"は\t助詞,係助詞,*,*,*,*,は,ハ,ワ"))
(2,(22,"map\t名詞,固有名詞,組織,*,*,*,*"))
(3,(29,"関数\t名詞,一般,*,*,*,*,関数,カンスウ,カンスー"))
(4,(23,"を\t助詞,格助詞,一般,*,*,*,を,ヲ,ヲ"))
(5,(33,"勉強\t名詞,サ変接続,*,*,*,*,勉強,ベンキョウ,ベンキョー"))
(6,(28,"し\t動詞,自立,*,*,サ変・スル,連用形,する,シ,シ"))
(7,(23,"て\t助詞,接続助詞,*,*,*,*,て,テ,テ"))
(8,(35,"いき\t動詞,非自立,*,*,五段・カ行促音便,連用形,いく,イキ,イキ"))
(9,(31,"ます\t助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス"))
(10,(21,"。\t記号,句点,*,*,*,*,。,。,。"))
(11,(3,"EOS"))

長さとそれ自身がタプルの形で出力されています。
zipは2つのリストの要素をペア(タプル)にしてくっつける(zip)高階関数です。型を見てみます。

*Main Lib> :t zip
zip :: [a] -> [b] -> [(a, b)]

下手に日本語で表現するよりも式で見たほうが速いですね。この辺りもHaskellのいいところです。
型を見るだけでその関数で何が出来るかがある程度推測できます。
上の例だとlens(長さのリスト)とss(文字列のリスト)をペアにしています。

mapの型

mapに戻ります。例のごとく型を見てみましょう。

*Main Lib> :t map
map :: (a -> b) -> [a] -> [b]

第一引数は関数(a型の値をb型の値に変換する)、第二引数はリスト(a型)、で出力結果がリスト(b型)になっています。
型をみるだけである程度予測できそうですね。

関数の差し替え

次に、lengthでなくdivideAtTabをmapしてみます。

*Main Lib> divided = map divideAtTab2 ss
*Main Lib> cshowUI divided
(0,("今日","名詞,副詞可能,*,*,*,*,今日,キョウ,キョー"))
(1,("は","助詞,係助詞,*,*,*,*,は,ハ,ワ"))
(2,("map","名詞,固有名詞,組織,*,*,*,*"))
(3,("関数","名詞,一般,*,*,*,*,関数,カンスウ,カンスー"))
(4,("を","助詞,格助詞,一般,*,*,*,を,ヲ,ヲ"))
(5,("勉強","名詞,サ変接続,*,*,*,*,勉強,ベンキョウ,ベンキョー"))
(6,("し","動詞,自立,*,*,サ変・スル,連用形,する,シ,シ"))
(7,("て","助詞,接続助詞,*,*,*,*,て,テ,テ"))
(8,("いき","動詞,非自立,*,*,五段・カ行促音便,連用形,いく,イキ,イキ"))
(9,("ます","助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス"))
(10,("。","記号,句点,*,*,*,*,。,。,。"))
(11,("EOS"*** Exception: Prelude.tail: empty list

何か最後にエラーが出ていますが、これは後で直します。
いかがでしょうか?簡単に欲しい結果が得られます。
関数をまずつくっておく(今回であればdivideAtTab2)、
それをmapを使ってリストの要素すべてに適用、
というのはよくあるパターンです。
関数を差し替えるだけで色々出来ます。
divideAtTab2というのはtabにしか使えないので、拡張しておきます。

isNotSpecChar c c1 = c /= c1
divideAt :: Char -> String -> (String, String)
divideAt c s = (takeWhile (isNotSpecChar c) s, tail (dropWhile (isNotSpecChar c) s))

さて、上の("今日","名詞,副詞可能,*,*,*,*,今日,キョウ,キョー")
について、タプルの第二要素では"名詞"だけが必要で"副詞可能,*,*,*,*,今日,キョウ,キョー"は不要です。
後で使うかも知れませんが、今は削っておきます。上のresult2の箇所を変更します
どうやって削りますか?今までに出てきた知識と関数で容易に削ることが出来ます。
せっかくなのでラムダ式を使ってみましょう(長くなったので次回)。

シェアする

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

フォローする