Haskell勉強会準備室3 (Data.Listの関数の紹介とタプル)

前回はIOについてのざっくりした説明をしました。
ここからはmapという高階関数、タプル、関数定義、λ式(ラムダ式、lambda Expression)、とレキシカルスコープ(lexical scope)を学んでいきます。
(追記、長くなったので、この回は関数のData.Listの一部関数、部分適用、タプル、関数定義に絞ります)。
これらはいかにも関数型プログラミングな概念です。かなり重要です。
再出ですが、Haskellは純粋関数型プログラミング言語です。
あとは、勉強会の最初に述べましたが、Data.Listの関数をいくつか学びます。
Data.Listの説明書はHackageを見てください。
HaskellのライブラリはHackageに説明書きが用意されます。
Haskellの色んな解説ページ(このページもそうですが)を見るだけでなく、Hackageを自分で見る癖をつけましょう。
英語なのがきついかも知れませんが、段々慣れてきます。
今回の説明の後に必ず見ておいてください。

下準備

VSCodeのREPLに以下の式を入力してください。

ss <- getMecabed "今日はmap関数を勉強していきます。"

前回の復習ですが、これはgetMecabedの結果をssに束縛(bind)しています。
getMecabedはIOを返すので、そのIOを取り除くために(<-)を使っています。
ssの中身を見るには式ssを評価すれば良いのですが、文字化けします。

*Main Lib> ss
["\20170\26085\t\21517\35422,\21103\35422\21487\33021,*,*,*,*,\20170\26085,\12461\12519\12454,\12461\12519\12540","\12399\t\21161\35422,\20418\21161\35422,*,*,*,*,\12399,\12495,\12527","map\t\21517\35422,\22266\26377\21517\35422,\32068\32340,*,*,*,*","\38306\25968\t\21517\35422,\19968\33324,*,*,*,*,\38306\25968,\12459\12531\12473\12454,\12459\12531\12473\12540","\12434\t\21161\35422,\26684\21161\35422,\19968\33324,*,*,*,\12434,\12530,\12530","\21193\24375\t\21517\35422,\12469\22793\25509\32154,*,*,*,*,\21193\24375,\12505\12531\12461\12519\12454,\12505\12531\12461\12519\12540","\12375\t\21205\35422,\33258\31435,*,*,\12469\22793\12539\12473\12523,\36899\29992\24418,\12377\12427,\12471,\12471","\12390\t\21161\35422,\25509\32154\21161\35422,*,*,*,*,\12390,\12486,\12486","\12356\12365\t\21205\35422,\38750\33258\31435,*,*,\20116\27573\12539\12459\34892\20419\38899\20415,\36899\29992\24418,\12356\12367,\12452\12461,\12452\12461","\12414\12377\t\21161\21205\35422,*,*,*,\29305\27530\12539\12510\12473,\22522\26412\24418,\12414\12377,\12510\12473,\12510\12473","\12290\t\35352\21495,\21477\28857,*,*,*,*,\12290,\12290,\12290","EOS"]

これを回避するにはuprint ssという式を入力してください。

*Main Lib> uprint ss
["今日\t名詞,副詞可能,*,*,*,*,今日,キョウ,キョー","は\t助詞,係助詞,*,*,*,*,は,ハ,ワ","map\t名詞,固有名詞,組織,*,*,*,*","関数\t名詞,一般,*,*,*,*,関数,カンスウ,カンスー","を\t助詞,格助詞,一般,*,*,*,を,ヲ,ヲ","勉強\t名詞,サ変接続,*,*,*,*,勉強,ベンキョウ,ベンキョー","し\t動詞,自立,*,*,サ変・スル,連用形,する,シ,シ","て\t助詞,接続助詞,*,*,*,*,て,テ,テ","いき\t動詞,非自立,*,*,五段・カ行促音便,連用形,いく,イキ,イキ","ます\t助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス","。\t記号,句点,*,*,*,*,。,。,。","EOS"]

uprintのuはユニコード(Unicode)のuです。日本語はユニコードになります。
もう少し結果を見やすくする関数を用意しておきました。
以下を入力してみてください。

cshowU ss

下のような結果になります。

*Main Lib> cshowU ss
"今日\t名詞,副詞可能,*,*,*,*,今日,キョウ,キョー"
"は\t助詞,係助詞,*,*,*,*,は,ハ,ワ"
"map\t名詞,固有名詞,組織,*,*,*,*"
"関数\t名詞,一般,*,*,*,*,関数,カンスウ,カンスー"
"を\t助詞,格助詞,一般,*,*,*,を,ヲ,ヲ"
"勉強\t名詞,サ変接続,*,*,*,*,勉強,ベンキョウ,ベンキョー"
"し\t動詞,自立,*,*,サ変・スル,連用形,する,シ,シ"
"て\t助詞,接続助詞,*,*,*,*,て,テ,テ"
"いき\t動詞,非自立,*,*,五段・カ行促音便,連用形,いく,イキ,イキ"
"ます\t助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス"
"。\t記号,句点,*,*,*,*,。,。,。"
"EOS"
[(),(),(),(),(),(),(),(),(),(),(),()]

cshowUIという関数も用意しました。こちらも試してみてください。
さて、これ、色々な情報が載っていますが、もう少し加工したくなります
具体的には\t辺りで分割すると具合が良さそうです。
というのも、\tより左は単語(これをトークン(Token))であり、右は文法情報になっています。
いきなり全部考えるのは難しいので、ひとつだけ取り出しておきます。
以下をREPLに打ちましょう。

s = "今日\t名詞,副詞可能,*,*,*,*,今日,キョウ,キョー"

ここからはこのsをなんとかしていきます。

初めてのData.List関数

\tの左右で分割する方法は幾つかあります。

作戦1 -take, drop関数

簡単なのは文字数で分割することです。
以下を評価してみてください。

token = take 2 s
gramInfo = drop 3 s

gramはgrammar(文法)を意識しています。uprintしてみましょう。

*Main Lib> uprint token
"今日"
*Main Lib> uprint gramInfo
"名詞,副詞可能,*,*,*,*,今日,キョウ,キョー"

はい、所望の結果が得られています。
takeとdropがどういう関数か、想像できますか?
takeとdropの型を見ておきます。見たことがない関数が出てきた際には、
型をチェックしましょう。

take :: Int -> String -> String
drop :: Int -> String -> String

こんな感じです。Haskellの関数について少し補足が必要です。
数学や、他の言語だと関数は、

take' (2, s)
drop' (3, s)

みたいに書くと思います。Haskellでもこう書くこと(こう関数定義すること)も出来ますが、上のように、

take 2 s
drop 3 s

みたいに書かれることが多いです。
これは関数の部分適用、という概念と密接に関わってきますが、、
良い機会なのでここで述べてしまいます。

関数の部分適用

上のtakeやdropの定義は少し省略がなされています。正確には以下のとおりです。takeだけ取り上げます。

take :: Int -> (String -> String)
(take n) str = ...

カッコをつけてみました。型宣言の方から見てみます。
takeはIntを受け取って(String -> String)という型を持つ関数を返す、と読めます。
実際そうなっています。REPLでtake 2の型をチェックしてみてください。
Haskellだと、関数に引数(ここでは2)を適用することで、新しい関数(ここではtake 2)を返すことが出来ます。
これを 関数の部分適用と言います。
あんまし述べていませんでしたが、関数型プログラミングは、
その名の通り関数を駆使してプログラムしていくスタイルです。
既存の関数から新しい関数をつくれる、この辺りは流石と言った感じです。
後で述べますが、高階関数やラムダと組み合わせることで、更に関数を拡張できます。
で、元に戻りますが、なぜ下のtakeのように書くか、分かる感じがしませんか?

take' (2, s)
take 2 s

(2, s)だとこれらの2とsの2つを同時にtakeに与えている事になります。これだと2だけの部分適用というのは出てきません。
しかし、これも詳しくはラムダ式を習ってからですが、回避可能です。以下は、ラムダ式を知っている人向けの解説です。
知らない人は後で見てみてください。要点は、ラムダ式があれば引数の位置は問題にならずに部分適用を自由にやれる、ということです。

take2 s = (\n -> take' (n, s)) 2

上のようにラムダ式でかけば、部分適用したものが得られます。REPLでfの型と動作を確認してみてください。

take' (n, s) = take n s
take2 s = (\n -> take' (n, s)) 2
:t take2
:t (take 2)
take2 "Hello"
take 2 "Hello"
uprint (take2 "これはテストです。")
uprint (take 2 "これはテストです。")
*Main Lib> take' (n, s) = take n s
*Main Lib> take2 s = (\n -> take' (n, s)) 2
*Main Lib> :t take2
take2 :: [a] -> [a]
*Main Lib> :t (take 2)
(take 2) :: [a] -> [a]
*Main Lib> take2 "Hello"
"He"
*Main Lib> take 2 "Hello"
"He"
*Main Lib> uprint (take2 "これはテストです。")
"これ"
*Main Lib> uprint (take 2 "これはテストです。")
"これ"

関数型プログラミングをするには、部分適用しやすい関数の形、という縛りがあると辛いです。
ラムダ式がその辺りの柔軟さを可能とします。正直ラムダ式がわかっていれば、そう恐れることはないです。
ただし見れば解ると思いますが、冗長です。タイピング量が多いです。
なので、部分適用しやすい形で関数を定義する、というのが、
Haskellっぽい関数の定義の仕方になってきます。
最後にどうやって見えるか、イメージを書いておきます。

plus6Element :: Int -> Int -> Int -> Int -> Int -> Int -> Int
plus6Element x y z a b c = x + y + z + a + b + c <- plus6Element x y z a b cは6つ整数が与えられてそれらの合計(整数)を返す関数。
(plus6Element x y z a b) c = x + y + z + a + b + c <- (plus6Element x y z a b)はあとひとつ整数が与えられれば、合計(整数)を返すような関数、と見る
(plus6Element x y z a) b c = x + y + z + a + b + c <- (plus6Element x y z a)はあとふたつ整数が与えられれば、合計(整数)を返すような関数、と見る
(plus6Element x y z) a b c = x + y + z + a + b + c <- (plus6Element x y z)はあとみっつ整数が与えられれば、合計(整数)を返すような関数、と見る

タプル

ここでタプルという概念について説明します。ちょうどいいタイミングです。
というのも、上に出てきたのはタプルです。

take' :: (Int, [a]) -> [a]
take' (n, s) = take n s

(Int, [a])と(n, s)、これがタプルになります。前者は型についてのタプルになっており、後者は値についてのタプルになっています。
数学とか物理だと(x, y)みたいな座標がありますが、これはタプルの一例です。違ってくるのは、(x, y)だと大概はxもyも実数ですが、
Haskellのタプルだとxとyは異なるもの(型)でも良いです。
上もそうですね、[a]としてStringを考えれば、(Int, String)となり、(Int, Int)である必要はないと分かります。
このように、異なる型をセットで扱える、というのがタプルになります。
前回出てきたリストとの対比を考えると、リストはその要素が全て同じ型である必要があります。

iss = ["これは", 1, "です"] これはN.G.
iss = ["これは", "1", "です"] これはO.K.
ss = ["これは", "テスト", "です"] これはO.K.
ss = ["これ", 'は', "テスト", "で", 'す'] これはN.G.
ss = ["これ", "は", "テスト", "で", "す"] これはO.K.

それぞれ、なぜN.G.か、なぜO.K.かを考えてみてください。

関数定義

作戦1 -take, drop関数を用いた定義

さて、次に関数定義についてです。既にgetMecabedのような関数について、
手製云々言っていることからも分かるように、関数は自分で定義できます。
上のsを\tの左右で分割する関数を定義していきましょう。
関数定義は1.関数の型の宣言、2.具体的な実装の2つを書きます。
実装というのは関数の内容のことです。型を見ただけだとどういう関数かの詳細はわかりません。
実は1については省略が可能ですが、最初は両方書いていきましょう。
Haskell以外の他言語(C++とか)でも、型宣言は重要で必須とされたりします。
関数名はdivideAtTabとしておきます。言い忘れましたが、\tはタブを表します。

divideAtTab :: String -> ??

入力はsなので、Stringで良いと思います。出力をどうしますか?
左の結果と右の結果を返す必要があります。2つ以上の結果を返すには先ほど出てきたタプルを使うのが基本です。
先ほどは関数の引数としてのタプルを考えましたが、関数の出力としてもタプルを使えます。

divideAtTab :: String -> (String, String)

になります。次に具体的な実装を考えます。
最初に答えを書いちゃいます。

divideAtTab :: String -> (String, String)
divideAtTab s = (result1, result2)
   where
     result1 = take 2 s
     result2 = drop 3 s

whereというのは、「ゴニョゴニョが出てきたが、「ここにおいて」このゴニョゴニョは以下のとおりです。」という意味です。
英語の関係代名詞、関係副詞みたいなものです。実際whereは関係副詞ですね。
上のゴニョゴニョはresult1とresult2に相当します。上から順番に読んでいった場合、result1とresult2は初出なので、
更に読まないと何者かわかりません。

ファイルへの書き込み

REPLへの複数行の入力

REPLは基本的には1行ずつ入力していきますが、上のdivideAtTabは複数行にまたがっています。
こういうのをREPLに入力するには、:{, :}を使います。以下のように入力してみてください。

:{
divideAtTab :: String -> (String, String)
divideAtTab s = (result1, result2)
   where
     result1 = take 2 s
     result2 = drop 3 s
:}

エラーが出ませんね。
しかし、これをいちいちREPLに書いていくのは面倒です。
ソース側に書き込んでしまえば、REPL起動時(stack ghciした際)に関数を併せて読み込んでくれます。
ソースにdivideAtTabを書き込んで、そのあと:t divideAtTabなどを試してみてください。
関数が存在しないというエラーは出てこないはずです。

ホームワーク(宿題)

HackageのData.Listの、
Basic functionsの動作を確認しましょう。
型を見ると以下のとおりになっています。

(++) :: [a] -> [a] -> [a]
head :: [a] -> a
last :: [a] -> a
tail :: [a] -> [a]
init :: [a] -> [a]
uncons :: [a] -> Maybe (a, [a])
null :: Foldable t => t a -> Bool
length :: Foldable t => t a -> Int

Foldableを無視すれば全て[a]が引数になっています。
FoldableもListのようなものです。正確にはListよりも広い概念です。
下を試してみてください。

xs = [0 .. 10]
ys = [200, 202 .. 210]
xs ++ ys
head xs
last xs
tail xs
init xs
uncons xs
null xs
null []
length xs
zs = ['a' .. 'z']
ws = ['A' .. 'Z']
zs ++ ws
head zs
last zs
tail zs
init zs
uncons zs
null zs
length zs

一つ説明を忘れていました。Haskellは上のxs, ys, zs, wsのように、
連続するリストを簡単に生成できます。

まとめ

今回は関数の部分適用、タプル、関数定義を学びました。次回は高階関数を考えていきます。
ネタばらししておきますが、上で定義したdivideAtTabはダメです(笑)。
takeとdropではなく、takeWhileとdropWhileを使うといい感じなのですが、これらは高階関数ですので次回に回します。

シェアする

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

フォローする