Cogito Ergo Sum.

我思う故に我あり

リスト内包表記

・リスト内包表記

 プログラミング言語Pythonを学ぶ途中で僕が大いに戸惑ったのが、「リスト内包表記」だ(「セット内包表記」というヤツもある)。まぁ、「リスト」や「セット(集合)」を記述する際の単なる「表記法」に過ぎないと言えば過ぎないし、確かに、ある種のリスト操作(やセットの操作)において、この「内包表記」を用いると驚くほどスマートに記述できるような場合もあるのだが…、これがどうにも僕は苦手だ。

 だいたい「内包表記」という用語がどうにも不自然な気がする。こんな言葉、僕はこれまでの人生で1度も聞いたことがない。

 今年に入ってからプログラミング言語Haskellの勉強を始めて、再びブチ当たったのがこの「リスト内包表記」。むしろ、Haskellを含む「関数型プログラミング言語」の文化から、この「内包表記」をPythonは取り入れたのかもしれない。

 ところがどうしてこれが面白いもので、Pythonでは苦手にしていた内包表記が、Haskellコード中に出てくると何だかホッとするのだ(笑)。リスト内包表記をHaskellのコードの中に置くと、妙に「命令型(手続き型)プログラミング言語」っぽく見えるのだ。あと、SQLのSELECT文のようだな、とも思う。

 例えば、[1, 2, 3, 4, 5]というリストから、各要素をそれぞれ2乗した値からなる(別の)リストを作りたいとする。それをHaskellの「リスト内包表記」では、

[x ^ 2 | x <- [1..5]]

みたいに書く。

 Pythonだと、

[x ** 2 for x in range(1, 6)]

こんな感じかな?

 こういうコードを見た時に、僕の頭の中では、

arr1 = range(1, 6)
arr2 = [ ]
for x in arr1:
 arr2.append(x ** 2)

こんな感じの処理を思い浮かべている。

 僕はPythonよりもRubyの方が慣れてるんだけど、Rubyだと、

arr1 = (1..5).to_a
arr2 = [ ]
arr1.each{|x| arr2 << x ** 2}

こんな感じか。

 いずれにせよ、頭の中では、まず空のリスト(配列)を用意して、そこに「ループ処理」として、元のリスト(配列)の要素から新たに生成した値を順に追加していっている。最終的に、所望のデータ(arr2)が手に入る。発想がまさに「手続き型」なんだよね。

 「『リスト内包表記』なんて単なる表記法に過ぎない」と割り切れば、C言語系で、

for (i = 0; i < n; i++) {
 //処理
}

は、

i = 0;
while (i < n) {
 //処理
 i++;
}

と同じこと、ってのと大して変わらんのかもしれないけど…。とにかく僕は、Pythonではこの「リスト内包表記」が苦手。

 ところが、そもそも「手続き型のループ処理」を記述できないHaskellのコード中に、まさにその「手続き型のループ処理」の匂いのする「リスト内包表記」が出てくると、何だかホッとしてしまう(笑)。僕の頭の中は完全に「手続き型」なので。

 

・List Comprehension

 書籍化もされた(日本語版も出ている)WEB上の人気Haskell入門「Learn You a Haskell for Great Good!」を読んでいて(英語!)、この「リスト内包表記」のことを英語では「list comprehension」と呼ぶことを知った。「comprehension」=「理解」というのが僕の「comprehension」=「理解」なので、これまた大いに戸惑った。「リスト理解」って何やねん?

 そこで、英和辞典を見てみたところ…、「comprehension」には「理解」「読解力」というような意味の他に、「包含」という意味があるらしい。動詞「comprehend」にも、「理解する」という意味だけでなく「含む」という意味もあるようだ。「包含」か…。

 ちなみに、「list comprehension」そのものは載っていなかったが、「listening comprehension」=「リスニング能力(聴解力)」ならあった。似てるけど、全然違う(笑)。

 

・「集合の表し方」

 PythonHaskellの入門書を読んでいると、この「リスト内包表記」について、「高校数学の『集合』のところで習うアレ」みたいに説明されていることが多い。ところが僕には…、全く身に覚えがない。上にも書いた通り、「内包」という用語には全く見覚えがない。

 そこで、7〜8年前から17〜18年前に刊行された3冊の高校数学の教科書を見てみた(数研出版実教出版、東京書籍)。その時々の学習指導要領によって違うのだろうが、「集合」についてはだいたい「数学I」か「数学A」で習うようだ。と言うことは、高校1年か…。僕は中学までは数学が一番の得意科目だったのに、高校1年で全くわからなくなってしまい、「仕方なく」文系に進んだクチ。高1数学の内容なら、何も覚えていないというのにも納得がいく。

 で、教科書や参考書の当該箇所をジックリ読んでみたのだが…、「内包」「内包表記」という用語そのものはやはり載っていない(もちろん別の時代の学習指導要領下では載っていた可能性はあるが)。

 そこでは、「集合の表し方」として、「要素を書き並べる」方法と「要素の満たす条件を文章や式で示す」方法、の2通りがあることが述べられており、例えば「12の正の約数の集合A」を

・「要素を書き並べる」方法
A = {1, 2, 3, 4, 6, 12}

・「要素の満たす条件を文章や式で示す」方法
A = {x | xは12の正の約数}

などと表記することが示されている。

 後者の

A = {x | xは12の正の約数}

のような書き方には何となく見覚えがあり、高校生の当時、

A = {12の正の約数}

で充分じゃね〜か!?と二度手間な表現と感じていたことをウッスラと思い出した。

 「要素の満たす条件を文章や式で示す」方法についてはいくつか例が載っていて、例えば、「正の偶数全体の集合B」なら、

B = {x | xは偶数, x > 0}
B = {2n | nは整数, 1 ≦ n}
B = {2n | n = 1, 2, 3, …}
B = {2n | nは自然数}
B = {2n | n ∈ N}

等々、様々な表記が可能であることが示されている。

 こうやって見ると、確かに、

b = [x | x <- [1..], even x]
b = [x | x <- [2,4..]]
b = [2 * n | n <- [1..]]

等々、「高校数学で習ったアレと同じ」と書きたくなってしまうのも何となくわかる。Haskellのリスト内包表記で用いられる「<-」という記号も、これまでは「左向きの矢印」=「←」に見えていたけど、数学記号の「∈」のように見えると言えなくもない(笑)。ちょっと無理あるように思うけど…(笑)。

 更に読み進めていくと、集合と集合の間の「包含関係」についての記述が出てくる(「P ⊂ Q」みたいなヤツね)。ここへきてようやく、「comprehension」の訳語「包含」と「内包表記」が結び付いた! 「内包」も数学辞典とかになら載ってるのかね…?

 意外だったのは「A ∩ B」。僕は「積集合」と昔習ったような気がするんだけど、僕の見た3冊の教科書ではどれも「共通部分」と表現されている(それに対して、「A ∪ B」は「和集合」)。教科書には載っていなかったが、ある参考書(『チャート式』!)には「2つの集合の直積(direct product)」という概念が載っていた。RubyのArrayクラスのメソッド「product」はこっちかな(「共通部分」を求めるメソッドは「intersection」)。

 Haskellだと、2つの集合の「共通部分」を求める関数は「Data.List」モジュール内にある「intersect」、「和集合」は「union」。「直積」は…、そういう関数がないワケがない!とは思うけど、まだ見つけられない。こういう時こそ「リスト内包表記」だ!

[(x, y) | x <- arr1, y <- arr2]

こんなのね。

 

・「リスト内包表記」から「高階関数」へ

 と言うワケで、Haskellのオカゲで「リスト内包表記」に対する苦手意識はだいぶ払拭されてきたのだけど…、Haskellの勉強も3週間近くになってくると、今度は逆に「リスト内包表記は野暮」という感覚に変わりつつある(笑)。

 例えば、上述の『[1, 2, 3, 4, 5]というリストから、各要素をそれぞれ2乗したリストを作りたい』なら、こないだまでなら、

[n ^ 2 | n <- [1..5]]

こう書いていたのに、学習が2週間を過ぎた辺りから、高階関数「map」を用いて、

map (^2) [1..5]

と書きたくなってきた。上の「リスト内包表記」を用いた書き方は如何にも野暮ったい。

 同様に、『文字列「ABBA」中の文字「A」の個数を数えたい』なら、

length [c | c <- "ABBA", c == 'A']

よりも、高階関数の「filter」を用いた、

length $ filter (=='A') "ABBA"

(length . filter (=='A')) "ABBA"

かな。

 高階関数の「fold」系はまだ使いこなせないけど、「map」や「filter」なら、Rubyの「Enumerable」モジュールの「collect」と「select」を使うのと大して変わらんし…(もっとも、僕は「collect」と「select」のどっちがどっちだったかどうしても覚えられない(「必要条件」と「十分条件」並に覚えられない!)ので、普段は「map」と「find_all」を使っているが)。

 『[1, 2, 3, 4, 5]というリストから、各要素をそれぞれ2乗したリストを作りたい』時、さっきは

arr1 = (1..5).to_a
arr2 = [ ]
arr1.each{|x| arr2 << x ** 2}

こんな風に書いたけど、実際には

arr1 = (1..5).to_a
arr2 = arr1.map{|x| x * x}

こう書くことの方が多い。

 もっとも、「ABBA」の方は、サスガに

"ABBA".find_all{|c| c == 'A'}.length

とは書かないなぁ。

"ABBA".count('A')

か。

 Haskellを学び始めてからRubyのリファレンスを見ていると、Haskellの関数と同名のメソッドが多々あることに気が付いた。せっかくHaskellを学んでいるんだから、そういうメソッドをもっと積極的に使ってみようかな!