とほほのHaskell入門

目次

概要

Haskellとは

関数型言語

例えば手続き型言語では下記の様に、様々な変数の値を変化させながら計算し、最後にその結果を出力します。

# 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 Stack

最近では 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

お決まりの 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)

文字(Char)

文字は、1文字の半角英数字記号やUnicode文字を扱うことができます。

'a'		-- 文字「a」
'あ'		-- Unicodeの「あ」
'\x3042'	-- Unicodeの「あ」

文字列(String)

文字列は、文字のリストとして定義されます。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		-- 累乗(expr1expr2も実数)

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	-- exprlist に含まれていれば
expr `notElem` list	-- exprlist に含まれていなければ

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文

do は式や指定した式を処理します。式には ブロック を指定することもできます。

main = do { print "A"; print "B"; print "C" }

ブロックは レイアウト を用いて下記の様に記述することもできます。

main = do
  print "A"
  print "B"
  print "C"

let文

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文

if文は、expr1 が真であれば expr2 を、偽であれば expr3 を返します。

if expr1 then expr2 else expr3

使用例を下記に示します。

isZero x =
  if x == 0 then
    "Zero"
  else
    "NotZero"
main = putStrLn $ isZero 123

case文

case 文は、exprpattern1 にマッチすれば 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文

where 文は、変数や補助関数を局所的に定義します。

main = print $ add x y
  where
    x = 123
    y = 456
    add x y = x + y

import文

import はモジュールを読み込みます。

import [qualified] ModuleName [as AliasName] [[hiding] (name, ...)]

下記の例では ordchr 関数を呼ぶために 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 には forwhile などのループ構文はありません。下記の様な再帰関数を用いてループを実現します。下記では 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 に似たデータ型です。BoolTrue または False を値として持つ列挙型です。下記の例では、RedGreen または Blue を値として持つ列挙型 Color を次のように定義することができます。データ型は大文字で始める必要があります。

data Color = Red | Green | Blue

データ型の値はそのままでは print で出力することができません。derivingShow という 型クラス を付与することにより出力可能になります。

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}

RectCircleFigure というひとつのデータ型で表現しているため、下記の様に 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)

newtype は新たな型を生成します。フィールドがひとつのデータ型は data の代わりに newtype で定義することができます。newtype の方が効率的で高速に動作します。

newtype Pixel = Pixel Int deriving Show

main = do
  let a = Pixel 300
  print a

型シノニム (type)

type は、ある型のシノニム(同義語)を生成します。例えば、String という型は下記の様に Char のリスト [Char] の型シノニムとして定義されています。

type String = [Char]

型シノニムの定義例を下記に示します。

type Person = (Name, Address)
type Name = String
type Address = None | Addr String

型クラス (class)

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型 (正確には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)

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)

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)

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)

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..]

Cインタフェースの呼び出し(FFI)

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

パッケージ管理(ghc-pkg, cabal)

Haskell Platform にはパッケージ管理コマンド cabal が同梱されます。

# ghc-pkg list			# パッケージの一覧を表示する
# cabal update			# パッケージをアップデートする
# cabal info package		# パッケージの情報を表示する
# cabal install package		# パッケージをインストールする

Copyright (C) 2020 杜甫々
初版:2020年12月27日 最終更新:2020年12月27日
http://www.tohoho-web.com/ex/haskell.html