7関数型プログラミング言語としてのいくつかの機能

本章は,関数型のプログラムの記述を便利にするためのEgisonの機能をいくつか紹介する.

7.1無名パラメーター関数

無名パラメーター関数(anonymous parameter function)を使うと引数に名前をつけずに関数を定義できる. ラムダ式を使うと関数の命名を省略できる. 引数の命名まで省略できる無名パラメーター関数は,これをさらにおしすすめたものと考えることができる. たとえば,第一引数を10倍して第二引数と足し合わせる無名パラメーター関数は以下のように定義できる.

2#($1 * 10 + $2) 1 2
-- 12

#の前の2\(2\)引数の関数を定義していることを意味する. 関数のボディの中で現れる$1$2はそれぞれ第一引数と第二引数を表している.

無名パラメータ関数には,引数にタプルをとるものとリストをとるものの2つの変種がある. この2つの変種は#の前に指定する引数の数を括弧で囲むことによって定義する. #の前の数を()で囲むとタプルを引数に取る無名パラメーター関数を定義できる.

(2)#($1, $2) (10, 20)
-- (10, 20)

#の前の数を[]で囲むとリストを引数に取る無名パラメーター関数を定義できる.

[2]#($2, $1) [10, 20]
-- (20, 10)

7.2無名matchAll関数・無名match関数

関数定義のとき,仮引数をすぐにパターンマッチすることが多い. するとその仮引数の名前がそのパターンマッチのターゲットとしてしか役に立たない. この問題を解決するために無名matchAll関数と無名match関数がある.

まず,無名matchAll関数を紹介する. \にスペースを入れずにmatchAllを続けることで無名matchAll関数は定義される. このときっパターンマッチのターゲットは省略される. この無名matchAll関数の引数がターゲットとなるからである.

\matchAll as list something with
  | $hs ++ $ts -> (hs, ts)

無名match関数は,無名matchAll関数のmatchAllmatchに変えるだけで定義できる.

\match as list something with
  | nil -> True
  | _ -> False

7.3中置演算子の定義

Egisonはユーザーが新しく中置演算子を定義する機能を提供している. 中置演算子を宣言するには,infix,またはinfixlinfixrを使う. infixinfixlinfixrはそれぞれ結合性のない演算子,左結合の演算子,右結合の演算子を定義するのに使う. infixinfixlinfixrは共通して三つの引数をとる. 第一引数には,これから定義する中置演算子が関数であるのか,パターンコンストラクタであるのか指定するために,expressionpatternというキーワードをとる. 第二引数には,これから定義する中置演算子の優先度を整数値でとる. 第三引数には,これから定義する中置演算子の名前をとる.

中置演算子として使う関数を定義する例として論理積&&を定義すると以下のようになる. 型注釈により、2つのブール値を引数にとりブール値を返すことを示している.

infixr expression 5 &&

def (&&) (a: Bool) (b: Bool) : Bool := match (a, b) as (eq, eq) with
  | (#True, #True) -> True
  | _              -> False

中置演算子として使うパターンコンストラクタを定義する例としてジョイン・パターン++を定義すると以下のようになる. 型注釈により、型パラメータ{a}とマッチャー引数(m: Matcher a)を受け取り、リスト型[a]に対するマッチャーを返すことを示している.

infixl pattern 7 ++

def list {a} (m: Matcher a) : Matcher [a] := matcher
  | $ ++ $ as (list m, list m) with
    ...
演算子演算子の内容優先度
^べき乗8
*掛け算7
/割り算7
%割り算の余り7
.テンソルの掛け算7
+足し算6
-引き算6
::コンス5
++アペンド5
=, <= >=, <, >比較4
&&論理積3
||論理和2
$関数適用0
中置演算子一覧(関数)

演算子演算子の内容優先度
^べき乗8
*掛け算7
/割り算7
+足し算6
::コンス5
++アペンド5
&andパターン3
|orパターン2
中置演算子一覧(パターン)

7.4型クラス

Egisonは型クラス(type class)をサポートしている. 型クラスは,複数の型に共通する振る舞いを抽象化するための機構である. Egisonの型クラスはHaskellの型クラスに似た構文と意味論をもつ.

7.4.1型クラスの定義

型クラスはclassキーワードを使って定義する. 型クラスの定義には,型変数と,その型変数に対して要求される関数のシグネチャを含める. たとえば,等値性を比較できる型を表すEq型クラスは以下のように定義される.

class Eq a where
  (==) (x: a) (y: a) : Bool
  (/=) (x: a) (y: a) : Bool

この定義は,「型aEqのインスタンスであるためには,2つのa型の値を受け取りBoolを返す==/=という演算を提供しなければならない」ことを意味する.

Egisonはextendsキーワードによるスーパークラス宣言もサポートしている. 型クラスは1つ以上のスーパークラスを宣言でき,サブクラスのインスタンスである型はスーパークラスのインスタンスでもなければならない.

これらの機能を使って,Egisonは数値型のための以下の代数的型クラス階層を定義している. まず,加法の階層は以下のように定義される.

class AddSemigroup a where
  (+) (x: a) (y: a) : a

class AddMonoid a extends AddSemigroup a where
  zero : a

class AddGroup a extends AddMonoid a where
  neg (x: a) : a

AddSemigroupは加法を提供し,AddMonoidはそれに零元を追加し,AddGroupはさらに符号反転を追加する.

同様に,乗法の階層は以下のように定義される.

class MulSemigroup a where
  (*) (x: a) (y: a) : a

class MulMonoid a extends MulSemigroup a where
  one : a

class MulGroup a extends MulMonoid a where
  inv (x: a) : a

これらの階層を組み合わせて複合的な代数構造を定義する. 型クラスはカンマ区切りで複数のスーパークラスを継承できる. 独自のメソッドをもたない型クラスは,スーパークラスを組み合わせるだけのマーカークラスとして機能する.

class Ring a extends AddGroup a, MulMonoid a
class Field a extends Ring a, MulGroup a

Ringは加法群と乗法モノイドの両方の構造を要求し,Fieldはさらに乗法逆元を要求する.

数式処理向けの拡張も定義されている.

class GCDDomain a extends Ring a where
  gcd (x: a) (y: a) : a

class EuclideanDomain a extends GCDDomain a where
  divMod (x: a) (y: a) : (a, a)

7.4.2型クラスのインスタンス

具体的な型を型クラスのインスタンスにするにはinstanceキーワードを使う. たとえば,Integer型をEqのインスタンスにするには以下のように書く.

instance Eq Integer where
  (==) x y := x = y
  (/=) x y := not (x == y)

MathValue型を代数的型クラス階層のインスタンスにする例は以下のとおりである. 階層の各レベルごとにインスタンス定義が必要になる.

instance AddSemigroup MathValue where
  (+) x y := plusForMathValue x y

instance AddMonoid MathValue where
  zero := 0

instance AddGroup MathValue where
  neg x := minusForMathValue 0 x

instance MulSemigroup MathValue where
  (*) x y := multForMathValue x y

instance MulMonoid MathValue where
  one := 1

instance MulGroup MathValue where
  inv x := divForMathValue 1 x

メソッドをもたないマーカークラスのインスタンスはwhere句なしで宣言できる.

instance Ring MathValue
instance Field MathValue

型クラスのインスタンス定義には型クラス制約を含めることもできる. たとえば,Tensor a型をEqのインスタンスにするとき,要素型aEqのインスタンスである必要がある場合は以下のように書く.

instance {Eq a} Eq (Tensor a) where
  (==) t1 t2 := t1 = t2
  (/=) t1 t2 := not (t1 == t2)

7.4.3型クラス制約の使い方

型クラスを使うことで,多相的な関数に制約を課すことができる. 型クラス制約は波括弧{}で囲んで関数の型シグネチャに記述する. たとえば,2つの値の等値性を比較する関数は以下のように定義できる.

def eqAs {Eq a} (m: Matcher a) (x: a) (y: a) : Bool := ...

この型シグネチャは「型aEqのインスタンスであるとき,eqAsMatcher aaaを受け取りBoolを返す」ことを意味する.

複数の型クラス制約を課すこともできる. たとえば,順序付けと等値性の両方を必要とする関数は以下のように書ける.

def compare {Eq a, Ord a} (x: a) (y: a) : Ordering := ...

代数的型クラス階層を使うことで,減算と除算を最小限の型クラス制約をもつ導出演算として定義できる.

def (-) {AddGroup a} (x: a) (y: a) : a := x + neg y
def (/) {Field a} (x: a) (y: a) : a := x * inv y

同様に,sumproductも適切な制約で定義される.

def sum {AddMonoid a} (xs: [a]) : a := foldl (+) zero xs
def product {MulMonoid a} (xs: [a]) : a := foldl (*) one xs

型クラス制約は,関数だけでなくマッチャーの定義にも使える. たとえば,sortedListマッチャーは,要素型が順序付け可能(Ordのインスタンス)であることを要求する.

def sortedList {Ord a} (m: Matcher a) : Matcher [a] := matcher
  ...

7.4.4型クラスの実装:辞書渡しスタイル

Egisonの型クラスは辞書渡しスタイル(dictionary-passing style)で実装されている. これは,型クラス制約をもつ関数が,実際にはその型クラスのメソッドを含む辞書を引数として受け取る関数に変換されることを意味する.

たとえば,以下のような型クラス制約をもつ関数は

def double {AddSemigroup a} (x: a) : a := x + x

内部的には以下のように辞書パラメータを受け取る関数に変換される.

def double (dict_AddSemigroup_a) (x: a) : a :=
  (dict_AddSemigroup_a_"plus") x x

ここでdict_AddSemigroup_aAddSemigroup aインスタンスのメソッド(この場合は+)を含むハッシュ(辞書)である.

具体的な型に対して関数を呼び出す場合,適切なインスタンスの辞書が自動的に渡される. たとえば,double 1は以下のように変換される.

double addSemigroupInteger 1

ここでaddSemigroupIntegerAddSemigroup Integerインスタンスの辞書である.

この辞書渡しスタイルにより,型クラスの動的ディスパッチ(実行時の型に応じたメソッドの選択)が実現される. また,型クラス制約をもつ関数同士の呼び出しも正しく処理される. たとえば,以下のような関数では

def myPlus {AddSemigroup a} (x: a) (y: a) : a := x + y
def myPlus2 {AddSemigroup a} (x: a) (y: a) : a := myPlus x y

myPlus2が受け取った辞書がmyPlusに正しく渡される.

7.5IO入出力

遅延評価を基本の評価戦略とするEgisonは,副作用をもつプログラムを記述するための特別な構文をもつ. Egisonは静的型システムをもつ言語であり,Haskellと同じような方法で副作用をもつプログラムを記述する.

7.5.1main関数

たとえば,”Hello world!”という文字列を出力するプログラムは以下のように書ける. 型注釈により、コマンドライン引数の文字列リストを受け取り、IO処理を実行することを示している.

def main (args: [String]) : IO () := write "Hello world!\n"

コマンドラインオプション-tがない場合は,Egisonはmain関数を実行する. 上記のプログラムは以下のように実行できる.

$ cat hello.egi
def main (args: [String]) : IO () := write "Hello world!\n"
$ egison hello.egi
Hello world!

writeはEgisonに組み込みで実装されているIO関数である. writeは第一引数に文字列をとり,その文字列を標準出力に印字する. IO関数にはwrite以外にもいくつもある. 図7.5.1にその一覧をまとめた.

関数名関数の内容
return \(x\)\(x\)をIO関数の結果として返す.
openInputFile \(path\)パス\(path\)のファイルを読み込みモードで開き,そのファイルへの入力ポートを返す.
openOutputFile \(path\)パス\(path\)のファイルを書き込みモードで開き,そのファイルへの出力ポートを返す.
closeInputPort \(port\)入力ポート\(port\)を閉じる.
closeOutputPort \(port\)出力ポート\(port\)を閉じる.
readChar標準入力から一文字読み取り,その文字を返す.
readLine標準入力から一行読み取り,その文字列を返す.
writeChar \(c\)標準出力に文字\(c\)を書き込む.
write \(s\)標準出力に文字列\(s\)を書き込む.
readCharFromPort \(port\)入力ポート\(port\)から一文字読み取り,その文字を返す.
readLineFromPort \(port\)入力ポート\(port\)から一行読み取り,その文字列を返す.
writeCharToPort \(c\) \(port\)文字\(c\)を出力ポート\(port\)に書き込む.
writeToPort \(s\) \(port\)文字列\(s\)を出力ポート\(port\)に書き込む.
isEof標準入力がEOF(ファイルの終端)に達しているか調べ,達していればTrue,そうでなければFalseを返す.
isEofPort \(port\)入力ポート\(port\)がEOFに達しているか調べ,達していればTrue,そうでなければFalseを返す.
flush標準出力のバッファに溜まっている内容を即座に出力する.
flushPort \(port\)出力ポート\(port\)のバッファに溜まっている内容を即座に出力する.
readFile \(path\)パス\(path\)のファイルの全内容を文字列として読み込んで返す.
rand \(n_1\) \(n_2\)整数\(n_1\)から\(n_2\)までの範囲の整数をランダムに返す(両端を含む).
f.rand \(f_1\) \(f_2\)浮動小数点数\(f_1\)から\(f_2\)までの範囲の浮動小数点数をランダムに返す.
newIORef変更可能な変数(参照)を生成してその参照を返す.初期値は未定義.
writeIORef \(ref\) \(x\)参照\(ref\)が参照する変更可能な変数の値を\(x\)に更新する.
readIORef \(ref\)参照\(ref\)が参照する変更可能な変数の現在の値を返す.
readProcess \(cmd\) \(args\) \(input\)コマンド\(cmd\)を引数リスト\(args\)で実行し,標準入力に文字列\(input\)を与え,その標準出力を文字列として返す.
io \(ioAction\)IO関数\(ioAction\)を純粋な関数の中で実行し,その結果を返す(unsafePerformIOに相当).
IO関数一覧

mainの第一引数にはコマンドライン引数がはいる. コマンドライン引数は文字列としてmainの第一引数に渡される.

$ cat args.egi
def main (args: [String]) : IO () := write (show args)
$ egison args.egi hello world 1
["hello", "world", "1"]

Egisonでつくったスクリプトをコマンドにしたい場合はシェバン(shebang)を使えばよい.

$ cat args
#!/usr/local/egison

def main (args: [String]) : IO () := write (show args)
$ args hello world 1
["hello", "world", "1"]

7.5.2do

do式を使うと,複数のIO関数を一つにまとめ,順番に実行することができる. do式はHaskellのdo記法に対応している. たとえば,以下のプログラムはユーザーの入力を一行読み取り,それを出力する.

def main (arg: [String]) : IO () := do
  let line := readLine ()
  write line

do式と再帰関数を組み合わせることができる. 以下はインタプリタのREPL(Read-Eval-Print Loop)のEvalをぬいたプログラムである.

def main (arg: [String]) : IO () := repl

def repl : IO () := do
  write "> "
  flush ()
  let line := readLine ()
  write line
  flush ()
  repl

Haskellと同様に,オフサイドルールを使わないdo記法の構文もサポートしている.

do { print "foo" ; print "bar" ; print "baz" }

なお,Egisonのdo式はIOモナドに対してのみ使用可能であり,Haskellのようにリストモナドやその他のモナドに対して使うことはできない.

7.5.3io関数

io関数はHaskellのunsafePerformanceIO関数に対応する組み込み関数である. io関数を使うとIO関数でない通常の関数のなかでIO関数を呼び出せるようになる. これはprintfデバッグをしたりするのに便利である.

以下のプログラムは二つのサイコロをふりそれらの目の合計値を返す.

def dice : Integer := io (rand 1 6)

dice + dice

7.6組み込み関数

Egisonには多数の組み込み関数(primitive functions)が用意されている. これらの関数は言語処理系に組み込まれており,ユーザーが直接利用できる. 本節では主要な組み込み関数をカテゴリ別に紹介する.

7.6.1算術演算関数

整数演算と浮動小数点演算のための組み込み関数が提供されている. 表7.6.1に主要な算術演算関数を示す.

関数名関数の内容
i.+ \(x\) \(y\)整数\(x\)\(y\)の和を返す.
i.- \(x\) \(y\)整数\(x\)\(y\)の差を返す.
i.* \(x\) \(y\)整数\(x\)\(y\)の積を返す.
i./ \(x\) \(y\)整数\(x\)\(y\)で割った商を有理数として返す.
i.modulo \(x\) \(y\)整数\(x\)\(y\)で割った剰余を返す(mod).
i.quotient \(x\) \(y\)整数\(x\)\(y\)で割った商を返す(quot).
i.% \(x\) \(y\)整数\(x\)\(y\)で割った剰余を返す(rem).
i.power \(x\) \(y\)整数\(x\)\(y\)乗を返す.
i.abs \(x\)整数\(x\)の絶対値を返す.
i.neg \(x\)整数\(x\)の符号を反転した値を返す.
i.< \(x\) \(y\)整数\(x\)\(y\)より小さければTrue,そうでなければFalseを返す.
i.<= \(x\) \(y\)整数\(x\)\(y\)以下であればTrue,そうでなければFalseを返す.
i.> \(x\) \(y\)整数\(x\)\(y\)より大きければTrue,そうでなければFalseを返す.
i.>= \(x\) \(y\)整数\(x\)\(y\)以上であればTrue,そうでなければFalseを返す.
f.+ \(x\) \(y\)浮動小数点数\(x\)\(y\)の和を返す.
f.- \(x\) \(y\)浮動小数点数\(x\)\(y\)の差を返す.
f.* \(x\) \(y\)浮動小数点数\(x\)\(y\)の積を返す.
f./ \(x\) \(y\)浮動小数点数\(x\)\(y\)で割った商を返す.
f.abs \(x\)浮動小数点数\(x\)の絶対値を返す.
f.neg \(x\)浮動小数点数\(x\)の符号を反転した値を返す.
f.< \(x\) \(y\)浮動小数点数\(x\)\(y\)より小さければTrue,そうでなければFalseを返す.
f.<= \(x\) \(y\)浮動小数点数\(x\)\(y\)以下であればTrue,そうでなければFalseを返す.
f.> \(x\) \(y\)浮動小数点数\(x\)\(y\)より大きければTrue,そうでなければFalseを返す.
f.>= \(x\) \(y\)浮動小数点数\(x\)\(y\)以上であればTrue,そうでなければFalseを返す.
round \(x\)浮動小数点数\(x\)を最も近い整数に丸める.
floor \(x\)浮動小数点数\(x\)以下の最大の整数を返す(床関数).
ceiling \(x\)浮動小数点数\(x\)以上の最小の整数を返す(天井関数).
truncate \(x\)浮動小数点数\(x\)の小数部分を切り捨てた整数を返す.
算術演算関数一覧

また,浮動小数点数に対する数学関数も提供されている(表7.6.1).

関数名関数の内容
f.sqrt \(x\)浮動小数点数\(x\)の平方根を返す.
f.exp \(x\)自然対数の底\(e\)\(x\)乗を返す.
f.log \(x\)浮動小数点数\(x\)の自然対数を返す.
f.sin \(x\)\(x\)のサイン(正弦)を返す.
f.cos \(x\)\(x\)のコサイン(余弦)を返す.
f.tan \(x\)\(x\)のタンジェント(正接)を返す.
f.asin \(x\)\(x\)のアークサイン(逆正弦)を返す.
f.acos \(x\)\(x\)のアークコサイン(逆余弦)を返す.
f.atan \(x\)\(x\)のアークタンジェント(逆正接)を返す.
f.sinh \(x\)\(x\)の双曲線サイン(双曲正弦)を返す.
f.cosh \(x\)\(x\)の双曲線コサイン(双曲余弦)を返す.
f.tanh \(x\)\(x\)の双曲線タンジェント(双曲正接)を返す.
f.asinh \(x\)\(x\)の逆双曲線サイン(逆双曲正弦)を返す.
f.acosh \(x\)\(x\)の逆双曲線コサイン(逆双曲余弦)を返す.
f.atanh \(x\)\(x\)の逆双曲線タンジェント(逆双曲正接)を返す.
数学関数一覧

数学定数として,円周率f.pi\(\pi \approx 3.14159\))と自然対数の底f.e\(e \approx 2.71828\))が定義されている.

7.6.2文字列操作関数

文字列を操作するための組み込み関数が提供されている(表7.6.2).

関数名関数の内容
pack \(cs\)文字のリスト\(cs\)を文字列に変換する.
unpack \(s\)文字列\(s\)を文字のリストに変換する.
unconsString \(s\)文字列\(s\)の先頭文字と残りの文字列のタプルを返す.
lengthString \(s\)文字列\(s\)の長さ(文字数)を返す.
appendString \(s_1\) \(s_2\)文字列\(s_1\)\(s_2\)を連結した文字列を返す.
splitString \(pat\) \(s\)文字列\(s\)をパターン\(pat\)で分割し,部分文字列のリストを返す.
regex \(pat\) \(s\)正規表現\(pat\)で文字列\(s\)をマッチし,マッチ前・マッチ部分・マッチ後の3つ組を返す.
regexCg \(pat\) \(s\)正規表現\(pat\)で文字列\(s\)をマッチし,キャプチャグループを含む結果を返す.
read \(s\)文字列\(s\)をEgisonの式として解析し,評価した結果を返す.
readTsv \(s\)タブ区切り文字列\(s\)を解析し,評価した結果を返す.
show \(x\)\(x\)を文字列表現に変換する.
showTsv \(x\)\(x\)をタブ区切り形式の文字列に変換する.
文字列操作関数一覧

7.6.3型変換・型チェック関数

型変換と型チェックのための組み込み関数が提供されている(表7.6.3).

関数名関数の内容
itof \(n\)整数\(n\)を浮動小数点数に変換する.
rtof \(r\)有理数\(r\)を浮動小数点数に変換する.
ctoi \(c\)文字\(c\)をそのUnicodeコードポイント(整数)に変換する.
itoc \(n\)整数\(n\)を対応するUnicode文字に変換する.
isInteger \(x\)\(x\)が整数であればTrue,そうでなければFalseを返す.
isRational \(x\)\(x\)が有理数であればTrue,そうでなければFalseを返す.
型変換・型チェック関数一覧

注:EgisonではMathValueIntegerRationalが同一視されるため,isIntegerisRationalは数式処理システムで数値の性質を判定するために使われる.

7.6.4テンソル関連関数

テンソルの情報を取得するための組み込み関数が提供されている(表7.6.4).

関数名関数の内容
tensorShape \(t\)テンソル\(t\)の形状(各次元のサイズ)をリストとして返す.
tensorToList \(t\)テンソル\(t\)の要素をリストに変換する.
dfOrder \(t\)微分形式\(t\)の階数を返す.
addSubscript \(sym\) \(sub\)シンボル\(sym\)に下付き添字\(sub\)を追加する.
addSuperscript \(sym\) \(sup\)シンボル\(sym\)に上付き添字\(sup\)を追加する.
テンソル関連関数一覧

7.6.5デバッグ・テスト関数

プログラムのデバッグとテストのための組み込み関数が提供されている(表7.6.5).

関数名関数の内容
assert \(label\) \(test\)テスト\(test\)Trueでなければラベル\(label\)を含むアサーションエラーを発生させる.
assertEqual \(label\) \(actual\) \(expected\)\(actual\)\(expected\)が等しくなければラベル\(label\)を含むアサーションエラーを発生させる.
デバッグ・テスト関数一覧

7.7seq

seq式は,プログラムを部分的に遅延評価ではなく正格評価したいときに使う. 遅延評価は便利であるが,ときに,メモリを無駄に確保し効率が悪いことがある. 部分的にプログラムを正格評価することで,プログラムの効率が向上することがある. seq式は第一引数のプログラムを正格評価したあと,第二引数のプログラムを評価する. たとえば,foldlは以下のようにseq式を使うと効率が向上することが知られている. 型注釈により、畳み込み関数、初期値、リストを受け取り、累積結果を返すことを示している.

def foldl {a, b} (fn: b -> a -> b) (init: b) (ls: [a]) : b :=
  match ls as list something with
    | [] -> init
    | $x :: $xs ->
      let z := fn init x
        in seq z (foldl fn z xs)
この本を別の言語で読む: English, 日本語