Edited at

Haskell入門ハンズオン! #5 - 当日用資料 (1/5)


Haskell入門ハンズオン! #5 - 当日用資料 (1/5)


はじめに

この記事は2019.2.2 Sat.のHaskell入門ハンズオン! #5の当日用の資料です。基本的にスライドの内容とおなじ内容になっています。


講師紹介

「Haskell - 教養としての関数型プログラミング」の著者。

cover.png


コンピュータとの出会い

コンピュータとの出会いはMSX。入部したコンピュータクラブに何台かMSXが置いてあった。

msx.JPG


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

hhkb2_croped.png


コーヒー

最近は、すこしだけ、コーヒーにこっている。毎朝、エスプレッソを抽出してカフェラテを飲んでいる。

cafelatte_croped.jpg

焙煎用の網で自家焙煎もしている。

coffee_beans_croped.jpg


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


samples/simpleFun.hs

g x y = x + y


ファイルに定義した関数は:loadで読み込める。

> :load samples/simpleFun.hs

> g 3 4
7

関数には型宣言をつけておいたほうがいい。

% vim samples/simpleFun.hs


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


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


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


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


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


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


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式を使う。

その2へ