代数プログラミング入門 Ch3 Haskellを使ってみよう
1 Haskellを使ってみよう
1.1 ghci
前節では, Stackを利用した,プロジェクトの作成と実行に関して扱いましたが, Haskellにも対話環境が存在します.
stack ghci
コマンドを打つことで, Haskellの対話環境(REPL
)が立ち上がります.
この節では,Haskellの基礎について学びますが,ghciの紹介も併せて,いくつかの基礎的な仕様については,ghci上で確認してみましょう.
1.2 終了
ghciではコマンドを:
の後に入力します. ghciの終了コマンドは:q
です.
❯ stack ghci
ghci>:q
Leaving GHCi.
1.3 コメントアウト
Haslellではコメントアウトは --
です. 複数行に渡る場合は {- -}
で囲みます.
Haskellのプログラムを読んでいると --|
や --^
というタイプのコメントを良く見ますが, こちらはHaskellのドキュメント生成ライブラリにおいて, ドキュメント中に説明として記述するための記号です.
またコメント中に >>>
と記述することでテストが実装できるなどいろいろなものがありますが,本資料では扱いません.
> -- コメント
ghci> {- コメント-} ghci
1.4 複数行モード
ghci上で複数行のプログラムを書く場合には :{ :}
でプログラムを囲います. 例えば,先程のフィボナッチ数のプログラムをghci上で実行する場合,位置行ずつ定義すると,定義が更新されてき最後の f n = f (n-1) + f (n-2)
のみが記憶されます. この場合,n
は無限にマイナスに続いていくため,Stack Overflow
エラーが表示されます.
> fib 0 = 1 -- fの定義が上書きされる
ghci> fib 1 = 1 -- fの定義が上書きされる
ghci> f n = f (n-1) + f (n-2)
ghci> f 12
ghci*** Exception: stack overflow
:{ :}
で囲むことでひとまとまりの定義として認識されます.
> :{
ghci| fib :: Int -> Int
ghci| fib 0 = 1
ghci| fib 1 = 1
ghci| fib n = fib (n-1) + fib (n-2)
ghci| :}
ghci> fib 12
ghci233
なお,スクリプトの場合は,:{ :}
なしでそのまま改行すれば問題ありません.
1.5 データ型
型に関しては,かなり奥が深い,というよりHaskellの面白さは自分で型を作っていくことにあります. ただ,いきなりそれをすると,わけがわからなくなるのでまずは代数的データ型などには触れず以下の基礎的な型に関して説明します.
- 数値型
- 整数 (Int, Integer)
- 実数 (Float,Double)
- タプル
- リスト (List)
- 文字,文字列 (Char,String,Text)
- 論理型(Bool)
Haskellにおいて,値のデータ型はある程度自動推論されますが,特定のデータ型を明示したい場合には,値の後ろに:: データ型
をつけます.
> 1 :: Int
ghci1
> 1 :: Double
ghci1.0
ghci
において形の確認は:t
あるいは:type
コマンドの後ろに確認したいデータを入力することで行えます.
> :t 'c'
ghci'c' :: Char
> :type (1 :: Int)
ghci1 :: Int) :: Int (
1.5.1 数値型
Haskellの基本的な数値型には以下の4つがあります. クラスに関しては後に扱うので,今はデータ型の更に大きな分類程度に考えておいてください.
クラス | データ型 | 意味 |
---|---|---|
Integer (整数) | Int |
固定長整数型 |
Integer (整数) | Integer |
多倍長整数型 |
Fractional (小数) | Float |
単精度浮動小数型 |
Fractional (小数) | Double |
倍精度浮動小数型 |
Int
とInteger
は整数
, Float
とDouble
は実数
を表しています.
固定長/多倍長
, 単精度/倍精度
というのはどういう意味でしょうか?
コンピューターでは,データはすべて0
と1
のいずれかを表すbit
の集まりによって表現されます. ちなみに8bit
で1byte
, 1024byte
で1Kbyte
です.したがって,プログラミングで扱うデータに使用できるデータ量には制限があり,無限の長さの整数や少数を利用することはできません.
コンピューターの計算は主に中央演算処理装置(CPU)で行われますが,その計算過程でデータを一時的に記録するCPU内部の装置のことを汎用レジスタといい,現在では64bit
以下の汎用レジスタを持った64bit CPU
が良く利用されています.
現在一般的な64bit CPU
においてHaskellは整数と小数を表すのに一般的に最大64bit
の領域を確保します. したがって,整数では64bitで表せるデータ量(-9223372036854775808 ~ 9223372036854775807
)を超えた整数を扱うことはできません.
ちなみにIntの最大値,最小値はghciで以下のように確認できます( 使用しているコンピューターによっては結果が変わる可能性があります).
> minBound :: Int
ghci-9223372036854775808
> maxBound :: Int
ghci9223372036854775807
最大(小)値を超えるとオーバーフローします.
> 9223372036854775807 :: Int
ghci9223372036854775807
> (9223372036854775807 + 1) :: Int
ghci-9223372036854775808
1.5.2 数値型の演算
Haskellにおける数値型の基本的な演算子は以下のように定義されています. 実数と整数で挙動が異なるものがあるので注意が必要です.
演算子には優先順位が設定されており,数字が大きいものから順に適用されます(最小0,最大9).
また,式を()
で囲むことで,その内部が優先的に計算されます.
また,()
が式の最後に来る場合には$
記号以下が()
に囲まれているとみなすことができます.
計算 | 記号 | 優先順 |
---|---|---|
足し算 | + |
6 |
引き算 | - |
6 |
掛け算 | * |
7 |
割り算 | / |
7 |
冪乗(整数) | ^ |
8 |
冪乗(実数) | ** |
8 |
> 1 + 1
ghci2
> 2 - 1
ghci1
> 3 * 3
ghci9
> 9 / 3
ghci3.0
> 3 ^ 3
ghci27
> 3 ** 3
ghci27.0
> 3 ^ (1/2) -- エラー
ghci
<interactive>:7:3: error: [GHC-39999]
> 3 ** (1/2)
ghci1.7320508075688772
> 2 * 3 + 1
ghci7
> 2 * (3 + 1)
ghci8
> 2 * $ 3 + 1
ghci8
これらは中置演算子として定義されていますが演算子を()
で囲むことによって前置(逆ポーランド記法)で利用することができます.
> (+) 3 4
ghci7
> (*) ((+) 3 4) 2
ghci14
> (*) 2 $ (+) 3 4
ghci14
また, 2引数関数として定義された前置の演算子は ``
(バッククオート)で囲むことで, 中置演算子として利用できます.
計算 | 記号 | 優先順 |
---|---|---|
整数除算 | div |
7 |
剰余 | mod |
6 |
> 5 /2
ghci2.5
> div 5 2
ghci2
> 5 `div` 2
ghci2
> 5 `mod` 2
ghci1
1.5.3 数値型の変換
Integral(整数)
からFractional(小数)
への変換は, fromIntegral
を利用します.
ghci> fromIntegral (1 :: Int) :: Double
1.0
ghci> div 5 2
2
ghci> 2 ** (div 5 2)
<interactive>:6:1: error: [GHC-39999]
• Ambiguous type variable ‘a0’ arising from a use of ‘print’
prevents the constraint ‘(Show a0)’ from being solved.
Probable fix: use a type annotation to specify what ‘a0’ should be.
Potentially matching instances:
instance [safe] Show Version -- Defined in ‘Data.Version’
instance Show Exception.ArithException
-- Defined in ‘GHC.Exception.Type’
...plus 39 others
...plus 20 instances involving out-of-scope types
(use -fprint-potential-instances to see them all)
• In a stmt of an interactive GHCi command: print it
ghci> 2 ** (fromIntegral (div 5 2))
4.0
Fractional(小数)
からIntegral(整数)
への変換は,基本的に何かしらの切り捨てを実施します.
切り捨て関数名 | 意味 | 例 |
---|---|---|
ceiling | 小数点以下を切り上げる | ceiling 3.2 → 4 , ceiling (-3.2) → -3 |
floor | 小数点以下を切り下げる | floor 3.8 → 3 , floor (-3.8) → -4 |
truncate | 小数部分を単純に切り捨てる | truncate 3.8 → 3 , truncate (-3.8) → -3 |
round | 最も近い整数に丸める | round 3.5 → 4 , round 3.4 → 3 , round (-3.5) → -4 |
> 2 ^ (2 / 1)
ghci
<interactive>:8:3: error: [GHC-39999]
Could not deduce ‘Integral b0’ arising from a use of ‘^’
• --- 省略
> 2 ^ (truncate (2 / 1))
ghci4
練習問題
以下の問題をREPLを使って自分で解いてみましょう. 問題自体は小学生でも解けますが,重要なのはHaskellの挙動を確認することです. どのように計算したかを併せて説明してください.
飴が40個あります.7人で同じ数ずつ分けると1人分は何個で何個あまりますか?
底辺5cm,高さ4cmの三角形の面積はいくつですか?
2の8乗はいくつですか?
累乗と掛け算の計算順序を丸括弧を使った計算で確かめてください.
1.5.4 リスト
複数のデータをまとめる方法はいくつかありますが,データを1列に並べたList
型は代表的なデータ型です. Haskellには配列(Array
やVector
)もありますが,まずはList
について学習しましょう.
リストの操作にはここで扱う以外にもリスト内包表記
や高階関数
など様々なものがありますが,ここでは最も基本的ないくつかの機能のみに絞って,後ほど詳細を扱います.
Listはリストリテラル[]
の中に要素を記入して,,
(コンマ)で区切ることで宣言できます.
Haskellにおいて,リテラルとは,特定のデータ型の値を直接記述する構文のことを指します.
リストリテラル
[]
は,[]
内の記述をリスト型として扱うリテラル数値を記入するとそれは数値型として扱われる数値リテラル
""
で囲まれた記述は文字列型として扱われる文字列リテラル
などがあります.
Haskellでは,自作したデータ型にリテラルを定めるなど様々な用法がありますが,ここでは扱いません.
> [1,2,3]
ghci1,2,3] [
[]
のみで空のリストが生成されます.
> []
ghci []
注意点として,HaskellはPythonなどの言語のようにダックタイピング
が許されていないため異なるデータを単一のリストの要素に含めることはできません.
> [1,2.0,3]
ghci1.0,2.0,3.0]
[> [1::Int,2::Double,3::Int]
ghci
<interactive>:22:9: error: [GHC-83865]
Couldn't match expected type ‘Int’ with actual type ‘Double’
• In the expression: 2 :: Double
• In the expression: [1 :: Int, 2 :: Double, 3 :: Int]
In an equation for ‘it’: it = [1 :: Int, 2 :: Double, 3 :: Int]
リストのデータ型は,要素のデータ型をリストリテラル[]
で囲んだ形で表されます.
> :type ([1::Int,2::Int])
ghci1::Int,2::Int]) :: [Int]
([> :type ['a','b','c']
ghci'a','b','c'] :: [Char] [
また,リストは先頭要素 : リスト
によって宣言することも可能です. :
をcons 構築子
といいます. 構築子の意味については後ほど代数的データ型
の説明とともに扱います.
> 1 : []
ghci1]
[> 1 : [2,3]
ghci1,2,3]
[> 1 : 2 : [3]
ghci1,2,3] [
リストの要素のインデックスによる取得は !!
演算子を用いてxs !! インデックス
の形で行います. インデックスは0から始まります. インデックスが超過した場合はエラーとなります.
> [1,2,3] !! 0
ghci1
> [1,2,3] !! 1
ghci2
> [1,2,3] !! 2
ghci3
> [1,2,3] !! 3
ghci*** Exception: Prelude.!!: index too large
CallStack (from HasCallStack):
error, called at libraries/base/GHC/List.hs:1366:14 in base:GHC.List
/base/GHC/List.hs:1376:50 in base:GHC.List
tooLarge, called at libraries!!, called at <interactive>:37:9 in interactive:Ghci15
m~n
までの連続したリストを生成する場合には,[m..n]
と記述します.これを数列表記
といいます.
> [1 .. 10]
ghci1,2,3,4,5,6,7,8,9,10] [
コンマと併用することで階差数列などを表現することも可能です.
> [1,3..10]
ghci1,3,5,7,9] [
[x..]
と終わりを指定しないことで,無限数列も作成できます. ghciでそのまま実行すると永遠に表示が止まりません(ctrl+C
で止まります). ここでは,[1,3,5,...]
の10番目と100番目の値を取り出してみます.
> [1,3..] !! 10
ghci21
> [1,3..] !! 100
ghci201
Pythonなど言語では,値が宣言/生成されたタイミングでコンピュータがその値を評価する正格(strict)評価
が一般的です. 一方HaskellはDefaultでは,実際にその値が呼び出された際に評価される遅延(lazy)評価
を採用しており,それによりこのような無限の値を実現することができます.
正格評価で無限に値が続くリストを生成した場合, 生成した時点で永遠に計算が終わりませんが,遅延評価では無限のリストの中の具体的な値を利用するさいにその値が利用されます.
この機能はHaskellの大きな特徴の一つですが,一方でメモリリークや,速度の低下の原因になることがあります. したがって,ある程度大きなプログラムを書く場合には,正格評価と,遅延評価を明示的に切り替えることが推奨されています.
最初は気にする必要はありませんが,パッケージなどの提供するHaskellのデータ型には,strictなものとlazyなものの両方が用意されていることが多いので,違いを覚えておくと後々役に立ちます.
Haskellでリストを扱う際には,暗黙にx
などの単一のアルファベットが要素,xs
などの複数形がリストを表している場合が多くx:xs
などと記述してリストの最初の要素と残りのリストを表します.
詳細は後ほど扱いますが,束縛
されたリストからパターンマッチ
によって値を取り出す場合によく利用されます.
> x:xs = [1,2,3]
ghci> x
ghci1
> xs
ghci2,3] [
リスト同士の結合は++
演算子によって行います.
> [1] ++ [2,3]
ghci1,2,3] [
リストの長さはlength
関数で取得できます.
> length []
ghci0
> length [1,2,3]
ghci3
1.5.5 タプル
Haskellではデータの組み合わせを表す方法として,後述の直積型
がありますが,タプルも良く利用されます.タプルを利用するには要素を()
(丸括弧)で囲い,,
(コンマ)で区切ります. 要素数に制限はありません.
> (1,2)
ghci1,2)
(> (1,2,3)
ghci1,2,3) (
リストと同様に要素数の異なるタプルや,要素のデータ型の異なるタプルは別のデータ型として区別され,同一のリストなどに入れることはできません.
> [(1,2),(1,2,3)]
ghci
<interactive>:60:8: error: [GHC-83865]
Couldn't match expected type: (a, b)
• type: (a0, b0, c0)
with actual In the expression: (1, 2, 3)
• In the expression: [(1, 2), (1, 2, 3)]
In an equation for ‘it’: it = [(1, 2), (1, 2, 3)]
Relevant bindings include
• it :: [(a, b)] (bound at <interactive>:60:1)
要素数が2つのリストに限定して,要素を取り出す関数 fst
,snd
が用意されていますが,値の取り出しはパターンマッチがよく利用されます.
> fst (1,2)
ghci1
> snd (1,2)
ghci2
> (x,y) = (1,2)
ghci> x
ghci1
> y
ghci2
1.5.6 文字列型
Haskellの文字列型は歴史的に少し複雑な状況になっており,PreludeにおけるString
型の使い勝手があまり良くありません. なので, text
パッケージの提供するText
型を利用するのが一般的です. なので,後ほどTextを導入しますが,一旦String型に関して見てみましょう.
Haskellでは1文字を表す Char
型と文字列を表すString
型を区別し,Char
は''
(シングルクォーテーション),String
は""
(ダブルクオーテーション)で囲みます.
> 'c'
ghci'c'
> :t 'c'
ghci'c' :: Char
> "String"
ghci> :t "String"
ghci"String" :: String
Haskellにおける文字型Char
のリスト[Char]
の別名(型シノニム
)です. 型シノニム
は型に別の名前をつけることで,用途などを区別する機能です.
型シノニムは,以下のように, type 型シノニム = 元のデータ型
という形で定義します.
type String = [Char]
したがって,String型にはListの演算が適用できます.
> "String" !! 1
ghci't'
> '!' : "String"
ghci"!String"
> "!!" ++ "String"
ghci"!!String"
> length "String"
ghci6
ただし,String
型は非効率なため,現在ではあまり使われておらず,基本的にtext
パッケージの提供する Data.Text
を利用することが推奨されています.
package.yaml
のdependencies
に以下のようにtext
を追加します.
dependencies:
- base >= 4.7 && < 5
- text
スクリプトの最初に以下のように,記述することで文字列リテラル""
がText
型に利用できるようになります.
{-# LANGUAGE OverloadedStrings #-}
import Data.Text
{-# LANGUAGE OverloadedStrings #-}
は言語拡張を表しており,Haskellの処理系に機能を追加する宣言です. OverloadedString
は文字列リテラルをTextなどの他の文字列を表すデータ型に適用できるようにする拡張です.
ghci
で言語拡張を導入するには,:set
に続けて -X言語拡張名
を記述します.
> :type "Text"
ghci"Text" :: String
> :set -XOverloadedStrings
ghci> import Data.Text
ghci> :type "Text"
ghci"Text" :: Data.String.IsString a => a
1.6 論理型(Bool)
それが正しいか間違っているか判別できる文を命題といいます. 命題の結果を表すものとして真(正しい),偽(間違っている)という値を用います. 真と偽を併せて真偽値といいます.
例えば,1は2より大きい
という命題は,間違っているので偽となります. 人間は必ず死ぬ
という命題は,今のところ不老不死の人間がいないので真です.
真偽値を表すデータ型としてBool
があります. Bool
はTrue
(真),False
(偽)のいずれかです.
Haskellには命題の判定を行う関係演算子
として,以下のようなものが準備されています.
記号 | 意味 |
---|---|
> |
より大きい |
>= |
以上 |
< |
より小さい |
<= |
以下 |
== |
等しい |
/= |
等しくない |
数値などの大小関係を調べるときには,比較演算子 >
,>=
.<
,<=
を利用します. 演算子の左右に数値を書くと,結果に応じて真偽値が帰ってきます.
> 1 > 2
ghciFalse
> 1 < 1.5
ghciTrue
値が等しいか/等しくないかを判定するには,==
と!=
を利用します.
> 4 == 4
ghciTrue
> "cat" /= "cat"
ghciFalse
True
や False
などのBool
値は, AND
(かつ),OR
(または),NOT
という演算で計算することができます(XOR
というのもあるが省略).
HaskellではAND は &&
, OR は ||
, NOT は not
という演算子が提供されています.
A,Bが命題だとして,A && B
は両方True
のときに,True
となります. A || B
は片方どちらかがTrue
のときにTrue
となります.
例えば,
1は2より大きい かつ 2は0より大きい
という命題は,2は0より大きい
は正しいですが,1は2より大きい
が間違っているので全体として,False
です.ネコは哺乳類である または ネコは鳥類である
という命題はネコは鳥類である
が間違っていますが全体としてはTrue
です.
演算の結果は,それぞれ以下のようになります. これを真偽値表といいます. ここでは,最低限の例だけを紹介しますが,より深く理解したい人は論理学などの講義を受講しましょう.
命題Aの値 | Bの値 | A && B |
A || B |
---|---|---|---|
True | False | True | True |
False | True | False | True |
True | False | False | True |
False | False | False | True |
> 1 > 2
ghciFalse
> 2 > 0
ghciTrue
> 1 > 2 && 2 > 0
ghciFalse
> 1 > 2 || 2 > 0
ghciTrue
not
は命題の否定を表しており True
がFalse
,False
がTrue
になります.not
は命題の前に書きます.
ghci> 1 > 2
False
ghci> not (1 > 2)
True
演習
ある値が偶数かどうかは,2で割った余りが0かどうかを判定することで判定できます.
x=101
,y=202
として, 以下の命題の真偽を判定してください.
- xが偶数
- yが偶数
- xが偶数かつyが偶数
- xが偶数またはyが偶数
- x + y が奇数
1.7 スクリプトファイルの実行
ここから先は,コードが複数行に渡ることが多くなるので,ghciの利用をやめてスクリプトを書きます.
app
フォルダ内に practice.hs
を作成しそこで事例の勉強をしましょう.
practice.hs
ファイルを作成したら,ファイルを以下のように記述しましょう.
{-# LANGUAGE OverloadedStrings #-}
import Data.Text
main :: IO ()
= putStrLn "practice" main
module XXX () where
という記述は,他のファイルからインポート可能なmodule化を行うための宣言です.
また,Stackでは,大文字で始まる*.hs
ファイルは,moduleとして認識されます.
したがって,一つのプロジェクトに複数の実行可能ファイルを生成する場合には,
module XXX () where
の記述をなくし, ファイル名を小文字ではじめる必要があります.
これは,Hello World
のために編集したMain.hs
も同様であるため,Main.hs
をhello.hs
に名前を変更し,ファイル内の module Main (main) where
の記述も削除し,以下のように変更しましょう.
import Lib
main :: IO ()
= helloWorld main
package.yaml
のexecutables:
を以下のように編集してhello.hs
とpractice.hs
を実行可能ファイルとして登録します. Data.Text
を利用するために,dependencies:
以下に- text
を追加しておきましょう.
dependencies:
- base >= 4.7 && < 5
- text
#ghc-options:
library:
source-dirs: src
executables:
hello:
main: main.hs
source-dirs: app
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- hello-world
practice:
main: practice.hs
source-dirs: app
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- hello-world
stack run practice
でpractice!
と表示されれば成功です.
これからスクリプトで実行していくにあたって,practice.hs
の中身をもう少し詳しく見てみましょう.
import Lib
main :: IO ()
= putStrLn "practice!!" main
haskellのプログラムを実行すると, main関数
のみが実行されます.
Haskellは関数型言語なので,これからimport Lib
とmain
の間に関数を定義していき,main
の中で実行していくことになります.
main 関数で行うことは関数として実行することになりますが,これから学習する通常の関数の定義で記述するのは今は難しいので,do
記法を紹介します. main 関数の=以下にdo
と書くことで,do以下のインデントブロックに記述された内容が手続き型的に1行ずつ実行されます.
以下のプログラムでは, "practice1"
,"practice2"
,"practice3"
の順に標準出力されます.
import Lib
main :: IO ()
= do
main putStrLn "practice1" -- "practice1"
putStrLn "practice2" -- "practice2"
putStrLn "practice3" -- "practice3"
stack run practice
の結果を確認すると以下のようになります.
> stack run practice
"practice1"
"practice2"
"practice3"
また,ghciと異なって,出力結果が同じ画面に現れないので, 以降のコード例では, その行の結果をコメントで書くこととします. コメント部分は,記述しなくても結果は変わらないので,省略しても構いません.