Haskell入門ハンズオン! #5 - 当日用資料 (1/5)
はじめに
この記事は2019.2.2 Sat.のHaskell入門ハンズオン! #5の当日用の資料です。基本的にスライドの内容とおなじ内容になっています。
講師紹介
「Haskell - 教養としての関数型プログラミング」の著者。
コンピュータとの出会い
コンピュータとの出会いはMSX。入部したコンピュータクラブに何台かMSXが置いてあった。
MSXとは
2006年にMacがPowerPCからPC/AT互換機になってから、一般的なパソコンは、ほぼPC/AT互換機になった。それよりも、ずっとむかし、パソコンのアーキテクチャが群雄割拠だった時代(1980年代)、マイクロソフトとアスキーが提唱したパソコンのアーキテクチャ(1983)。時代の波に乗れずに消えていってしまった。MSXにはROM上にBASICが内蔵されていた。BASIC言語によるプログラミングは、つぎのような感じ(Wikipediaより)。
10 REM 5つ数える("3"だけ飛ばす)
20 FOR I = 1 TO 5
25 IF I = 3 THEN GOTO 40
30 PRINT I
40 NEXT
RUN
基本的にGOTO文を多用する。行番号を指定して、そこに飛ぶというやりかた。
言語、OSなど
コンピュータクラブに所属していたころは、BASICを(MSXの台数が不足していたので)紙に書いていた。
大学時代にPerlに出会う。ホームページにアクセスカウンターや掲示板をつけたかったので。その後、C言語、Ruby、Python、Common Lisp、Schemeなどなどに手を出した。青木峰郎「Rubyソースコード完全解説」の「(Rubyのソースコードは)少なくともHaskellやPL/Iで書いてあるわけではないので一般人にも読める可能性が高い」という一文から、興味をもち「Haskell: The Craft of Functional Programming」を購入。Haskellの魅力にはまる。以降はHaskell一筋だ。
だいたい、つぎのようになる。
- Haskell歴 16年
- Gentoo歴 17年
- Vim派
- 漢字直接入力(TUT-code)
- シェルはZsh
- キーボードはHHKB
コーヒー
最近は、すこしだけ、コーヒーにこっている。毎朝、エスプレッソを抽出してカフェラテを飲んでいる。
焙煎用の網で自家焙煎もしている。
Haskellについて
Haskellは楽しい
- Haskellを学ぶのは楽しい。
- Haskellでコードを書くのは楽しい。
- Haskellでコードを書くとき、安心感がある。
Haskellを学ぶ理由として、それだけでも十分かなと思う。Haskellは手になじむ道具だ。実際にあるていど学ぶことで、その楽しさや安心感がわかると思う。「Haskellのよさ」はこの感覚だ。この感覚をつたえることは難しい。でも、不十分ながらもHaskellの何がいいのかを説明してみよう。
Haskellの、どこがいいのだろうか。
- 安心感
- 適切な抽象化による二重化の回避
- ムダのない文法
そのあたりだろうか。
Haskellは、どんな言語?
Haskellは、どんな言語だろうか。実際に書くまえに、言語を紹介するのはむずかしい。ここでは「手続き」と「関数」というキーワードで説明していく。
- 手続き: メモリなどの「状態」を変化させるもの
- 「手続き」は「実行」されて、結果として状態が変化する
- 関数: 「引数」をあたえられて「式」になる
- 「式」は「評価」されて「値」になる
多くの言語で「手続き」と「関数」は、わけられていない。たとえば、つぎのような(Rubyの)コードを考える。
$number = 123
def add3(x)
$number += 321
return(x + 3)
end
このメソッドadd3は「状態を変化させる」ので「手続き」であると考えられる。しかし、add3(5)は評価されて8になるという点で、このメソッドは「関数」であるとも言える。
このように、「手続き」のようでもあり「関数」のようでもあるナニカ。そのナニカが、ほとんどの言語において、処理の記述に使われている。このナニカの問題は「関数」とはちがって、単純な「置き換え」で理解できないことだ。コードのなかのadd3(5)を8に置き換えることはできない。これは「コードが何を意味するのか」を簡単には把握できないことを意味する。
Haskellでは「関数」と「手続き」とは、きちんとわけられている。「関数」は状態を変化させないし、状態の影響をうけない。「手続き」は「関数」の引数になり、返り値にもなる。つまり、Haskellでは「手続き」は、ほかの「値」とおなじように、関数によって処理されるモノだ。
ほかの言語では「状態変化」が頻繁に使われる。「状態」は時間の関数であり、タイミングによって変化してしまう、あつかいづらいものだ。必要のないところで「状態変化」を使うことは、ムダに話を複雑にしてしまう。
とはいえ、Haskellの何がいいのかを説明するのは
Haskellのよさを言葉にするのはむずかしい。実際にコードを書くまで、わからないところがある。Haskellを学ぶことで、ほかの言語を使ったとしても、よりきれいで安全な書きかたをするクセがつく。高度な抽象化が可能なので、コードの二重化などを避けることができる。ムダのない気持ちのいいコードが書ける。
実際に使われてるの?
Haskellは、いろいろなところで使われている。たとえば、以外のような会社で使われている。
- Facebook (SNS)
- NTTデータ (データ通信、システム構築)
- ASAHIネット (プロバイダ)
- Tsuru Capital (高頻度取引)
それぞれ、つぎのような用途で使っている。
- スパム対策
- COBOL資産の分析
- 認証サーバ
- 全般
「既存のライブラリを使えばすむ」のではなく、それぞれの問題にあわせた解決が必要なとき、「複雑さ」を制御しながら解を組み立てていく。そのような場面がHaskellにむいていると感じる。また、Tsuru Capitalでは「Haskellを使えば優秀な人材が集まる」といった戦略もあった、とか。これは、勉強会を主催するうえで、僕自身が感じていることでもある。
このハンズオンについて
このハンズオンでは、実際にHaskellのコードを動かしてみる。はじめのうちは、対話環境で式の打ち込みと結果の表示を試す。
また、演習として課題のコードを書いてもらい模範解答の解説もしようと思う。
あとのほうで、独立して動作するアプリケーションを作ってみる。最後に自分達で考えたプログラムを作ってもらうことになる。
時間に限りがあるので、すべてを説明することはできない。「できるだけ理解しやすいように、また、自分でちょっとしたコードが書けるように」と考えた。Haskellは「難しいこと」をするときに、本当の力を発揮する。なのでHaskellの魅力を十分にはつたえられていないかもしれない。
対話環境
「値」「関数」「型」について学ぶ。ここでは、対話環境に値を打ち込みながら学んでいく。
ディレクトリの用意
バージョンのちがいなどを避けるために、こちらで用意したディレクトリを使用する。gitが使える人は、つぎのようにする。
% git clone https://github.com/YoshikuniJujo/haskell-nyumon-handson-work.git
%はプロンプトを意味する。入力しない。gitが使えない人は、つぎのアドレスからZIP圧縮されたファイルをダウンロードして解凍する。
https://bit.ly/2RUIS2E
それぞれのOSのやりかたで解凍する。Unix系OSの例を示す。
% unzip haskell-nyumon-handson-work-master.zip
それぞれのOSのやりかたで名前を含える。
% mv haskell-nyumon-handson-work-master haskell-nyumon-handson-work
(それぞれのOSのやりかたで)ディレクトリを移動する。
% cd haskell-nyumon-handson-work/work-d20190202
対話環境は、つぎのようにして立ち上げる。
% stack ghci
(何行かのメッセージ)
*Main Lib>
%はZshでは標準的なプロンプト。入力しない。*Main Lib>は対話環境のプロンプト、これからは省略して'>'のみで示すことにする。
値と型
とりあえず、対話環境に「値」を打ち込んでみよう。
> 123
123
> 'c'
'c
> True
True
値には「型」がある。
> :type 'c'
'c' :: Char
> :type True
True :: Bool
四則演算もできる。
> 3 + 5
8
> 12 - 7
5
> 3 * 12
36
> 38 `div` 4
9
> 38 / 4
9.5
整数のわり算と実数のわり算とは、意味がちがうので別になっている。
対話環境では直前の結果を、変数itによって再利用できる。
> 12345 * 67890
838102050
> it * 5
4190510250
変数it以外の変数も、明示的に書けば使える。
> x = 123
> x * 2
246
> name = "taro"
> name
"taro"
変数に値をわりあてることを「[値]が[変数]を束縛する」という。ほかの言語を使える人なら、「[値]を[変数]に代入する」とおなじ意味であると考えれば(ここでは)いい。
関数と型
関数は引数をとる。引数をとった関数は値に評価される。
> :module Data.Char
> toUpper 'c'
'C'
関数toUpperは引数を大文字にする関数。モジュールData.Charから公開されている。関数にも型がある。
> :type toUpper
toUpper :: Char -> Char
Char型の引数をとって、Char型の値をかえす。関数の型は、引数と返り値の型を(->)でつないだもの。もうひとつの例。
> isUpper 'c'
False
関数isUpperは引数が大文字ならばTrueを、そうでなければFalseをかえす。型をみてみよう。
> :type isUpper
isUpper :: Char -> Bool
Char型の引数をとって、Bool型の値をかえす。
関数定義
関数を自分で定義するには、つぎのように書く。
[関数名] [引数1] [引数2] ... = [式]
実際に定義してみよう。
> f x y = x + y
> f 3 4
7
関数をファイルから読み込むこともできる。
% vim samples/simpleFun.hs
g x y = x + y
ファイルに定義した関数は:loadで読み込める。
> :load samples/simpleFun.hs
> g 3 4
7
関数には型宣言をつけておいたほうがいい。
% vim samples/simpleFun.hs
g :: Integer -> Integer -> Integer
g x y = x + y
型宣言は、つぎのようになる。
変数 :: 型
また、型IntegerはHaskellでの標準的な多倍長整数型。関数の型は、つぎのように表される。
引数1の型 -> 引数2の型 -> ... -> 返り値の型
型注釈
型宣言は「変数の型」を宣言する。よって、変数を束縛しない「値」の宣言はできない。「値(式)」の型を特定したいときには「型注釈」を使う。ちゃんと理解するには「型クラス」を学ぶ必要があるが、Haskellでの数値リテラルは「多相的」だ。なので、必要に応じて、型注釈をつけて型を特定する。
> 123 :: Integer
123
> :type it
it :: Integer
> 123 :: Double
123.0
> :type it
it :: Double
型Doubleは代表的な実数型。
関数リテラル
3をたす関数を考える。
> add3 n = n + 3
> add3 8
11
「3をたす関数」が変数add3を束縛している。その「3をたす関数」そのものを表すには、つぎのようにする。
\n -> n + 3
やってみよう。
> add3 = \n -> n + 3
> add3 8
11
一般的に、関数リテラルは、つぎのように書ける。
\引数1 引数2 ... -> 式
つぎのふたつの定義は、おなじことだ。
関数名 引数1 引数2 ... = 式
関数名 = \引数1 引数2 ... -> 式
うえのかたちは、したのかたちの構文糖と考えられる。関数リテラルは変数を束縛せずに使える。
> (\n -> n + 3) 8
11
演算子
Haskellでは関数と演算子のちがいは構文だけだ。()と``でたがいに変換できる。
> mod 15 4
3
> 15 `mod` 4
3
> 12 + 25
37
> (+) 12 25
37
演算子を関数に変換するとき、前か後ろのどちらかの引数を、あらかじめ適用しておくことができる。これを、演算子の部分適用という。
> (/ 5) 65
13.0
> (65 /) 5
13.0
(/ 5)は何かを5でわる関数になり、(65 /)は65を何かでわる関数になる。
ガード
関数を定義するときなどに、条件によって結果を変えることができる。ガードという構文が使える。
% vim samples/guard.hs
div2 :: Integer -> Integer
div2 n | even n = n `div` 2
| otherwise = n
ガードは、つぎのような構文になる。
| (Bool値をかえす式) = (結果の式)
ガードは関数定義のときに複数つけられる。otherwiseはTrueということ。つまり、「それ以外の場合」を受け入れる。関数evenは引数が偶数のときTrueをかえす。試してみる。
> :load samples/guard.hs
> div2 14
7
> div2 19
19
Bool値をかえす関数
Bool値をかえす関数を述語とよぶ。
> 12 == 12
True
> 3 == 9
False
> 3 < 15
True
> 3 >= 15
False
いくつかの述語をみた。
if式
真偽値によって処理を分岐させるにはガードが使える。やっていることは、おなじだが、構文的に異なる特徴をもつのがif式。
> if 12 == 12 then "good" else "bad"
"good"
> if 3 >= 15 then "foo" else "bar"
"bar"
真偽値に対する演算
真偽値に対する演算として、つぎのみっつがある。
(&&), (||), not
> True && True
True
> True && False
False
> True || False
True
> False || False
False
> not True
False
それぞれ「かつ」「または」「...でない」。
let式
局所的に変数を束縛することができる。
> let x = 8 in x + 5
13
関数適用演算子
Haskellには関数を適用する演算子($)がある。
> :module Data.Char
> toUpper 'x'
'X'
> toUpper $ 'x'
'X'
「いったい何の意味が?!!!」
ひとつには「結合力」がある。演算子($)は結合力が最弱なので
f $ ...
のように書かれていたとき、...の部分がひとかたまりで、その全体に関数fを適用するということが、すぐにわかる。試してみよう。
> negate 3 + 5
2
> negate $ 3 + 5
-8
関数show
対話環境で評価の結果を「表示」してきた。「表示」には内部では「文字列化」関数showが使われている。関数showは「表示可能」な値を「文字列」に変換する。
> 123
123
> show 123
"123"
ふたつめでは、showによって表示用の文字列に変換された文字列が、さらに、暗黙のshowで、表示用に変換された。
エラーを発生させる
未定義を意味する値undefinedがある。コードを書いている途中で、「あとで定義しよう」というときに使える。
> undefined
*** Exception: Prelude.undefined
...
値undefinedを評価しようとすると例外が発生する。メッセージを指定して例外を発生させたいときもある。
> error "Oh, my god!"
*** Exception: Oh, my god!
...
なんらかの理由で定義できない値について、説明つきのエラーを発生させるのに使える。
再帰関数
関数を定義するときに定義のなかに、その関数自体が出てくる。そのような関数を再帰関数と呼ぶ。
0からnまでの整数の和を計算する関数。
% vim samples/rec.hs
sum123 :: Integer -> Integer
sum123 0 = 0
sum123 n = sum123 (n - 1) + n
対話環境で試してみよう。
> :load samples/rec.hs
> sum123 8
36
> sum123 100
5050
定義を再掲する。
sum123 0 = 0
sum123 n = sum123 (n - 1) + n
n = 3について展開してみよう。
sum123 3
=> sum123 2 + 3
=> (sum123 1 + 2) + 3
=> ((sum123 0 + 1) + 1) + 2) + 3
=> ((0 + 1) + 2) + 3
=> 6
ここまでの、まとめ
値、型、関数、再帰関数について学んだ。値には型がある。型は文字型Char、整数型Integer、真偽値型Boolなど。引数をとって返り値をかえすのが関数だ。関数にも型がある。
[引数1の型] -> [引数2の型] -> ... -> [返り値の型]
ファイルに定義した関数を読み込むには:loadコマンド。
定義に自分自身を使うのが再帰関数。例として、0からnまでの和を求める関数を示した。再帰関数を使えば「くりかえし」を表現できる。
タプル、Maybe値、リスト
Haskellで、よく使われるデータ構造を学ぶ。
- タプル
- Maybe値
- リスト
タプル
ふたつの値をペアにしたいときに使う。
> (123, 'c')
(123,'c')
ペアにした値の、それぞれを取り出したいときは、パターンマッチを使う。
> f (n, c) = 2 * n
> f (123, 'c')
246
型宣言は、つぎのようになる。
p :: (Integer, Char)
p = (123, 'c')
Maybe値
値が「ない」かもしれないときに使う。
% vim samples/maybe.hs
safeDiv :: Integer -> Integer -> Maybe Integer
safeDiv _ 0 = Nothing
safeDiv a b = Just (a `div` b)
'_'(アンダースコア)は、引数を使用しないときに使う。「ワイルドカード」と呼ぶ。関数divは整数の除算を行う。``(バッククォート)でかこむと関数は演算子になる。除数が0のとき結果はない(Nothing)。そうでないときは、結果をJustでくるむ。試してみよう。
> :load samples/maybe.hs
> safeDiv 35 4
Just 8
> safeDiv 15 0
Nothing
Maybe値のなかみを使いたいときは、パターンマッチを使う。
% vim samples/maybe.hs
addToMaybe :: Maybe Integer -> Integer -> Maybe Integer
addToMaybe Nothing _ = Nothing
addToMaybe (Just a) b = Just (a + b)
Nothingや(Just a)によってMaybe値をパターンマッチしている。パターンマッチはうえから順に試されていく。マッチしたパターンに含まれる変数が、なかみの値に束縛される。試してみる。
> :reload
> addToMaybe (safeDiv 35 4) 7
Just 15
リスト
おなじ型の値がたくさん必要なとき、リストを使う。
> [123, 456, 789]
[123,456,789]
リストを操作するにはパターンマッチと再帰を使う。
% vim samples/list.hs
sumAll :: [Integer] -> Integer
sumAll [] = 0
sumAll (n : ns) = n + sumAll ns
リストは(n : ns)のようなパターンで、(先頭の値 : 残りの値)のようにわけられる。要素をもたないリストは[]で表される。要素をもたないリストの総和は0。先頭がnで残りがnsであるリストの総和は、値nにリストnsの総和をたしたもの。試してみる。
> :load samples/list.hs
> sumAll [5, 7, 9, 2]
23
> sumAll [123, 456, 789]
1368
> sumAll [2, 4, 5]
11
定義を再掲する。
sumAll [] = 0
sumAll (n : ns) = n + sumAll ns
展開してみよう。
sumAll [2, 4, 5]
=> 2 + sumAll [4, 5]
=> 2 + (4 + sumAll [5])
=> 2 + (4 + (5 + sumAll []))
=> 2 + (4 + (5 + 0))
=> 11
文字列
Haskellでは、デフォルトの文字列は文字のリストだ。文字列は、つぎのように表記される。
"foobar"
これは、つぎのリストとおなじ。
['f', 'o', 'o', 'b', 'a', 'r']
たしかめてみよう。
> "foobar"
"foobar"
> ['f', 'o', 'o', 'b', 'a', 'r']
"foobar"
case式
パターンマッチを関数定義ではなく、式のなかに書くにはcase式を使う。
% vim samples/case.hs
import Data.Char
checkAnswer :: Char -> Maybe Bool
checkAnswer c = case toLower c of
'y' -> Just True
'n' -> Just False
_ -> Nothing
文字cをtoLowerで小文字化したものに対して、パターンマッチをおこなっている。試してみる。
> :load samples/case.hs
> checkAnswer 'y'
Just True
> checkAnswer 'N'
Just False
> checkAnswer 'p'
Nothing
タプル、Maybe値、リストのまとめ
Haskellで用意されているデータ構造を紹介した。
- タプル
- Maybe値
- リスト
なかみを取り出すにはパターンマッチを使う。リストのなかみをすべて利用するには、再帰関数を利用する。パターンマッチを「式のなか」でするには、case式を使う。