「Hskell入門ハンズオン #2」の当日用の資料(2)
関数適用演算子と関数合成演算子
関数適用演算子
関数適用演算子(\$)を紹介する。ここで、逆数をもとめる関数recipについて考える。
> recip 8
0.125
これは、「関数recipを数値8に適用した」ということだ。演算子(\$)は、第1引数の関数を第2引数の値に適用する。
> recip $ 8
0.125
「なぜ、こんな演算子が定義されているのだろう?」「必要ないのでは?」関数適用演算子(\$)のレーゾンデートル(存在意義)は?つぎの例をみてみよう。
> logBase 2 (recip (3 + 5))
-3.0
3 + 5の逆数が2の何等であるかをもとめている。このくらいならいいが、「逆数の対数の絶対値の...」のように、長々と空いていくことを考えよう。
abs (logBase 2 (recip (...)))
最後に閉じ括弧が...))))))))のように続くことになる。つぎの例をみてみよう。
> logBase 2 $ recip $ 3 + 5
-3.0
関数適用演算子(\$)なら...))))))))のようにはならない。関数の適用の連鎖が長々と続いたりする。
abs $ logBase 2 $ recip $ ...
丸括弧が入れ子になるよりスマートに書ける。
関数合成演算子
関数合成演算子を紹介する。小文字にして文字コードをもとめる関数の例でみていこう。
> :module Data.Char
> fun c = ord (toLower c)
> fun 'Y'
121
この関数funは関数ordとtoLowerとを、くっつけたものだ。このように「くっつける」ことを「関数合成」と呼ぶ。関数合成演算子(.)を使った関数fun2は、つぎのようになる。
> fun2 = ord . toLower
> fun2 'Y'
121
関数funよりもfun2のほうが、「小文字化して、文字コードをもとめる」という意味を、直接的に表現できている。
関数適用演算子を関数合成演算子に置き換える
つぎのように関数適用を2回以上使う例をみる。
> abs $ logBase 2 $ recip 8
3.0
これを、つぎのように書き換えることができる。
> abs . logBase 2 $ recip 8
3.0
結合力が演算子(.)のほうが(\$)よりも強いことに注意する。また、演算子(\$)は右結合だ。よって、それぞれについて、丸括弧を明示すると、つぎのようになる。
abs $ (logBase 2 $ recip 8)
(abs . logBase 2) $ recip 8
「8の逆数の対数をもとめて、絶対値をもとめる」のと、「8の逆数に、絶対値関数と対数関数を合成したものを適用する」のふたつは、おなじ意味になる。
演算子(\$)が連続したとき、1番右の(\$)以外は、すべて、演算子(.)に置き換えられる。逆に、演算子(\$)が一番右にあるとき、その左の演算子(.)の連続は、すべて、演算子(\$)に置き換えて、解釈できる。理屈を理解しなくても、つぎのような置き換えができることだけ、覚えておこう。
f $ g $ h $ i $ j $ ... $ z x
f . g . h . i . j . ... $ z x
個人的には前者のような、かたちがあらわれたら、後者のように書き換えることにしている。「見ため」がきれいだからだ。
いくつかの関数を学ぶ(1)
Haskellで、はじめから定義されている関数のうちのいくつかを紹介する。ここでは、つぎの7個を学ぶ。
- 関数map, filter, take, drop
- 関数replicate, reverse
- 演算子(++)
関数map
関数mapは、リストのすべてについて、おなじ変換をする。「変換」をあらわす関数を第1引数にする。
> double x = x * 2
> map double [1, 2, 3, 4, 5]
[2,4,6,8,10]
第1引数に「引数を2倍にする関数」をとり、リストのすべての要素に、それを適用している。
関数リテラル
ところで、関数mapの第1引数とするために、つぎのような関数doubleを定義した。
double x = x * 2
この関数doubleは、これ以降、使う予定はない。それなのに、いちいち名前をつけるのは、めんどうだ。このようなとき関数リテラル(無名関数)を使う。
> map (\x -> x * 2) [1, 2, 3, 4, 5]
[2,4,6,8,10]
関数リテラルの記法は、つぎのようになる。
\[仮引数1] [仮引数2] ... -> [式]
関数filter
リストから条件を満たす値だけ取り出す関数だ。リストから奇数だけを取り出してみる。
> filter odd [1, 2, 3, 4, 5]
[1,3,5]
第1引数である関数oddによって、奇数かどうかを判定する。
関数replicate
おなじ値をくりかえしたリストを作成する。
> replicate 10 123
[123,123,123,123,123,123,123,123,123,123]
整数値123を10回くりかえしたリストだ。
関数take, drop
リストの前のほうのいくつかを取り出す。
> take 3 [1, 2, 3, 4, 5]
[1,2,3]
リストの前のほうのいくつかを落とす。
> drop 3 [1, 2, 3, 4, 5]
[4,5]
それぞれ、前のほうの3つの要素、前のほうの3つ以外の要素が、取り出されている。
関数reverse
リストを逆順にする関数だ。
> reverse [1, 2, 3, 4, 5]
[5,4,3,2,1]
リストが逆順になった。
演算子(++)
ふたつのリストを結合する。
> [1, 2, 3] ++ [4, 5]
[1,2,3,4,5]
リストが結合された。Haskellでのデフォルトの文字列は文字のリストなので、文字列は演算子(++)で結合できる。
> "Hello" ++ "World"
"HelloWorld"
まとめ
ここまで紹介してきた関数(mapから演算子(++)まで)は、どれも自分で定義できる。定義は再帰的になる。時間の関係で、ここでは説明しない。これらの関数を使いこなせるようになろう。ここで学んだ関数は、つぎの7個である。
- 関数map, filter, replicate
- 関数take, drop
- 関数reverse, 演算子(++)
いくつかの関数を学ぶ(2)
さらに、つぎの4個の関数を学ぶ。
- 関数randoms, mkStdGen
- 関数unlines
- 関数read
関数randoms, mkStdGen
乱数のリストを生成する関数を紹介する。関数randomsなどは、現在では、標準的なパッケージには含まれていないので、いちど対話環境をぬけて、randomsパッケージを導入しながら対話環境を立ち上げる。
> :quit
% stack ghci --package random
モジュールSystem.Randomを導入する。
> :module System.Random
関数randomsは「乱数の種」を引数として、ランダムな値を要素とする無限リストをかえす。「乱数の種」は関数mkStdGenで整数から作れる。
> take 10 . randoms $ mkStdGen 8
[-398575370259562870,-6370604356117182359,
8399777519602674086,...
関数unlines
文字列を要素とするリストを、行の集わりと解釈して、それぞれの行のおわりに改行を追加した、ひとつの文字列としてかえす関数。
> unlines ["hello", "world"]
"hello\nworld\n"
helloやworldの、それぞれのうしろに\n(改行)が追加される。そのうえで、結合され、ひとつの文字列になる。
関数read
文字列を適切な値に変換する。
> read "12345" + 54321
66666
文字列"12345"が整数値12345に変換された。
入出力を学ぶ
関数
いろいろな関数をみてきた。関数に引数をあたえたものが値に評価される。たとえば、つぎのような計算を考える。
> 3 + 2 * 5
13
関数(演算子)である(+)や(*)に対して、整数値3, 2, 5をあたえることで、式3 + 2 * 5が作られ、それが値13に評価される。関数は引数をとり、それによって作られた式が評価される。関数にできることは、それだけだ。
動作
式を値に評価すること。たしかに、それができるのは、すばらしいこと。しかし、僕らがプログラミングでやりたいことは、それだけじゃない。ウェブサーバを作るにも、ゲームを作るにも、何らかの「動作」が必要になる。「純粋な関数型」のわくぐみで「動作」をあつかいたい。どう考えるか?基本になる「動作」を、関数によって組み合わせればいい。関数putStrLnは、文字列を表示する。
> putStrLn "hello"
hello
> putStrLn "world"
world
putStrLn "hello"は「helloと表示する動作」に評価される。対話環境は、評価の結果が「動作」になったとき、その動作を「実行」する。これらの動作を、「XのつぎにYを実行する」のように組み合わせる。
> putStrLn "hello" >> putStrLn "world"
hello
world
「基本になる動作」を関数によって組み合わせて、「複雑な動作」を組み立てていく。ファイルaction.hsに、つぎのように定義する。
hello = putStrLn "hello" >> putStrLn "world"
対話環境に読み込み、試してみる。
> :load action.hs
> hello
hello
world
動作を「関数」で組み合わせていくことで、複雑な動作を組み立てていくことができる。演算子(>>)で動作を組み合わせて、より複雑な動作を組み立てていく。もちろん、それは「いい考え」だ。しかし、たとえばレシピを考えてみよう。
鍋を火にかける
沸騰したらパスタを入れる
パスタが、やわらかくなったらザルにあげる
このように、順番に書かれた内容は、順番に実行されることが期待される。Haskellでは、明示的に演算子(>>)を書かなくてすむ「do記法」という書きかたが用意されている。do記法でaction.hsの関数helloを書き換えてみよう。
hello = do
putStrLn "hello"
putStrLn "world"
このように明示的な演算子(>>)を書くかわりに、動作を並べて書くことができる。注意する点は、列挙される動作は、予約語doのある行よりも深くインデントし、それぞれの「列挙される動作」どうしのインデントは「そろえる」ということだ。どうだろうか。「ふつうの言語」の書きかたに近くなったのではないだろうか。
入力
関数putStrLnによって作られる動作は「出力」だ。動作には「出力」だけでなく「入力」もある。たとえば、ユーザの打ち込んだ文字列を取り込みたい。そんなときは、「値をかえす動作」を使う。
> getLine
(helloと入力する)hello
"hello"
動作getLineはユーザからの入力を待ち、打ち込まれた文字列を動作からかえる値とする。対話環境は式が「値をかえす動作」に評価されたとき、その動作を実行し、さらに、かえされた値を表示する。かえされた値を使うことを考えよう。名前を入力すると、「Hello, 誰々!」のように、あいさつしてくれる動作をつくる。ファイルaction.hsに、つぎの関数を定義する。
greeting = do
name <- getLine
putStrLn $ "Hello, " ++ name ++ "!"
試してみる。
> :reload
> greeting
(自分の名前を入力)Yoshikuni
Hello, Yoshikuni!
つぎのように「<-」を使うことで、動作からかえされた値を変数に束縛できる。
var <- action
この変数varは、このあとに列挙される動作のなかで、使うことができる。
打ち込まれた文字列を数値に変換し、それを摂氏温度として、絶対温度に変換された値をかえす動作を考える。ファイルaction.hsに、つぎの関数を定義する。
getAbsolute = do
c <- getLine
return $ read c + 273
試してみる。
> :reload
> getAbsolute
(25と入力)25
298
動作getAbsoluteの最後の行では、returnという関数が使われている。関数returnは「何もせずに」引数の値をかえす動作を作る。この動作を最後に置くことで、動作getAbsoluteで、絶対温度の値をかえすことができる。
入出力に関する関数、動作
いくつかの入出力に関する関数、動作を学ぶ。ここで学ぶ関数、動作は、つぎの6個だ。
- 関数putStr, print, 動作getChar
- 動作getArgs
- 関数hGetBuffering, hSetBuffering
関数putStr
すでに関数putStrLnをみた。
> putStrLn "hello"
hello
関数putStrLnは引数である文字列を、標準出力に出力したあと、改行を出力する。関数putStrは改行を出力しない。
> putStr "hello"
hello>
改行が出力されないので、プロンプトが出力のあとに続く。
関数print
関数printは文字列ではなく「値」を表示する。
> print 123
123
値を文字列化する関数showがある。
> show 123
"123"
関数printは関数putStrLnとshowを結合したものだ。
> (putStrLn . show) 123
123
実は、対話環境は、評価した結果が動作でないとき、結果の値を関数printに引数としてあたえ、作られた「動作」を実行していた。よって、対話環境では、つぎのふたつの結果がおなじになる。
123
print 123
関数getChar
ユーザの打ち込んだ文字列を行単位で取得する関数getLineはすでにみた。ユーザの打ち込んだ文字を、ひとつだけ取得する関数getCharをみてみよう。
> getChar
(cと入力する)c'c'
動作getCharは入力された文字をかえす。かえされた文字は対話環境によって表示される。
動作getArgs
ターミナルから呼び出すアプリケーションを作っていると、コマンダライン引数を取得したくなる。動作getArgsを使う。ファイルaction.hsに、定義を追加する。
printArgs = do
as <- getArgs
print as
関数getArgsは、モジュールSystem.Environmentから公開されている。ファイルの先頭に、つぎのように追加する。
import System.Environment
モジュールを導入するには予約語importを使う。対話環境で試してみる。
> printArgs
[]
対話環境なので、コマンドライン引数は指定されていない。動作getArgsのかえすリストは空リストになる。対話環境のなかでコマンドライン引数を指定するには、コマンド:runを使う。
> :run printArgs hello world
["hello","world"]
バッファリング
バッファリングというものがある。バッファリングのモードを変えることで、一文字ずつ入力を行うのか、一行ずつ入力を行うのか、を変えることができる。現在のバッファリングのモードを確認する。
> :module System.IO
> hGetBuffering stdin
NoBuffering
現在のバッファリングのモードはNoBufferingである。つまり、入力は一文字ずつ処理される。
> getChar
(cと入力)c'c'
バッファリングのモードを変えるには、関数hSetBufferingを使う。
> hSetBuffering stdin LineBuffering
> hGetBuffering stdin
LineBuffering
> getChar
(c123と入力し改行を入力)c'c'
> 123
123
改行を入力するまで、文字'c'は読み込まれない。1文字読み込んだあとの文字列"123"は、対話環境への「つぎの入力」とされる。
モジュールと実行可能ファイル
プログラムを組みうえで、モジュール分割という考えかたは重要だ。Haskellでは1モジュール、1ファイルとなる。モジュールの作りかたをみる。また、単独で実行可能なファイルの作りかたをみる。
モジュール
適当なモジュールを作ってみる。つぎのようなファイルSome.hsを作る。
module Some where
fun x = x * x
対話環境に読み込む。
> :load Some.hs
*Some> fun 8
64
自分で作ったモジュールを、定義ファイルのなかで、使うことができる。ファイルother.hsを作成する。
import Some
f = fun . fun
対話環境で試してみよう。
> :load other.hs
> f 3
81
プロンプトに、そのモジュールの名前が表示される。
実行可能ファイル
実行可能ファイルを作るには、動作で「特別な変数main」を束縛する。つぎのようなファイルhello.hsを作る。
main = putStrLn "Hello, world!"
つぎのようなコマンドを実行する。
% stack ghc -- hello.hs -o hello
すると、実行可能ファイルhelloが作成される。
% ./hello
Hello, world!