LoginSignup
4
4

More than 5 years have passed since last update.

7つの言語 7つの世界 ~第8章 Haskell 2日目~

Posted at

はじめに

 昨日に引き続き、Haskellを勉強する。

2日目本編「スポックの大きな強み」

高階関数

 Haskellのも、本書で取り上げている全ての言語と同じように高階プログラミングという概念を取り入れている。とりわけ、Haskellは、この高階関数という概念に大きく依存しているらしい。

無名関数
Prelude> (\x -> x) "Logical."
"Logical."
Prelude> (\x -> x ++ " captain.") "Logical,"
"Logical, captain."

 このように、Haskellの無名関数のシンタックスシュガーは(\parm1 .. parmn -> function_body)と書くだけである。他の関数と組み合わせることで強力な道具となる。

mapとwhere

 まず、map関数を無名関数とリストに適用してみる。mapはリストの各項に無名関数を適用し、結果をコレクションとして返す。

Prelude> map (\x -> x * x) [1, 2, 3]
[1,4,9]

 これと同じ処理を1つの関数にまとめてから、無名関数をローカルスコープの関数に変えてみる。次のようになる。

map.hs
module Main where
        squareAll list = map square list
                where square x = x * x

 まず、listという引数をとる関数squareAllを宣言する。次にmapを使用して、squareという関数をlist内のすべての項に適用する。そして、新しい機能であるwhereを用いて、squareのローカル版を宣言する。関数をwhereにバインドする必要はなく、任意の変数をバインドすることも可能である。
 これを実行すると、

Prelude> :load map.hs
[1 of 1] Compiling Main             ( map.hs, interpreted )
Ok, modules loaded: Main.
*Main> squareAll [1, 2, 3]
[1,4,9]

 セクションと呼ばれる部分的な関数の一種と共にmapを使用することも出来るようである。

Prelude> map (+ 1) [1, 2, 3]
[2,3,4]

 (+ 1)は部分適用関数である。+関数は2つの引数をとるが、ここでは1つの引数しか与えられていない。その結果、その結果、(x + 1)のように単一の引数xをとる関数が得られる。
 +関数という言い方(符号じゃないのか!?)にも、少し引っかかる。部分適用関数は昨日の記事のコメントでも議論されていたものと同じなのでしょうか?
 普段(1 + 1)のように書くところを(+ 1)と書くことで、2つの引数の内の1つだけを指定して、(x + 1)のように1つの引数をと部分適用関数になったという認識で良いのだろうか?

filter, foldl, foldr

 次によく使用する関数としてfilterがある。filterは、リスト内の要素にテストを適用する。次に例を示す。

Prelude> odd 5
True
Prelude> filter odd [1, 2, 3, 4, 5]
[1,3,5]

 odd関数は、奇数を判定する。filterによって、リスト内の要素でodd関数がTrueの要素をリストで返している。

 次に、foldlとfoldrの使い方を見ていく。これは、ClojureやScalaと同じようである。

Prelude> foldl (\x carryOver -> carryOver + x) 0 [1..10]
55
Prelude> foldl1 (+) [1 .. 3]
6

 1行目では、キャリーオーバーの初期値を0として、リストの各要素に関数を適用している。その際、関数が返す結果をcarryOver引数として、リストの各要素をもう1つの引数として与える。
 2行目は、畳み込みのもう1つの例である。この例では、+演算子を、2つの引数をとり整数値を返す純粋な関数として用いている。foldr1を使用すれば、右から左に畳み込むことも可能であるようだ。

部分適用関数とカリー化

 前のセクションで説明した関数の合成と部分適用関数の概念はHaskellにとって重要かつ中心となる部分のようなので、詳しく勉強する。
 Haskellのすべての関数は1つの引数をとるらしい。しかし、そこで疑問に思うのが「+のように2つの数値を加算する関数はどのようにして書くのか?」である。
 実際、Haskellのすべての関数は1つの引数をとるということを、型のシンタックスを単純化するため、prodという関数を作成して確認してみる。

Prelude> let prod x y = x * y
Prelude> prod 3 4
12
Prelude> :t prod
prod :: Num a => a -> a -> a

 関数の型を:tオプションによって確認した。Num a => の部分は、「以下の型記述で、aはNum(クラス)の型である」という意味である。残りの部分は、以前から登場してはいたが、正確な説明は省いていたので、この辺できちんと説明する。Haskellでは、ある概念を用いて、複数の引数をとる1つの関数を、1つの引数をとる複数の関数に分割する。これを実現するのが、部分適用という手法である。
 部分適用とは、全部ではなく一部の引数をバインドするという意味である。例えば、prodを部分適用して、別の関数を作成できる。

Prelude> let double = prod 2
Prelude> let triple = prod 3
Prelude> double 3
6
Prelude> triple 4
12

 まず、1, 2行目の関数の左辺に注目する。2引数の関数prodを定義しているが、最初の引数だけを与えている。prod 2の計算は簡単で、元の関数prod x y = x * yの x を 2 で置換すると、double y = 2 * y となる。実行結果を見ても期待通りに動いていることが確認できる。

 これで、先ほどの疑問は解けた。Haskellは、prod 2 4 を計算する時、実は(prod 2) 4 を計算しているのである。手順は次のとおりである。

  • 最初に、prod 2を実行して、関数(\y -> 2 * 4)を得る。
  • 次に、(\y -> 2 * y) 4, すなわち2 * 4 を実行して、8を得る。

 このプロセスはカリー化と呼ばれる。Haskellで複数の引数を取る関数は、カリー化されたものとして定義される。これにより、柔軟性が向上し、シンタックスは単純化される。ほとんどの場合は、カリー化されていてもいなくても、関数の値は同じなので、カリー化されていることを意識する必要は無いらしい。

遅延評価

 Haskellも(Clojureのシーケンスライブラリと同じように)遅延評価を多様しているらしい。遅延評価を用いると、無限リストを返す関数を作成できる。リスト構成子を使用して無限リストを作ることはよくあるらしい。無限範囲を作る例を見てみる。開始値はx, 増分はyである。

my_range.hs
module Main where
        myRange start step = start:(myRange (start + step) step)

 シンタックスは奇妙である。範囲の開始値と増分を引数に取るmyRangeという関数を作成している。リストの合成を使用して、ヘッドがstartで、テールが(myRange (start + step) step)のリストを作成している。myRange 1 1 を次々に評価していくと、次のような結果が得られる。

  • 1:myRange (2 1)
  • 1:2;myRange (3 1)
  • 1:2:3myRange (4 1)
  • 以降同様に続く。

 この再帰は無限に続くので、通常は、再帰を制限する他の関数と組み合わせて使用する。最初にmy_range.hsをロードするのを忘れないように注意である。

Prelude> :load my_range.hs 
[1 of 1] Compiling Main             ( my_range.hs, interpreted )
Ok, modules loaded: Main.
*Main> take 10 (myRange 10 1)
[10,11,12,13,14,15,16,17,18,19]
*Main> take 5 (myRange 0 5)
[0,5,10,15,20]

 一部の再帰関数はリスト合成構文を使用することで、より効率的に動作するようになる。以下に、リスト合成を用いて、フィボナッチ数列を作成する例を示す。

lazy_fib.hs
module Main where
        lazyFib x y = x:(lazyFib y (x + y))

        fib = lazyFib 1 1

        fibNth x = head (drop (x - 1) (take (x) fib))

 最初の関数は、全ての項が直前の2つの項の和となるような数列を作成している。これで数列はできたが、正式なフィボナッチ数列にするには数列を1と1で始める必要がある。そこで、fibはlazyFibに最初の2項を種として与える。最後に、もう1つヘルパー関数を用意する。この関数を使用すると、ユーザはdropとtakeを用いて数列の中の1つの項を取り出すことが出来る。それでは使用してみる。

Prelude> :load lazy_fib.hs 
[1 of 1] Compiling Main             ( lazy_fib.hs, interpreted )
Ok, modules loaded: Main.
*Main> take 5 (lazyFib 0 1)
[0,1,1,2,3]
*Main> take 5 (fib)
[1,1,2,3,5]
*Main> take 5 (drop 20 (lazyFib 0 1))
[6765,10946,17711,28657,46368]
*Main> fibNth 3
2
*Main> fibNth 6
8

 本書では、lazyFibを使用した時の実行結果がfibを使用した時の実行結果になっているがこれはtypoだと思われる。正しい実行結果は上記の通りである。
 また、3つの関数は美しく単純明快である。我々は無限数列を定義するだけで、Haskellは作業に必要な部分だけを計算する。複数の無限数列を組み合わせることができるようになると面白くなるらしい。手始めに、開始値を1つだけずらした2つのフィボナッチ数列(の各項)を足し合わせてみる。

*Main> take 5 (zipWith (+) fib (drop 1 fib))
[2,3,5,8,13]
*Main> take 5 (map (*2) [1..])
[2,4,6,8,10]
*Main> take 5 (map ((*2) . (*5)) fib)
[10,10,20,30,50]

 1行目では、zipWithを呼んで、無限リストの(同じインデックスの)各項同士を綴じ合わせてペアを作り、それを+関数に渡している。
 2行目では、mapを用いて部分適用関数*2を無限範囲[1..]に適用し、出来た無限範囲の先頭から5項を返している。
 3行目を内側から見ていく。(*5)は部分適用関数であり、この関数に何かを渡すと、5倍にされて返ってくる。その結果を別の部分適用関数(*2)に渡す。こうして出来た合成関数をmapに渡して、それをさらに無限フィボナッチ数列fibの各項に適用する。最後に、この無限数列をtake 5 に渡す。結果として、フィボナッチ数列の各項を5倍した後、更に2倍してできた数列の先頭の5項が返される。

2日目で学んだこと

 2日目の内容をバババーっとやったが、難しい内容もあるため復習は必須だと思われる。研究もあるため、次回更新(Haskell 3日目)は、09/08を予定している。

4
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4