Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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へ

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away