講義を通してHaskell入門時に習ったことのおさらいです。もうすぐ期末試験なので復習を兼ねて。なお、入門したばかりの素人が書いてるので信憑性はありません。「完全に理解した曲線」で言えば、今いるのはまさにここ。
(実際は自信もないけど)
この記事は、便所の落書き同然の個人用のメモとして捉えてください。
関数型プログラミングとは
命令型プログラミングとの違い
C言語やC++など、よく使われているのはハードウェア優先の命令型プログラミング。ハードウェア視点に立ってプログラムを書く必要があり、コンピュータが行う手順通りに命令を書かなければならないので人間にとってはわかりにくい(らしい)です。よってバグやエラーを起こしやすい。
一方、Haskellのような関数型プログラミングは人間の視点のまま実装できます。関数や数式を書いて実行すれば、その後の処理はコンピュータがうまい具合に動かしてくれます。
(以下、私見)
命令型プログラミングにどっぷり浸かってきた人からすれば、関数型より命令型のほうが楽です。
一般的な命令型プログラミングに慣れていない人のほうがむしろ習得しやすい、とよく言われているようです。(入門書にそう書いてあった)
何を定義する?
→各変数の数学的な「関係」を定義する
例)f : E→F
ここで言う「関係」とは、関数であれば引数に対してどんな値が返されるかとか、そういうことです。要は関数を定義します。
なお、C言語でも「関数」と呼ばれる概念があり日常的に使われていますが、関数型プログラミングの関数とは別物です。C言語では純粋な関数を定義することも可能ですが、副作用(後述)を伴う純粋な関数でないものも定義できます。そのためか、近年はC言語などの関数は「メソッド」などと言い換えられるようになっています。
副作用
プログラムの実行結果に何らかの状態変化を与えてしまうような、純粋な関数の機能ではない動作を副作用といいます。値を返さないメソッドもこれに該当します。例えば文字列を表示する、C言語で言うprintf()がこれに該当します。
Haskellでは副作用も例外的に利用可能ですが、今回は副作用は一切使いません。
GHCi
対話型のHaskellインタプリタ。コマンドプロンプトやターミナルで実行可能。
インストール方法
下記を参照。
Haskell環境構築 for Windows | 為せばnull
Haskell環境構築 for Mac | 為せばnull
使用例
Prelude> 5 + 2
7
Prelude> 2 * pi - 1
5.283185307179586
なお、冒頭に表示されるPreludeというのはGHCi起動時に自動で読み込まれるライブラリのこと。
コマンド
:cd
:ディレクトリ移動:help
または:?
:ヘルプ:load <ファイルパス>
または:l <ファイルパス>
:ソースファイル(拡張子hs)の読み込み:module <モジュール名>
:モジュールの読み込み:type <束縛名>
:束縛のデータ型の確認:quit
または:q
:GHCiの終了
なお、hsファイルはHaskellのプログラムを記述したソースコードのことです。
前置記法と中置記法
上記のように、
Prelude> 5 + 2
7
Prelude> 2 * pi - 1
5.283185307179586
のような中置記法の書き方も可能。
一方で、以下のような書き方でも同じ動作が可能です。
Prelude> (+) 5 2
7
Prelude> (-) ((*) 2 pi) 1
5.283185307179586
これを前置記法といいます。本来はこちらが標準的な書き方で、前者のような中置記法は例外的に認められています。結果はどちらも同じです。
Prelude> 2 ^ 4 - 1
15
Prelude> (-) ((^) 2 4) 1
15
Prelude> 2 * 3 * (5 - 1)
24
Prelude> (*)((*) 3 ((-) 5 1)) 2
24
束縛
Haskellで扱える変数はすべて不変(immutable)です。一度結びつけた値は変更できません。
そして変数への代入は「束縛」といいます。束縛とは、変数名と数値を結びつけることを指します。
束縛の例;
Prelude> a = 2
Prelude> a
2
Prelude> a - 1
1
ここで束縛名の頭文字は小文字にしなければならないことに注意。Haskellでは小文字と大文字は区別されます。
なお、変数束縛の状況は以下のようにして確認できます。
Prelude> :show bindings
a :: Num p => p = _
it :: Num p => p = _
上記ではaとitがNum型の束縛であることを示しています。データ型については後述。
データ型
C言語で言うint型やdouble型など。
データ型 | 概要 | 例 |
---|---|---|
Int | 固定長整数 | 123 |
Integer | 多倍長整数(上限のない整数型) | 456 |
Word | 符号なし整数(0以上の整数) | 789 |
Float | 単精度浮動小数点数 | 10.11 |
Double | 倍精度浮動小数点数 | 12.13 |
Bool | 真偽型(True or False) | True |
Rational | 有理数 | ‘a’ |
Char | 文字(1文字) | ‘a’ |
String | 文字列 | “abc” |
[] | リスト | [1, 2, 3] |
() | タプル | (123, ‘a’) |
リストは必ず1つのデータ型しか格納できないのに対し、タプルでは複数のデータ型を格納可能。上記の例では整数と文字を同一のタプルに格納しています。タプルとリストについての詳細は後半にて。
型クラス
データ型を示すクラスのこと。すなわち、複数のデータ型の集合をクラス分けしています。例えば、型クラスNumは数値型全般を表す型クラスで、そこにはInt、Integer、Float、Doubleといった数値型のデータ型が属します。
型クラス | 概要 | 所属するデータ型 |
---|---|---|
Num | 数値型全般 | Int, Integer, Float, Double, Word |
Eq | 等値(=)が成立するか否かを評価できるデータ型 | Int, Float, Double, Char, Bool, Word, Odering |
Ord | 大小関係が比較可能なデータ型 | Int, Float, Double, Char, Bool, Word, Odering |
Show | 文字列型に変換できるデータ型 | Int, Integer, Float, Double, Char, Bool, Word, Odering |
Fractional | 実数の割り算が定義されているもの | Num, Float, Double |
Floating | 実数 + 三角関数、根号、ネイピア数、対数など | Float, Double, pi, exp, log, sqrtなど |
Integral | 整数の割り算や剰余などが定義されているもの | Int, Integer, Word, quot, rem, div, modなど |
Enum | ある値の前後が定義されているもの | Int, Integer, Float, Double, Char, Bool, Word, (), Oderingなど |
これらはコマンドi: <型クラス名>
で確認できます。
例:
Prelude> :i Enum
type Enum :: * -> Constraint
class Enum a where
succ :: a -> a
pred :: a -> a
toEnum :: Int -> a
fromEnum :: a -> Int
enumFrom :: a -> [a]
enumFromThen :: a -> a -> [a]
enumFromTo :: a -> a -> [a]
enumFromThenTo :: a -> a -> a -> [a]
{-# MINIMAL toEnum, fromEnum #-}
-- Defined in ‘GHC.Enum’
instance Enum Word -- Defined in ‘GHC.Enum’
instance Enum Ordering -- Defined in ‘GHC.Enum’
instance Enum Integer -- Defined in ‘GHC.Enum’
instance Enum Int -- Defined in ‘GHC.Enum’
instance Enum Char -- Defined in ‘GHC.Enum’
instance Enum Bool -- Defined in ‘GHC.Enum’
instance Enum () -- Defined in ‘GHC.Enum’
instance Enum Float -- Defined in ‘GHC.Float’
instance Enum Double -- Defined in ‘GHC.Float’
静的型付け
Haskellではデータ型の暗黙的な自動変換はありません。例えば以下の例は実行不可能。
Prelude> 1 + "2"
<interactive>:4:1: error:
• No instance for (Num [Char]) arising from a use of ‘+’
• In the expression: 1 + "2"
In an equation for ‘it’: it = 1 + "2"
この例では数値と文字列を足そうとしています。JavaScriptなどでは自動変換されますが、Haskellではこのような場合、自動変換されることはありません。
ただしデータ型を意図的に変換させることは可能です。その場合、<値> :: <データ型>
のようにして記述します。例えば
Prelude> :type 3.34
3.34 :: Fractional p => p
Prelude> :type 3.34 :: Float
3.34 :: Float :: Float
前者のように、単純に実数を与えるとFractional型として扱われますが、値の後に:: Float
を付け加えることでFloat型に変換されています。
なお、以下の例ではエラーが発生します。
Prelude> :type 3.34 :: Int
<interactive>:1:1: error:
• No instance for (Fractional Int) arising from the literal ‘3.34’
• In the expression: 3.34 :: Int
これは、Fractional型からInt型に変換する手順がHaskellに実装されていないからです。
スコープ
束縛の使用可能範囲のこと。
※ここまでは対話型で実行しましたが、以後はhsファイルに各関数を記述して実行します。
ファイル名:main.hs
読み込み方法:
Prelude> :load main.hs
トップレベル変数
どこでも使用可能な変数のこと。C言語などで言うグローバル変数に相当。
main.hs
:
var1 :: Int
var1 = 1
var2 = var1 + 1
ローカル変数(局所変数)
特定のスコープ内でしか使用できない変数のこと。whereとletのいずれかで定義可能。
where
使用する箇所の下でローカル変数を定義。
例えば、半径r=5の円周の長さを求める関数circでは
main.hs
:
circ :: Double
circ = 2 * pi * r
where r = 5
このように、circ = 2 * pi * r
を定義した後にローカル変数rの値をwhere句内で定義します。このように書くと、ローカル変数rは関数circ内でのみ使用可能であり、circの外では使えません。
ここで、where句を書くときはwhereの前に2文字分の空白が必要なので要注意。
実行結果:
Prelude> :load main.hs
[1 of 1] Compiling Main ( main.hs, interpreted )
*Main> circ
31.41592653589793
なお、改行はどこでもOK。例えば
circ :: Double
circ = 2 * pi * r
where
r = 5
このように書くことも可能。実行結果も一緒です。
もちろん、別々のスコープなら同じ変数名が使用可能。例えば
main.hs
:
a = 1
f = a * b
where a = 2
b = a + 1
where a = 0
とすれば
Prelude> :load main.hs
[1 of 1] Compiling Main ( main.hs, interpreted )
*Main> f
2
このように正しく実行できます。
let
ローカル変数を使用する前に値を定義。
doを使って定義:
circ :: Double
circ = do
let r = 5
y = 2 * pi * r
y
doを使わずに定義(末尾にinが必要):
circ :: Double
circ =
let r = 5
y = 2 * pi * r in
y
どちらも実行結果は一緒。
Prelude> :load main.hs
[1 of 1] Compiling Main ( main.hs, interpreted )
*Main> circ
31.41592653589793
関数
いよいよ関数の登場です。と言っても先程のローカル変数の説明でちょっと出てきたけど。
※以降、ソースファイルmain.hs
の読み込み(:load main.hs
)部分の記述は省きますが実行方法は同様です。
単項関数
引数を1つしか持たない関数。
f x = x + 1
実行結果:
*Main> f 3
4
無名関数
その名の通り、名前を持たない関数。ラムダ式ともいいます。
バックスラッシュ(\)を使って記述します。
例えばf x = x + 1
はこのように定義することも可能。
f = \x -> x + 1
\x -> x + 1
がx+1を計算する無名関数で、関数f
はこの無名関数を返り値として返します。結果的に、第1引数x
に1を足した値が返されます。
*Main> f 3
4
高階関数
引数として関数を受け取ったり、関数を返り値として返す関数のこと。例えば
f x = \y -> x + y
は無名関数\y -> x + y
を返す高階関数。この場合、引数を1つ持つf
は更に引数を1つ持つ無名関数を返すので、実行時に引数は合計で2つ必要です。
*Main> f 1 2
3
引数のデータ型を指定
関数が持つ引数と返り値のデータ型を指定することも出来ます。
書き方:
関数名 :: 引数1のデータ型 -> 引数2のデータ型 -> ... -> 引数nのデータ型 -> 返り値のデータ型
例:
f :: Integer -> Integer
f x = x + 1
*Main > f 3
4
上記の例で、f
はInteger型の引数1つを受け取りInteger型の値を返す関数です。よってf :: Integer -> Integer
と表記します。
閉包表記
閉包表記によって、特定の引数、返り値をもつ関数を引数として受け取らせることも可能。
例:
g :: Integer -> Integer
g x = x + 1
h :: (Integer -> Integer) -> Integer
h f = f 2
*Main > h g
3
g
は引数+1を返すInteger -> Integer
の関数で、例えばg 2
なら3が返されます。
そして関数h
はInteger -> Integer
の関数を引数として受け取る関数です。このことは(Integer -> Integer)
と書くことで示しています。h
に対し関数g
を引数として渡すと、関数g
に対して引数2が与えられ、結果として2+1=3が返されます。
こんな使い方も可能です。
p :: Integer -> Integer -> Integer
p x y = x * y
q :: (Integer -> Integer -> Integer) -> Integer -> Integer
q f z = f z (z-1)
*Main> q p 10
90
関数p
はx×yを返すInteger -> Integer -> Integer
の関数で、関数q
は第1引数に与えられたInteger -> Integer -> Integer
の関数に対し、第2引数, (第2引数-1)
の順序で引数を与えます。
ここでq p 10
を実行した場合、qは結果的にp 10 9
を実行し、最終的に10×9=90が返されます。
型変数
上記のように、いちいちInteger -> Integer -> Integer
だのと書くのは面倒です。型変数を使うことで、データ型に名前をつけて省略して書くことが出来ます。C++でいうところのtemplete <T>
です。
書き方:
関数名 :: 型クラス1 型変数名1, ..., 型クラスn 型変数名n => 型変数名x -> 型変数名y -> ... -> 型変数名z
例:
p :: Num a => a -> a -> a
p x y = x * y
q :: Num a => (a -> a -> a) -> a -> a
q f z = f z (z-1)
*Main> q p 10
90
上記の例は先程示した関数p, qと同じ動作をします。なお、型変数に使えるのは型クラスなので、データ型であるInteger型ではなく型クラスであるNumクラスに変更しています。
カリー化
複数の引数を取る関数において、それぞれの引数に対して関数をネストさせること。
例:
r1 x y z = x + y + z
r2 x y = \z -> x + y + z
r3 x = \y -> \z -> x + y + z
r4 = \x -> \y -> \z -> x + y + z
上記の関数r1
〜r4
はどれも同じ動作をします。
*Main> r1 1 2 3
6
*Main> r2 1 2 3
6
*Main> r3 1 2 3
6
*Main> r4 1 2 3
6
r4
の場合、r4
は無名関数\x
を、\x
は\y
を、\y
は\z
を、\z
はx+y+zを返します。
Haskellでは複数の引数を持つ関数は内部でカリー化されて実行します。
おわりに
前半はここまで。後半も書くつもりだけど必ずしも書き終わる保証はないです。
追記:書きました。後半はこちら