例えば手続き型言語では下記の様に、様々な変数の値を変化させながら計算し、最後にその結果を出力します。
# C言語ライクな仮想の手続き型言語 main() { data_list = [100, 200, 300, 400] -- 計算したいデータはこれ total = 0 -- まず total に 0 を代入しておく for (i = 0; i < length(data_list); i++) { -- iの値を0から1ずつ増やしながら... total = total + data_list[i] -- 総和を計算していく } total = total * 1.1 -- 総和に税金(10%)をかける print(total) -- 結果を表示する }
関数型では次のように最初にやりたいことを書き、その定義を関数や式として定義していくだけで目的を果たすことができます。
# Haskellライクな仮想の関数型言語 data_list = [100, 200, 300, 400] -- 計算したいデータはこれ main() = print(total) -- 最終的な目的は結果(total)を表示すること total = calc_total(data_list) -- totalは、calc_total関数で data_list を処理したもの calc_total(x) = sum(x) * 1.1 -- calc_total関数は、引数の総和を計算して税金分をかけたもの
「iには1が代入されていて... totalには100が代入されていて...」といった変数状態を考えながら、コンピューター目線でデバッグするのが手続き型。変数状態を意識することなく、純粋に「定義は正しいか」でデバッグできるのが関数型言語の優位性です。
Haskell は 純粋関数型言語 (purely functional language)でもあります。純粋関数型はすべての関数で 副作用 を許しません。つまり、Haskell はコンソール、データベース、他システム等とやりとりすることはできません。...というのは嘘で、モナド という仕組みを使って、副作用に代替する機能を実装しています。
例えば、下記の例では main 関数の中で "Hello" をコンソールに書き出しているように見えますが、Haskell の実装では「"Hello"をコンソールに書き出す」という IOモナド を生成し、それを main 関数の戻り値として返却することにより、あくまで、main 関数の外でコンソールに書き出している、つまり main は純粋関数であるという体裁を保っています。
main = print "Hello"
Haskell では変数も参照透過性を保ち、いつも同じ値を返します。変数と呼んではいますが、一度初期化すると変更が許されない イミュータブル(不変)なオブジェクトです。変数の値を変動させながら手続きを記述するのではなく、純粋に関数定義のみを行うことでプログラミングしていきます。
コンパイラとして ghc があり、それをパッケージングした haskell-platform があります。
-- CentOS 7 -- # yum -y install epel-release # yum -y install haskell-platform # ghc --version The Glorious Glasgow Haskell Compilation System, version 7.6.3 -- CentOS 8 -- # dnf -y install epel-release # dnf -y install ghc # ghc --version The Glorious Glasgow Haskell Compilation System, version 8.2.2 -- Ubuntu 20.04 -- $ sudo apt -y install haskell-platform $ ghc --version The Glorious Glasgow Haskell Compilation System, version 8.6.5
最近では Haskell Platform を個人環境にインストールし、プロジェクト管理機能も備える Haskell Stack が主流となっているようです。
-- CentOS 8 -- $ sudo curl -sSL https://get.haskellstack.org/ | sh $ stack setup -- Ubuntu 20.04 -- $ sudo apt -y install haskell-stack # システムにstack(ちょっと古い)をインストールする $ stack upgrade # 個人環境に新しいstackをインストールする # "no such protocol name" エラーになる場合は /etc/protocols に "tcp 6 TCP" という行を追加 $ echo "export PATH=$HOME/.local/bin:$PATH" >> ~/.bashrc $ source ~/.bashrc # 個人環境のstackを使用するようにする $ stack setup # GHCなどを個人環境にインストールする
Haskell Stack の使用例を示します。
$ stack new sample # サンプルプロジェクトを作成する $ cd ./sample # プロジェクトに移動する $ vi ./app/Main.hs # サンプルプログラムを編集する $ stack build # ビルドする $ stack run # 実行する
お決まりの Hello world は次のようになります。main は最初に実行されるエントリポイント関数です。拡張子は .hs とします。
main = putStrLn "Hello world!"
ghc コマンドでコンパイルして実行します。
$ ghc Hello.hs $ ./Hello Hello world!
rungpc コマンドを用いてスクリプト言語のように実行することもできます。
$ runghc Hello.hs Hello world!
ghci を用いて対話的なインタプリタとして使用することもできます。Prelude は Haskell のスタンダードモジュールの名前です。
$ ghci
Prelude> putStrLn "Hello world!"
Hello world!
Prelude> Ctrl-Dで終了
Haskell の予約語を下記に示します。
case class data default deriving do else foreign if import in infix infixl infixr instance let module newtype of then type where _
-- から行末まで、または {- から -} までがコメントとみなされます。
-- 1行コメント {- 複数行コメント {- ネストしても良い -} -}
{ expr1; expr2; ... } は複数の式を一つの式として扱います。do とブロックを組み合わせることで、複数の式を実行することができます。
main = do { putStrLn "Red"; putStrLn "Green"; putStrLn "Blue" }
Python の様にインデントを用いることで、{ ... } ブロックの { と } を省略することができます。
main = do putStrLn "Red" putStrLn "Green" putStrLn "Blue"
main = do putStrLn "Red" putStrLn "Green" putStrLn "Blue"
putChar 'a' -- 文字を出力する putStr "ABC" -- 文字列を改行無しで出力する putStrLn "ABC" -- 文字列を改行付きで出力する print "ABC" -- 任意の型の値を改行付きで出力する(デバッグ用) x <- getChar -- 1文字入力する x <- getLine -- 文字列を入力する x <- getContents -- 複数行の文字列を入力する(EOFまで) x <- readLn -- 数値や "..." 形式の文字列を入力する
主な型の例として下記などがあります。
Bool -- 真偽型。True または False Char -- 文字型 String -- 文字列型。[Char] のシノニム Int -- 固定長整数(最低30bits以上) Integer -- 多倍長整数(any bits) Float -- 単精度浮動小数点数(32bits) Double -- 倍精度浮動小数点数(64bits) [Int] -- Intのリスト [Char] -- Charのリスト。String と等価 (Int, Char) -- Int と Char のタプル(後述) Int -> Int -- Int引数を受け取り、Int値を返却する関数型 Int -> Int -> Double -- Int引数を2つ受け取り、Double値を返却する関数型 a -- 任意の型 [a] -- 任意の型のリスト
変数と呼びますが、手続き型言語の変数とは異なり、値を変更することはできません。x という変数に 123 という値を「代入」するのではなく、123 という数値に x というラベルを「束縛(バインディング)」すると考えます。変数名には英数字とアンダーバー(_)とシングルクォート(')を使用できます。最初の文字は小文字ではなくてなりません。
x = 123 main = print x
変数の型を明示的に指定するには下記の様に宣言します。
x :: Int y :: Int x = 123 y = 234 main = print $ x + y
次のように束縛(binding)と型宣言を同時に行うこともできます。
x = 123 :: Int y = 234 :: Int main = print $ x + y
123 ... 10進数 0o123 oo123 ... 8進数 0x7fb 0X7FB ... 16進数 1.23 ... 小数点数 1.23e12 ... 浮動小数点数(1.23×1012)
文字は、1文字の半角英数字記号やUnicode文字を扱うことができます。
'a' -- 文字「a」 'あ' -- Unicodeの「あ」 '\x3042' -- Unicodeの「あ」
文字列は、文字のリストとして定義されます。String は [Char] のシノニム(同義語)です。下記のふたつは同義です。
"ABC" -- 文字列「ABC」 ['A', 'B', 'C'] -- 文字列「ABC」
末尾と継続行の先頭にバックスラッシュ(\)を入れることで、文字列を複数行に分割することができます。
str = "Hello \ \world!"
文字(Char)や文字列(String)の中では下記のエスケープシーケンスを使用できます。
\a ... アラート \b ... バックスペース \f ... フォームフィード \n ... 改行 \r ... キャリッジリターン \t ... タブ \v ... 垂直タブ \\ ... バックスラッシュ \" ... ダブルクォーテーション \' ... シングルクォーテーション \& ... ヌル文字 \o132 ... 8進数で132の文字(Z) \x5A ... 16進数で5Aの文字(Z) \90 ... 10進数で92の文字(Z) \& ... 区切り文字 (使用例: "\x5A\&123" → "Z123")
\LF や \ESC のような制御文字も指定できます。
\NUL \SOH \STX \ETX \EOT \ENQ \ACK \BEL \BS \HT \LF \VT \FF \CR \SO \SI \DLE \DC1 \DC2 \DC3 \DC4 \NAK \SYN \ETB \CAN \EM \SUB \ESC \FS \GS \RS \US \SP \DEL
リストは同じ型の値を一方向に並べたものです。配列とは異なり、内部的には最初の要素が次の要素へのポインタを保持するといったリスト構造で保持されています。Int のリスト型は [Int] と表します。
[1, 2, 3] -- 整数リスト([Int]) [1..3] -- [1, 2, 3] と等価 [1, 3...9] -- [1, 3, 5, 7, 9] と等価 [3, 2..0] -- [3, 2, 1, 0] と等価 ['a', 'b', 'c'] -- 文字リスト([Char])。"ABC" と等価 ['a'..'c'] -- ['a', 'b', 'c'] と等価 ["Red", "Green", "Blue"] -- 文字列リスト([String]) [1, 2, 3] !! 2 -- 3 (0から数えて2番目の要素を取り出す) [1, 2] ++ [3, 4] -- リストを連結する length [1, 2, 3] -- 3 (要素の数を返す) head [1, 2, 3] -- 1 (先頭の要素を返す) last [1, 2, 3] -- 3 (最後の要素を返す) tail [1, 2, 3] -- [2, 3] (先頭を除いた要素を返す) init [1, 2, 3] -- [1, 2] (末尾を除いた要素を返す) take 2 [1, 2, 3] -- [1, 2] (先頭から2個の要素を返す) takeWhile (<3) [1, 2, 3] -- [1, 2] (条件に合致する要素を返す) drop 2 [1, 2, 3] -- [3] (先頭から2個除いた要素を返す) dropWhile (<3) [1, 2, 3] -- [3] (条件に合致しない要素を返す) reverse [1, 2, 3] -- [3, 2, 1] (リストを逆方向にする) map (*2) [1, 2, 3] -- [2, 4, 6] (リストに対して関数を適用する)
[1, 2, 3] は 1:[2, 3] のように表すことができます。下記はすべて [1, 2, 3] と等価となります。
[1, 2, 3] 1:[2, 3] 1:2:[3] 1:2:3:[]
下記の例は、x に [1, 2, 3, 4, 5] をひとつずつ代入しながら、x * x の値を求めます。
s = [x * x | x <- [1..5]] -- [1, 4, 9, 16, 25]
カンマ(,) の後ろにガード条件をつけることもできます。下記の例は、x が 3 と異なる場合のみ x * x を計算します。
s = [x * x | x <- [1..5], x /= 3] -- [1, 4, 16, 25]
タプルはリストと似ていますが、要素は同じ型である必要はありません。
(1, 2, 3) (1, 'a', "ABC")
要素数が 0個のタプルはユニット(unit)と呼ばれます。
func = return ()
タプルの要素を取り出すには下記の様にします。3番目以降を取り出す際は変数を使用する必要があります。
fst (1, 'a', "ABC") -- 1(最初の要素を取り出す) snd (1, 'a', "ABC") -- 'a'(2番目要素を取り出す) (_, _, x) = (1, 'a', "ABC") -- x に "ABC" がバインドされる
expr1 + expr2 -- 加算 expr1 - expr2 -- 減算 expr1 * expr2 -- 乗算 expr1 / expr2 -- 除算 expr1 `div` expr2 -- 除算(-∞方向に丸める) expr1 `mod` expr2 -- 除算(`div`)の余り expr1 `rem` expr2 -- 除算(`quot`)の余り expr1 `quot` expr2 -- 除算(0方向に丸める) expr1 ^ expr2 -- 累乗(expr2は整数) expr1 ^^ expr2 -- 累乗(expr1は実数、expr2は整数) expr1 ** expr2 -- 累乗(expr1もexpr2も実数) expr1 == expr2 -- 等しければ expr1 /= expr2 -- 等しくなければ expr1 < expr2 -- 大きければ expr1 <= expr2 -- 以上であれば expr1 > expr2 -- 小さければ expr1 >= expr2 -- 以下であれば bool1 && bool2 -- かつ bool1 || bool2 -- または not bool -- 否定 list !! index -- リストの index番目の要素 list1 ++ list2 -- リストを連結(文字列連結にも使用可) value : list -- [value] ++ list と同義 expr `elem` list -- expr が list に含まれていれば expr `notElem` list -- expr が list に含まれていなければ func $ expr -- func ( expr ) と等価 func $! expr -- func ( expr ) と等価 (exprを即時評価する) func1 . func2 -- 関数合成 expr1 `seq` expr2 -- 正格評価を行う(遅延評価を行わない) var <- action -- アクションから値を取り出す func =<< action -- アクションから値を取り出し関数に引き渡す action >>= func -- アクションから値を取り出し関数に引き渡す stmt1 >> do {stmt2} -- do { stmt1; stmt2 } と等価
演算子の優先度は 0 ~ 9 まであります。
9 : !! . 8 : ^ ^^ ** 7 : * / `div` `mod` `rem` `quot` 6 : + - 5 : : ++ 4 : == /= < <= > >= `elem` `notElem` 3 : && 2 : || 1 : >> >>= 0 : $ $! `seq"
演算子を (...) で囲むことにより関数のように使用することができます。下記の2行は同じ意味を持ちます。
x = y + z x = (+) y z
逆に、二つの引数を持つ関数名を `...` で囲むことで演算子の様に使用することができます。
x = y `add` z
下記の例では x と y を引数として受け取り、その和を返却する関数 add を定義しています。
add x y = x + y
関数を呼び出すには次のようにします。print add 3 5 としてしまうと、(print add) 3 5 と解釈されてしまいますので、print (add 3 5) としています。
main = print (add 3 5) -- 8
括弧の代わりに $ を用いることもできます。$ は $ から行末までを (...) で囲むのと同じ意味になります。
main = print $ add 3 5 -- print (add 3 5) と同義
関数の型は下記の様に表します。最初の2つの Int は引数の型を表し、最後の Int は関数の戻り値の型を表します。
add :: Int -> Int -> Int
演算子を定義することができます。下記の例では、x * 1000 + y を計算する演算子 ^^^ を定義して使用しています。
x ^^^ y = x * 1000 + y
main = print $ 2 ^^^ 20 -- 2020
infix* を用いて定義した演算子の優先度を 0~9 の間で指定することができます。infixl は左結合、infixr は右結合、infix は結合無しを意味します。
x +++ y = x + y -- 加算演算子+++を定義 x *** y = x * y -- 乗算演算子***を定義 infixl 7 +++ -- +++ の優先度を7に設定 infixl 6 *** -- *** の優先度を6に設定 main = print $ 10 *** 3 +++ 2 -- 加算が先に計算されて 50 となる
演算子には下記の記号を使用します。
# $ % & * + . / < = > ? @ \ ^ | - ~
下記は、再帰関数を用いて n! (nの階乗) を求める関数 fact を定義しています。n! = n × (n - 1)!、1! = 1 というルールに従っています。
fact 0 = 1 fact n = n * fact (n - 1) main = print $ fact 5
ラムダ式は関数名の無い局所的な関数で、下記の形式で表されます。
\arg -> expr \(arg1, arg2) -> expr
ラムダ式を用いた実例を下記に示します。
main = do print c -- 31 where c = a + b -- 31 a = (\x -> x * x) 5 -- 25 (5 * 5) b = (\(x, y) -> x * y) (2, 3) -- 6 (2 * 3)
関数を引数の値によって別々に定義することができます。
func 1 = "One"
func 2 = "Two"
func 3 = "Three"
main = print $ func 1 -- "One"
パターンマッチを用いることで、階乗を求める関数 fact を次のように定義することができます。
fact 0 = 1 -- 0 の時は1を返す fact n = n * fact (n - 1) -- 0 以外の時は n * fact(n - 1) を返す main = print $ fact 5 -- 120
パターンマッチと似ていますが、下記の様にガード条件を用いた関数を定義することができます。condition1 が真の時は expr1 を、condition2 が真の時は expr2 を、さもなくば expr3 を返します。
funcname arg1, arg2, ... | condition1 = expr1 | condition2 = expr2 | otherwise = expr3
実際の使用例を下記に示します。
foo x | x == 1 = "One" | x == 2 = "Two" | x == 3 = "Three" | otherwise = "More..." main = putStrLn $ foo 2
なぜ「ガード条件」と呼ぶかですが、下記の例では foo という関数を、引数が5以上であることを条件としてガードして呼び出しています。
foo x | x >= 5 = x - 5 main = do print $ foo 5 -- 引数が5以上なので呼び出せる print $ foo 4 -- 引数が5未満なのでエラーとなる
. 演算子を用いて関数を合成することができます。例えば、下記の様な n に対して fn(n) = f(g(h(n))) のような演算を行うケースを考えます。
f n = n * 2 g n = n * 3 h n = n * 4 fn n = f(g(h(n))) main = print $ fn 5
ここで、f, g, h関数を . で連結することが可能です。下記の3行はいずれも同じ意味を持ちます。
fn n = f(g(h(n))) fn n = (f . g . h) n fn = (f . g . h)
関数の引数は @ を用いて複数の形式で受け取ることができます。下記では、引数の文字列全体を str として、また、先頭の文字を x、残りの文字を xs として受け取ることができます。
func str@(x:xs) = do print str -- "ABCDE" print x -- 'A' print xs -- "BCDE" main = do func "ABCDE"
do は式や指定した式を処理します。式には ブロック を指定することもできます。
main = do { print "A"; print "B"; print "C" }
ブロックは レイアウト を用いて下記の様に記述することもできます。
main = do print "A" print "B" print "C"
let は変数と値をバインド(束縛)します。一度バインドした変数は他の値に変更することはできません。
main = do let msg = "Hello" putStrLn msg
in ... を用いると、バインドした変数は in ... の中だけで有効な変数となります。
area_of_circle r = let pi = 3.14 in do r * r * pi main = print $ area_of_circle 1.23
if文は、expr1 が真であれば expr2 を、偽であれば expr3 を返します。
if expr1 then expr2 else expr3
使用例を下記に示します。
isZero x = if x == 0 then "Zero" else "NotZero" main = putStrLn $ isZero 123
case 文は、expr が pattern1 にマッチすれば expr1 を、pattern2 にマッチすれば expr2 を、さもなくば expr3 を返します。
case expr of pattern1 -> expr1 pattern2 -> expr2 _ -> expr3
使用例を下記に示します。
getColor x =
case x of
1 -> "Red"
2 -> "Green"
3 -> "Blue"
_ -> "Unknown"
main = putStrLn $ getColor 3 -- "Blue"
where 文は、変数や補助関数を局所的に定義します。
main = print $ add x y where x = 123 y = 456 add x y = x + y
import はモジュールを読み込みます。
import [qualified] ModuleName [as AliasName] [[hiding] (name, ...)]
下記の例では ord や chr 関数を呼ぶために Data.Char モジュールをインポートしています。
import Data.Char main = do { print $ ord 'A'; print $ chr 65 }
as でモジュールの別名を指定することができます。
import Data.Char as Ch main = do { print $ Ch.ord 'A'; print $ Ch.chr 65 }
qualified をつけると、モジュール名または別名が必須となります。
import qualified Data.Char as Ch
main = print $ ord 'A' -- Ch をつけていないのでエラー
(name, ...) を指定すると指定した値のみをインポートします。
import Data.Char as Ch (ord)
hiding (name, ...) を指定すると指定した値以外のものをインポートします。
import Data.Char as Ch hiding (ord)
Haskell には for や while などのループ構文はありません。下記の様な再帰関数を用いてループを実現します。下記では Hello を10回出力しています。loop n action は、action を実行し、n をデクリメントし、再度 loop を再起呼び出しします。n が 0 の時はそれ以上再起呼び出しはせず、ユニット () を返却します。
loop 0 action = return () loop n action = do { action; loop (n - 1) action } main = loop 10 $ putStrLn "Hello"
上記と同様なことを行う機能が、replicateM_ という モナド として提供されています。
import Control.Monad main = do replicateM_ 3 $ putStrLn "Hello"
data は列挙型、タプル型、直和型などのデータ型を定義します。
data TypeName = Constructor1 [ | Constructor2 ]... [deriving (TypeClass, ...)]
data TypeName = Constructor1 Type1a Type1b ... [ | Constructor2 Type2a Type2b ...]... [deriving (TypeClass, ...)]
data TypeName = Constructor1 { fieldLabel1a :: Type1a, fieldLabel1b :: Type1b, ... } [ | Constructor2 { fieldLabel2a :: Type2a, fieldLabel2b :: Type2b, ... } ]... [deriving (TypeClass, ...)]
列挙型は他言語の Enum に似たデータ型です。Bool は True または False を値として持つ列挙型です。下記の例では、Red、Green または Blue を値として持つ列挙型 Color を次のように定義することができます。データ型は大文字で始める必要があります。
data Color = Red | Green | Blue
データ型の値はそのままでは print で出力することができません。deriving で Show という 型クラス を付与することにより出力可能になります。
data Color = Red | Green | Blue deriving Show main = do let c = Red print c
また、Eq を付与することで == や /= で比較することが可能となります。
data Color = Red | Green | Blue deriving (Show, Eq) main = do let x = Red let y = Green if x == y then print "Equal" else print "Not Equal"
型クラスには次のものなどがあります。
Show -- print で出力可能な文字列に変換される Read -- 文字列から変換可能となる Eq -- == や /= で比較可能となる Ord -- < や > 等で大小比較可能となる Enum -- fromEnum や toEnum で数値と相互変換可能となる
タプル型は他言語の構造体(Struct)に似たデータ型です。下記の例では、Point というタプル型を定義しています。コンストラクタにも同じ名前を指定しています。Int 型の二つのフィールドを持ちます。deriving Show により print で出力可能にしています。
data Point = Point Int Int deriving Show
addPoint (Point x1 y1) (Point x2 y2) = Point (x1 + x2) (y1 + y2)
main = do
let a = Point 100 200
b = Point 300 400
c = addPoint a b
print c -- Point 400 600
次のように、フィールド名を指定することもできます。
data Point = Point { x, y :: Int } deriving Show
main = do
let a = Point { x = 100, y = 200 }
print a -- Point {x = 100, y = 200}
型名を下記の様に記述することにより、複数の型に対応するデータ型を宣言することができます。
data Point a = Point a a deriving Show main = do print $ Point 100 200 -- Int, Integerに対応 print $ Point 100.0 200.0 -- Doubleに対応
直和型は他言語の union に似たデータ型です。下記の例でデータ型 Figure は、x1, y1, x2, y2 フィールドを持つ Rect、または、x, y, r フィールドを持つ Circle のいずれかとして定義されます。
data Figure = Rect { x1, y1, x2, y2 :: Int } | Circle { x, y, r :: Int } deriving Show main = do let a = Rect { x1 = 100, y1 = 100, x2 = 200, y2 = 200 } b = Circle { x = 100, y = 100, r = 100 } print a -- Rect {x1 = 100, y1 = 100, x2 = 200, y2 = 200} print b -- Circle {x = 100, y = 100, r = 100}
Rect と Circle を Figure というひとつのデータ型で表現しているため、下記の様に Figure を引数としてその面積を返却する関数を定義することができます。
area :: Figure -> Double area (Rect x1 y1 x2 y2) = fromIntegral ((x2 - x1) * (y2 - y1)) area (Circle x y r) = (fromIntegral(r) * fromIntegral(r) * 3.14) main = do let a = Rect { x1 = 100, y1 = 100, x2 = 200, y2 = 200 } b = Circle { x = 100, y = 100, r = 50 } print $ area a -- 10000.0 print $ area b -- 7850.0
newtype は新たな型を生成します。フィールドがひとつのデータ型は data の代わりに newtype で定義することができます。newtype の方が効率的で高速に動作します。
newtype Pixel = Pixel Int deriving Show main = do let a = Pixel 300 print a
type は、ある型のシノニム(同義語)を生成します。例えば、String という型は下記の様に Char のリスト [Char] の型シノニムとして定義されています。
type String = [Char]
型シノニムの定義例を下記に示します。
type Person = (Name, Address) type Name = String type Address = None | Addr String
Haskell の class は他のオブジェクト指向系言語の class とは異なり、インタフェースに近い宣言を行います。下記の例では、型クラス Foo を定義しています。Foo 型クラスは任意の型(a)を受け取り、Stringを返却するメソッド foo を持ちます。instance を用いてそれぞれの型が引数に指定された場合の処理を実装します。
class Foo a where foo :: a -> String instance Foo Bool where foo True = "Bool: True" foo False = "Bool: False" instance Foo Int where foo x = "Int: " ++ show x instance Foo Char where foo x = "Char: " ++ [x] main = do putStrLn $ foo True -- Bool: True putStrLn $ foo (123::Int) -- Int: 123 putStrLn $ foo 'A' -- Char: A
instance Foo String where ... を定義しようとすると、String のようなシノニムに対しては定義できない旨のエラーが発生します。これを可能にするには、FlexibleInstances 拡張を宣言します。
{-# LANGUAGE FlexibleInstances #-} class Foo a where foo :: a -> String instance Foo String where foo x = "String: " ++ x main = do putStrLn $ foo "ABC"
Maybe型 (正確にはMaybeモナド)は、Just x または Nothing のどちらかの値を持つ型です。下記の例では関数 fn は、Int の引数を受け取り、Maybe String 型を返す関数です。Maybe String 型は、Just "One" だったり、Just "Two" だったり、Nothing だったりします。
fn :: Int -> Maybe String fn n | n == 1 = Just "One" | n == 2 = Just "Two" | otherwise = Nothing main = do print $ fn 1 -- Just "One" print $ fn 2 -- Just "Two" print $ fn 3 -- Nothing
Functor 型クラスは、map の汎用版である fmap クラスメソッドを持ちます。
$ ghci Prelude> :i Functor class Functor (f :: * -> *) where fmap :: (a -> b) -> f a -> f b
map は第二引数にリストしか受け取ることができませんが、fmap は Maybe 型(Nothing または Just x) やタプルを受け取ることもできます。
fn n = n * 2 main = do print $ fmap fn [1, 2, 3] -- [2, 4, 6] print $ fmap fn Nothing -- Nothing print $ fmap fn (Just 5) -- Just 10 print $ fmap fn (2, 3) -- (4, 6)
fmap fn x は fn <$> x と書くこともできます。
fn n = n * 2 main = do print $ fn <$> [1, 2, 3] -- [2, 4, 6] print $ fn <$> Nothing -- Nothing print $ fn <$> (Just 5) -- Just 10 print $ fn <$> (2, 3) -- (4, 6)
演算子 <$> は、List や Maybe などの任意の型をラッピングした値に対して、関数を適用しているとも言えます。
Applicative 型クラスは、Functor型クラスの派生クラスで、pure メソッドと <*> 演算子を持ちます。
Prelude> :i Applicative class Functor f => Applicative (f :: * -> *) where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b
pure は関数をラッピングします。<*> は、ラッピングした関数を、ラッピングした値に対して適用します。
main = do print $ pure (*2) <*> Just 5 -- Just10 print $ pure (*2) <*> [1, 2, 3] -- [2, 4, 6]
下記の様に複数の関数をリストに対して適用することも可能となります。
main = do
print $ [(*2), (*3)] <*> [1, 2, 3] -- [2, 4, 6, 3, 6, 9]
Monad 型クラスは、Applicative型クラスの派生クラスで、return メソッドと、>>= 演算子を持ちます。
class Applicative m => Monad (m :: * -> *) where (>>=) :: m a -> (a -> m b) -> m b return :: a -> m a
return は通常の値をラッピングされた値に変換します。演算子 >>= はバインド演算と呼ばれるもので、ラッピングされた値をラッピングされた値を返す関数に渡します。
fn x = return (2 * x) main = do print $ [1, 2, 3] >>= fn -- [2, 4, 6] print $ Just 5 >>= fn -- Just 10 print $ Nothing >>= fn -- Nothing
モナドには次のようなものがあります。
[] -- リスト Maybe -- Just x または Nothing 値を持つモナド Either -- Left a または Right b 値をもつモナド IO -- 入出力を司るモナド State -- 状態を扱うモナド Reader -- 「環境」から値を読み出すモナド Writer -- 値をログに書き込むモナド
putStrLng や print など、IO を司るものはすべてモナドとして実装されています。Haskell は純粋関数型言語なので、外部から値を読み込んだり、外部に値を書き出したりなどの副作用を持つことができません。そのため、putStrLng は値を書き出すのではなく、「値を書き出すというアクション」を返却します。Haskell のエントリポイントである main 関数が、こうした一連のアクションを返却し、GHC などの処理系が実際の入出力を行います。
module によりモジュールを定義することができます。ファイル名はモジュール名と同じ MyModule.hs とします。
module MyModule where add x y = x + y
これを下記の様に呼び出します。
import MyModule main = do print $ add 3 5
Haskell は 高階関数 (higher-order function) をサポートします。高階関数とは、関数自体を 第一級オブジェクト (値として代入したりできるもの) として扱い、関数の引数や戻り値で関数を引き渡しできるものをいいます。例えば map 関数は、第一引数で関数を受け取り、第二引数で受け取ったリストの各要素に対して関数を実行した結果を返します。JavaScript でもコールバック関数など、高階関数を多用しています。
fn x = x * 2
ans = map fn [1, 2, 3]
main = print ans -- [2, 4, 6]
複数の引数を受け取る関数に対して、一部の引数だけ渡しておき、後から残りを渡す方式を 部分適用 (partial application) と呼びます。
-- 部分適用を使用しない例 tax :: Double -> Double -> Double tax rate price = rate * price main = do print $ tax 0.1 2500 -- 2500円の消費税(250円)を求める print $ tax 0.1 3500 -- 3500円の消費税(350円)を求める
これを部分適用を用いて書き直すと下記の様になります。
-- 部分適用を使用した例 tax :: Double -> Double -> Double tax rate price = rate * price jptax = tax 0.1 -- 部分適用を利用した関数を定義する(第二引数が省略されている) main = do print $ jptax 2500 -- 2500円の消費税(250円)を求める print $ jptax 3500 -- 3500円の消費税(350円)を求める
演算子も関数と同様に使用できるので、部分適用を利用することができます。
exp2a = (^2) exp2b = (2^) main = do print $ exp2a 5 -- 5^2 と解釈されて25 print $ exp2b 5 -- 2^5 と解釈されて32
「複数の引数を持つ関数」を、「『元の関数の第1引数』を引数とし、『元の残りの引数を引数として結果を返す関数』を戻り値とする関数」にすることを カリー化 (currying) といいます。Haskell の名前の元になった Haskell Curry にちなんでカリー化と呼びますが、実際に考案したのは別の人だそうです。
まず、JavaScript での例を示します。add1() は3つの引数を取る関数ですが、これを、「x」を引数とし、「y, z を引数として結果を返す関数」を戻り値とする add2() にカリー化しています。呼び出しは add2(1)(2, 3) のようになります。さらにカリー化すると add3() になります。
function add1(x, y, z) { return x + y + z; } function add2(x) { return function(y, z) { return x + y + z; } } function add3(x) { return function(y) { return function(z) { return x + y + z; } } } console.log(add1(1, 2, 3)); console.log(add2(1)(2, 3)); console.log(add3(1)(2)(3));
Haskell の関数はデフォルトでカリー化されています。つまり、複数の引数を受け取って値を返す関数はすべて、ひとつの引数を受け取り、関数を返却する関数とみなすことができます。カリー化と部分適用は異なる概念ですが、カリー化によって部分適用が可能となります。
add x y z = x + y + z main = do print $ (add 1 2 3) -- 3つの引数を取る関数としても呼び出せる print $ (add 1) 2 3 -- 1つの引数を取り、その戻り値を残りの2つを引数にする関数とみなすこともできる
実は下記の add1 の記法は、add2 の記法の糖衣構文 (syntax sugar) です。Haskell の関数の型を、Int -> Int -> Int -> Int のように表すのも内部的には add2 のように処理していることに関係しています。
add1 :: Int -> Int -> Int -> Int add1 x y z = x + y + z add2 :: Int -> Int -> Int -> Int add2 = \x -> \y -> \z -> x + y + z
Haskell では通常の関数はカリー化されていますが、下記の様にタプルで引数を渡すことを非カリー化と呼んでいるようです。
add (x, y) = x + y main = print $ add (3, 5)
多くの言語が正格評価を採用しているのに対し、Haskell は 遅延評価 (lazy evaluation) を採用しています。式はどうしても必要となるときまで評価を行いません。遅延評価の対義語は 正格評価 (strict evaluation) です。
-- 正格評価の場合:main = fn 3 7 11 が実行される -- 遅延評価の場合:main = do { print (1+2); print (3+4) } が実行される。(5+6)は評価されない fn x y z = do { print x; print y } main = fn (1+2) (3+4) (5+6)
[1..] は [1, 2, 3, 4, ..., 1000000, ...] といった無限長のリスト、take n はリストの先頭から n 個の要素を取り出す関数ですが、遅延評価によりリストを無限参照してしまうリスクを低減することができます。
-- 正格評価の場合:最初に無限長リストを評価してしまうので無限ループになる -- 遅延評価の場合:リストの最初の5個のみ評価・返却される main = print $ take 5 [1..]
Foreign Function Interface (FFI) を用いて、Haskell から C言語で記述された関数を呼び出すことができます。まず、C言語で関数を用意します。
int plus(int a) { return a + 1; }
foreign を用いてこれを呼び出します。
import Foreign.C.Types foreign import ccall "plus" c_plus :: CInt -> IO CInt plus :: Int -> IO Int plus = fmap fromIntegral . c_plus . fromIntegral main :: IO () main = do print =<< plus 5
コンパイルは次のように行います。
# gcc -c plus.c # ghc Main.hs plus.o # ./Main 6
Haskell Platform にはパッケージ管理コマンド cabal が同梱されます。
# ghc-pkg list # パッケージの一覧を表示する # cabal update # パッケージをアップデートする # cabal info package # パッケージの情報を表示する # cabal install package # パッケージをインストールする