目次へ 最終更新 : 2003/1/19
本章では、関数(実際は、グラフ書換規則)と定数(実際は、グラフ式)が、どのようにCleanで定義されるのかを説明する。関数の本体は、(ルート)式からなる(3.4参照)。パターン(3.2参照)とガード(3.3参照)の助けによって、複数の関数代替部定義を区別できる。関数と定数は、関数定義内に局所定義できる。プログラミングの便宜(評価の強制、一意オブジェクトの監視と順次演算のスレッド化)の為に、特別なlet構成を提供する(3.5.1参照)。関数の型付けは、セクション3.7で議論する。多重定義関数については、6章参照。一意データ型に作用する関数については、9章参照。
関数定義 = [関数の型定義] // 関数の型付けについては、4章参照
関数の定義
関数の定義 = {関数代替部定義 ;}+
関数代替部定義 = 関数 {パターン} // パターンについては3.2参照
{LetBefore式} // 3.5.4参照
{{|ガード}=[>] 関数本体}+ // ガードについては3.3参照
[局所関数代替部定義] // 3.5参照
関数 = 関数名 // 通常の関数
| (関数名) // 前置を使用した演算子関数
関数本体 = ルート式 ; // 3.4参照
[局所関数定義] // 3.5参照
関数定義は、1つ以上の関数代替部(書換規則)定義からなる。そのような関数代替部の左手側では、(規則代替部(rule alternative)と呼ばれる)ガードされた関数本体(guarded function body)の並び全体を出力できるパターンを指定できる。特定の規則代替部のルート式(3.4参照)は、以下の場合に選択され評価される。
- 仮引数で指定されたパターンは、それに対応する関数適用の実引数に照合しており(3.2参照)、且つ、
- 右手側で指定されたオプションのガード(3.3参照)がTrueに評価される場合。
代替部は、テキスト順に試行される。関数には、その型定義が先行できる(セクション3.7)。
- 関数定義は、実装モジュール(2.3参照)でのみ認められる。
- 関数の代替部は、テキスト順にグループ化される(レイアウトセンシティブモードを選択していない場合は、セミコロンによって分割される)ことが必要である。
- 各関数代替部は、同一の関数記号で始まらなければならない。
- 関数は一定のアリティを持つので、各規則では、同一数の仮引数を指定しなければならない。関数は、カリー化して使用できるし、高階関数型言語で通常のように、どんな数の引数にも適用できる。
- 関数名は、原則として、同一の名前空間と同一の通用範囲内の他の名前とは異なっていなければならない(2.1参照)。しかし、関数と演算子を多重定義できる(6章参照)。
CLEANモジュールの関数定義の例
module example // モジュールヘッダ
import StdInt // 黙示的インポート
map:: (a -> b) [a] -> [b] // mapの型
map f list = [f e \\ e <- list] // 関数mapの定義
square:: Int -> Int // squareの型
square x = x * x // 関数squareの定義
Start:: [Int] // Start規則の型
Start = map square [1..1000] // Start規則の定義
演算子とは、中置演算子(括弧を省略する)か通常の前置演算子(引数に先行する演算子名は、括弧に囲まれていなければならない)として使用できる、アリティ2を持つ関数である。演算子の型定義(3.7.1参照)で定義できる優先順位(0から9)と固定性(infixleft、infixright、infix)は、式内の演算子適用の優先度を決める。より高い優先順位であれば、よりタイトに束縛される。演算子が同一優先順位を持つ場合、その固定性が優先度を決める。
- 演算子が中置で使用される場合、両側に引数が存在していなければならない。演算子は、カリー化された方法で使用できるが、その時には、通常の前置関数として使用しなければならない。
演算子定義
(++) infixr 0 :: [a] [a] -> [a]
(++) [] ly = ly
(++) [x:xs] ly = [x:xs ++ ly]
(o) infixr 9 :: (a -> b) (c -> a) -> (c -> b)
(o) f g = \x = f (g x)
関数定義の左手側で指定されるパターンによって、関数の仮引数が指定される。関数代替部は、関数適用の実引数が仮引数に照合する場合にのみ選択される。仮引数は、定数(部分パターンから構成できるオプションの引数を持つデータ構成子)か変数のどちらかである。
パターン = [変数 =:] 括弧パターン
括弧パターン = (グラフパターン)
| 構成子
| パターン変数
| 特殊パターン
| ダイナミックパターン
グラフパターン = 構成子 {パターン} // 通常のデータ構成子
| グラフパターン 構成子名 グラフパターン // 中置データ構成子
| パターン
パターン変数 = 変数
| _
パターン変数は、(ノード)変数かワイルドカードであればよい。変数とは、関数の実引数のあらゆる具体的値に照合する、関数の仮引数であるので、この引数の評価を強制しない。ワイルドカードとは、対応する引数が関数の右手側では使用されないことを指示するのに使用できる無名変数("_")である。変数は、(記号'=:'を使用して)パターンに接着でき、そのことによって、その内容だけではなくパターン全体を識別(ラベル付け)できるようになる。定数(データ構成子)が仮引数として指定される場合には、実引数は、照合を成功させる為に、同一定数を保持していなければならない。
代数データ型定義の例と、関数定義のパターン照合での使用例
::Tree a = Node a (Tree a) (Tree a)
| Nil
Mirror:: (Tree a) -> Tree a
Mirror (Node e left right) = Node e (Mirror right) (Mirror left)
Mirror Nil = Nil
無名変数の使用例
:: Complex :== (!Real,!Real) // シノニム型定義
realpart:: Complex -> Real
realpart (re,_) = re // reと_はパターン変数
パターンと部分パターンを識別するリストパターン、ガードと変数の使用法。
mergeは、2つの(ソートされた)遅延リストを1つの(ソートされた)リストに結合する
merge:: [Int] [Int] -> [Int]
merge f [] = f
merge [] s = s
merge f=:[x:xs] s=:[y:ys]
| x<y = [x:merge xs s]
| x==y = merge f ys
| otherwise = [y:merge f ys]
- 指定したパターンによって、関数を部分関数(3.7.3参照)に変えることができる。部分関数が、その関数の定義域の外側で適用されると、実行時エラーをもたらす。コンパイル時には、そのような状況が発生するかもしれないという警告が生成される。
関数の仮引数とその関数本体は、新しい局所通用範囲内に保持される。
- 関数定義の左手側に導入される全ての変数記号は、異なった名前を持たなければならない。
簡便性と効率性の為、定義済みの型とレコード型のデータ構造のパターン照合を表現する特別な構文を提供している。これらは、他の所で取り扱う(以下参照)。
特殊パターン = 基本値パターン // 4.1.2参照
| リストパターン // 4.2.2参照
| 組パターン // 4.3.2参照
| 配列パターン // 4.4.2参照
| レコードパターン // 5.2.2参照
ガード = 論理式
| otherwise
ガード(guard)とは、規則代替部に付けられた、パターン照合機構の汎化と見ることができる論理式である。つまり、左手側で定義されたパターンが照合し、且つ、その(オプションの)ガードがTrueに評価された場合にのみ、代替部が照合する(3.1参照)。そうでなければ、次の関数代替部が試行される。パターン照合は常に、ガードが評価される前に発生する。
このガードは、テキスト順に試行される。Trueを出力する最初のガードに対応する代替部が評価される。ガードを持たない右手側は、常にTrueと評価されるガードを持つものと見ることができる('otherwise'又は'デフォルト'ケース)。キーワードでは、otherwiseは、デフォルトオプションを強調したい人々にとっては、Trueのシノニムである。
- 関数の最後の規則代替部のみが、ガードとしてotherwiseを持つことができるが、ガードを持たなくともよい。
- ガードは、関数を部分関数に変えることができる(3.6.3参照)。部分関数がその定義域の外側に適用される場合には、実行時エラーがもたらされる。コンパイル時には、これを検知できない。
ガードを持つ関数定義
filter:: Int [Int] -> [Int]
filter pr [n:str]
| n mod pr == 0 = filter pr str
| otherwise = [n:filter pr str]
前のfilterに同等な定義
filter:: Int [Int] -> [Int]
filter pr [n:str]
| n mod pr == 0 = filter pr str
= [n:filter pr str]
ガードは、入れ子にできる。あるレベルのガードがTrueに評価される場合、その次のレベルのガードが試行される。
- 入れ子されたガードの少なくとも1つが成功であると保証する為に、入れ子ガード代替部は、最後の代替部として'デフォルトケース'を常に持たなければならない。
入れ子されたガードの例
例 引数1 引数2
| 述語11 引数1 // if 述語11 引数1
| 述語21 引数2 = 計算1 引数1 引数2 // then (if 述語21 引数2
| 述語22 引数2 = 計算2 引数1 引数2 // elseif 述語22 引数2 then
| otherwise = 計算3 引数1 引数2 // else ...)
| 述語12 引数1 = 計算4 引数1 引数2 // elseif 述語12 引数1 then ...
関数の主たる本体は、ルート式と呼ばれる。ルート式は、グラフ式である。
ルート式 = グラフ式
グラフ式 = 適用
適用 = {括弧グラフ}+
| グラフ式 演算子 グラフ式
括弧グラフ = グラフ変数
| 構成子
| 関数
| (グラフ式)
| ラムダ抽象 // 3.4.1参照
| Case式 // 3.4.2参照
| Let式 // 3.5.1参照
| 特殊式
| ダイナミック式
関数 = 関数名
| (関数名)
構成子 = 構成子名
| (構成子名)
演算 = 関数名
| 構成子名
グラフ変数 = 変数
| 選択子変数
式は一般に、実引数に対する関数の適用か又は、単純にデータ構成子を引数に適用することによるデータ構造の(自動的)生成を表現する。各関数又はデータ構成子は、カリー化された方法で使用できるので、任意数(0以上)の引数に適用できる。0引数に適用される関数と構成子は、構文上の一単位を形成するだけである(この場合、非演算子には、括弧は不要である)。
- 全ての式は、正当な型でなければならない(5章参照)。
- 式内で現れる全記号は、その式が現れる通用範囲内のどこかで定義されていなければならない。
- グラフ式の通用範囲内には、各ノード変数と選択子変数の定義がなければならない。
演算子とは、中置で適用できる、アリティ2を持って定義された特殊な関数又はデータ構成子である。演算子の型定義内で定義可能な優先順位(0から9)と固定性(infixleft、infixright、infix)は、式内の演算子適用の優先度を決める。より高い優先順位であれば、よりタイトに束縛される。演算子が等しい優先順位を持つ場合、固定性が優先度を決める。式内では、通常の関数適用は、非常に高い優先度(10)を持つ。レコード要素(5.2.1参照)と配列要素(4.4.1参照)の選択だけが、よりタイトに(11)束縛される。その他に、その優先度によって、括弧を省略できる場合がある。つまり、演算子適用は、ちょうど他の適用のように動作するのである。
- 式内で等しい優先順位を持つ演算子を、その固定性が衝突する方法で適用することは認められていない。従って、a1 op1 a2 op2 a3において、演算子op1とop2が同一の優先順位を持つ場合には、ある衝突が発生する。つまり、op1がinfixlと定義される場合、その式はa1 op1 (a2 op2 a3)と読まれなければならない一方で、op2がinfixlと定義される場合、その式は、(a1 op1 a2) op2 a3と読まれなければならない。
- 演算子が中置で使用される場合、両側に引数が存在していなければならない。演算子は、カリー化された(2つより少ない引数に適用される)方法で使用できるが、その場合には、通常の前置関数/構成子として使用されなければならない。演算子が通常の前置関数/構成子として使用される場合、それを括弧で囲まなければならない。
グラフ式内に現れることのできる変数には2種ある。つまり、関数の仮引数として導入される変数(3.1と3.2参照)と、(グラフ式の部分を識別する選択子内に定義される)選択子変数(3.6参照)である。
循環ルート式の例。yは、循環グラフを参照するルート式である。
乗算演算子*は、ここでは、カリー化された方法で、前置して使用される。
ham :: [Int]
ham = y
where y = [1:merge (map ((*) 2) y) (merge (map ((*) 3) y) (map ((*) 5) y))]
簡便と効率性の為に、特別な構文を提供して、定義済みの型と、特種な代数型とみなされるレコード型のデータ構造の式を生成する。これらは他の所で取り扱う。
特殊式 =| 基本値 // 4.1.1参照
| リスト // 4.2.1参照
| 組 // 4.3.1参照
| 配列 // 4.4.1参照
| 配列選択 // 4.4.1参照
| レコード // 5.2.1参照
| レコード選択 // 5.2.1参照
"正に即席で"式内に小さな関数を定義することが便利な場合がある。この為に、ラムダ抽象(lambda abstraction)を定義できる。通常の関数定義では一般的なように、パターンであってもよい複数の仮引数を持つことのできる無名関数が定義される(3章参照)。しかし、この方法では、シンプルな関数しか定義できない。つまり、ガード、規則代替部と局所定義はない。
CLEAN 1.3との互換性の為、関数本体から仮引数を分離するのに矢('->')を使用することも認められている。
ラムダ抽象 = \ {パターン} = グラフ式
| \ {パターン} -> グラフ式
ラムダ式の例
AddTupleList:: [(Int,Int)] -> [Int]
AddTupleList list = map (\(x,y) = x+y) list
ラムダ式は、新規の通用範囲を導入する(2.1参照)。
定義されている無名関数の引数は、それに対応する関数本体でただ1つの意味しか持たない。

プログラミングの簡便の為に、case式と条件式(conditional expression)を加えている。
Case式 = case グラフ式 of
{ {Case代替部定義}+ }
| if 括弧グラフ 括弧グラフ 括弧グラフ
Case代替部定義 = {パターン}
{{LetBefore式}
{| ガード} = [>] 関数本体}+
[局所関数代替部定義]
| {パターン}
{{LetBefore式}
{| ガード} -> 関数本体}+
[局所関数代替部定義]
case式では、まず、弁別する式が評価され、その後、case代替部がテキスト順に試行される。case代替部は、関数代替部に似ている。これはそう奇妙なことではない。というのも、case式は内部的には、関数定義に翻訳されるからである(以下の例参照)。各代替部は、左手側パターン(3.2参照)を持ち、それには、let-before(3.5.4参照)とガード(3.3参照)がオプションとして後続する。パターンが照合し、オプションのガードがTrueに評価されると、それに対応する代替部が選択される。各case代替部には、新しいブロック構造(通用範囲)が生成される(2.1参照)。
CLEAN 1.3.xとの互換性の為に、case代替部を分ける矢('->')を使用することも認められている。
パターン内で定義される変数は、それに対応する代替部においてただ1つの意味を持つ。

- case式の全ての代替部は、同一型でなければならない。
- パターンのどれも照合しない場合、実行時エラーが生成される。
case式
h x = case g x of
[hd:_] = hd
[] = abort "result of call g x in h is empty"
は、意味的には以下のものと同等である。
h x = mycase (g x)
where
mycase [hd:_] = hd
mycase [] = abort "result of call g x in h is empty"
条件式では、まず、論理式が評価され、その後、then又はelse部が選択される。条件式は、単純な種類のcase式と見ることができる。
- 条件式のthenとelse部は、同一型でなければならない。
- 弁別式は、型Boolでなければならない。
局限された通用範囲を持ち、モジュール全体に渡っては可視的でない定義を導入することが便利である場合がある。局所通用範囲、つまり、特定のプログラム領域でしか意味を持たない関数を定義できる。その通用範囲の外側では、その関数は見えない。この局所性は、より良いプログラム構造を得るのに使用できる。つまり、特定のプログラム領域でしか使用されない関数は、その領域の外側では隠されたままでいることができる。
関数の他に、定数選択子を定義することも便利な場合がある。定数は、グラフ式と名付けられている(3.6参照)。
局所定義 = グラフ定義
| 関数定義
let式とは、局所関数と定数を定義可能な式内に新しい通用範囲(2.1参照)の導入を可能にする式である。このような局所定義は、以下の構文を持つlet式を使用すれば、式内のどこででも導入できる。
Let式 = let {{局所定義}+ } in グラフ式
letブロックで定義された関数と選択子は、以下のexpression内でただ1つの意味しか持たない。

リストの内包表記内で使用されるlet式の例
doublefibs n = [let a = fib i in (a, a) \\ i <- [0..n]]
各関数代替部の最後では、whereブロックの中に関数と定数グラフを局所的に定義できる。
局所関数代替部定義 = [where] {{局所定義}+ }
whereブロック内で定義された関数とグラフは、whereブロックの通用範囲を示す以下の図で示されるように、対応する関数代替部(つまり、パターンに従う全てのガードと規則代替部、3.1参照)内のどこででも使用できる。
whereブロックで定義された関数と選択子は、関数定義全体において局所的に使用できる。

sieveとfilterは、whereブロック内で定義された局所関数である。これらは、primesの内側でのみ意味を持つ。
大域レベルでは、この関数は見えない。
primes::[Int]
primes = sieve [2..]
where
sieve::[Int] -> [Int] // primesの局所関数
sieve [pr:r] = [pr:sieve (filter pr r)]
filter::Int [Int] -> [Int] // primesの局所関数
filter pr [n:r]
| n mod pr == 0 = filter pr r
| otherwise = [n:filter pr r]
この通用範囲規則は、周囲の関数代替部の仮引数が、局所定義された関数とグラフには可視的であるように存在していることに注意。従って、引数は、局所定義内で直接使用できる。このような局所定義は、常に明示的に型付けしなければならない訳ではない(3.7参照)。
primesの代替的な定義。関数filterは、sieveに局所定義されている。
filterは、sieveの引数prに直接にアクセスできる。
primes::[Int]
primes = sieve [2..]
where
sieve::[Int] -> [Int] // primesの局所関数
sieve [pr:r] = [pr:sieve (filter r)]
where
filter::[Int] -> [Int] // sieveの局所関数
filter [n:r]
| n mod pr == 0 = filter r
| otherwise = [n:filter r]
withブロックを使用すると、各ガード規則代替部の最後でも、関数とグラフを局所定義できる。
局所関数定義 = [with] {{局所定義}+ }
局所定義 = グラフ定義
| 関数定義
withブロック内で定義された関数とグラフは、withブロックの通用範囲を示す以下の図で指示されるように、対応する規則代替部の中でのみ使用できる。
withブロック内で定義された関数と選択子は、それに対応する関数代替部内でのみ局所的に使用できる。

この通用範囲規則は、周囲のガードされた規則代替部の引数が、局所定義された関数とグラフに可視的であるように存在することに注意して欲しい。従って、引数は、局所定義において直接に指示できる。このような局所定義は常に、明示的に型付けしなければならない訳ではない(3.7参照)。
CLEAN I/Oライブラリの入出力関数の多くは、状態遷移関数である。このような状態はしばしば、ある関数からまたある別の関数に、シングルスレッドな方法で渡され(9章参照)、特定の評価順序を強制する。これは、この状態が一意型である場合には確実に当て嵌まる。このスレッド化した引数は、その様々なバージョンを識別する為に改名されなければならない。以下の例は、典型的な例を示している。
状態遷移関数の使用。一意に型付けられた状態ファイルは、file、file1、file2と多くの改名を伴いながら、
ある関数からまたある別の関数に渡される。
readchars:: *File -> ([Char], *File)
readchars file
| not ok = ([],file1)
| otherwise = ([char:chars], file2)
where
(ok,char,file1) = freadc file
(chars,file2) = readchars file1
スレッド化された引数のこの明示的な改名は、非常に醜く見えるだけではなく、この種の定義を読むのも困難である場合がある(どの順序で何が起こっているのか?どの状態がどんな状況で渡されているのか?)。我々は次のことを認めなければならない。即ち、命令型のプログラミングスタイルの方が、I/Oを行う場合のような、物事が特定の順序で発生しなければならない場合には、読むのが遥かに簡単である。そういう訳で、let-before式を導入した。
let-before式は、ガード又は関数本体の前に定義できる特別なlet式である。この方法では、意図した発生順序で逐次的な動作を指定できる。let-before式は、以下の構文を持つ。
LetBefore式 = # {グラフ定義}+
| #!{グラフ定義}+
びっくりマーク(#!)を持つ形は、定義の左手側に現れるノードidの評価を強制する。Let-before式内では定数選択子(グラフ定義)しか定義できないことに注意。関数は定義できない。
let-before式は、命令型プログラミングの外見を得る為に、特別な通用範囲規則を持っている。これらの定義の左手側の変数は、その定義の右手側の通用範囲内には現れないが、後続の他の定義の通用範囲内には現れる(ルート式は含むが、whereブロックの局所定義は含まない)。
このことは、以下の図で示されている。

let-before式内で定義された変数は、where式内では使用できないことに注意。その逆は真である。つまり、where式内の定義は、let before式内で使用できる。
let before式、短縮記法、let beforeの特別な通用範囲を使用している再利用名の例
readchars:: *File -> ([Char], *File)
readchars file
# (ok,char,file) = freadc file
| not ok = ([],file)
# (chars,file) = readchars file
= ([char:chars], file)
スレッド化した引数を改名する同等な定義
readchars:: *File -> ([Char], *File)
readchars file
# (ok,char,file1) = freadc file
| not ok = ([],file1)
# (chars, file2) = readchars file1
= ([char:chars], file2)
この記法は危険でもあり得る。つまり、様々な地点で同一名が使用される一方で、名前の意味は常に同じではない(定義から定義に渡って変化する通用範囲を考慮しなければならない)。しかし、この記法は、一意型の引数をスレッド化するのに使用する場合にはかなり安全である。型システムは、このような引数が、正しくシングルスレッドな方法で使用されない場合に、それを指摘するだろう。その他の場合に命令型のプログラミングスタイルを採用することについては、let before式の使用を勧めない。
let before式の濫用
exchange:: (a, b) -> (b, a)
exchange (x, y)
# temp = x
x = y
y = temp
= (x, y)
定数式(実際は、グラフ)を他の式で使用できる(そして、その他の式が共有できる)ように、その式に名前を与えることができる。選択子と呼ばれる射影関数を経由して、定数の特定の部分を識別することもできる(以下参照)。
グラフ定義 = 選択子 =[:] グラフ式 ;
関数内に局所定義されたグラフ:lastとラベル付けられたグラフは、
関数StripNewline内で共有され、ただ一回だけ計算される。
StripNewline:: String -> String
StripNewline "" = ""
StripNewline string
| string !! last<>'\n' = string
| otherwise = string%(0,last-1)
where
last = maxindex string
実際にグラフが定義されると、式(の一部)に名前が与えられる。グラフの定義は、定数(データ)又は定(射影)関数の定義と比較できる。しかし、複数の同一グラフへの参照はそのグラフの共有をもたらすという意味を持つCLEANの基礎の意味論(1章参照)に従って、グラフは構成されるということに注意して欲しい。再帰的な参照は、循環グラフ構造(cyclic graph structure)をもたらすだろう。グラフは、それらがただ一回だけしか計算されず、その値は、定義されている通用範囲内で記憶されているという特性を有している。
グラフ定義は、定関数定義とは異なる。定関数定義は、アリティ0で定義された関数に過ぎない(3.1参照)。定関数定義は、通常のグラフ書換規則を定義する。つまり、ある関数への複数の参照は、(定)関数が、作られる関数記号の各出現毎に再計算されるように、それの同一定義が使用されるということを意味するに過ぎない。この違いは、関数定義の時間及び領域動作に影響を持つ場合がある(10.2参照)。
局所定義された循環定数グラフを使用して定義されたハミング数と、
大域定議された再帰的定関数によって定義されたハミング数。
最初の定義(ham1)は効率的である。というのも、既に計算された数が、共有を経由して再利用されるからである。
第2の定義は(ham2)の方が遥かに非効率である。というのも、再帰関数は、全てを再計算するからである。
ham1:: [Int]
ham1 = y
where y = [1:merge (map ((*) 2) y) (merge (map ((*) 3) y) (map ((*) 5) y))]
ham2:: [Int]
ham2 = [1:merge (map ((*) 2) ham2) (merge (map ((*) 3) ham2) (map ((*) 5) ham2 ))]
構文の上では、グラフの定義は、右手側から左手側を分割する記号によって、関数の定義と区別される。つまり、"=:"はグラフに使用されるが、関数には"=>"が使用される。しかし、一般的には、より一般的な記号"="が両方の型定義に使用される。一般に、その文脈から、意味されるものは明確である(関数は引数を持つ、選択子も認識が容易である)。しかし、シンプルな定数が定義される場合、その構文は曖昧である(これは、定グラフ定義でもあり得るし、定関数定義でもあり得る)。
可能な限りいつでも"="の使用を認める為に、以下の規則に従う。局所定数定義は、デフォルトでは、グラフ定義とみなされ、従って、共有されるが、大域的には、それはデフォルトでは、関数定義とみなされ(3.1参照)、従って、再計算される。もし異なった動作を得たいならば、"=:"(大域レベルで、共有される定数グラフを意味する)又は"=>"(局所レベルで、定関数であり、再計算されなければならないことを意味する)を使用することによって、定数定義の性質(共有されなければならないか、再計算されなければならないか)を明示的に述べなければならない。
局所定グラフ対局所定関数定義:biglist1とbiglist2は、ただの一回しか計算されないグラフであり、
biglist3は、適用される毎に計算される定関数である。
biglist1 = [1..10000] // (局所定義されれば)グラフ
biglist1 = [1..10000] // (大域定議されれば)定関数
biglist2 =: [1..10000] // (常に)グラフ
biglist3 => [1..10000] // (常に)定関数
ガーベッジコレクタは、プログラムグラフのルートに最早繋がっていない局所定義グラフを集める(1章参照)。
グラフ定義の左手側は、シンプルな名前でもよいが、選択子と呼ばれるより複雑なパターンでもよい。選択子(selector)とは、定義されている定数グラフ(の一部)を識別する為に、射影関数(projection function)を黙示的に定義する、1つ以上の新しい選択子変数を導入するパターンである。部分グラフを全体として識別したり、その部品を識別したりできる。選択子は、定数(代数型定義によって導入されるユーザー定義定数でもある)、変数及びワイルドカードを保持できる。ワイルドカードによって、特定の部品には関心がないことを指示できる。
選択子は大域定義できない。これは、let(3.5.1参照)、let-before(3.5.4参照)、whereブロック(3.5.2参照)とwithブロック(3.5.3参照)で定義できるのみである。更に、選択子は、リストの内包表記(4.2.1参照)と配列の内包表記(4.4.1参照)の生成子の左手側に現れることができる。
選択子 = 括弧パターン // 括弧パターンは、3.2参照
組要素を局所的に選ぶ選択子の使用例
unzip::[(a,b)] -> ([a],[b])
unzip [] = ([],[])
unzip [(x,y):xys] = ([x:xs],[y:ys])
where
(xs,ys) = unzip xys
- グラフ定義の左手側の選択子が、右手側のグラフに照合しない場合、実行時エラーがもたらされる。
- 選択子に導入される選択子変数は、互いに異なっており、同一通用範囲及び名前空間内で既に使用されていてはならない(1.2参照)。
- 実行時に失敗するかもしれないパターンの使用を回避する為、0アリティの構成子でテストすることは認められていない。例えば、選択子パターン内で使用されるリストは、[a:_]という形でなければならない。[a]は使用できない。というのも、これは、0アリティ構成子[]でのテストを含意する[a:[]]を表しているからである。もしパターンがレコードならば、関心のある内容を持つフィールドだけを、パターン内で指示すればよい。
- 配列は、選択子内のパターンとして使用できない。
- 選択子は大域定義できない。
一般的には、関数の型を明示的に指定する義務はないが(CLEANコンパイラは、型を一般的に推論できる)、明示的な型仕様は、プログラムの可読性増大の為に高く勧められる。
関数定義 = [関数の型定義]
関数定義
関数の型定義 = 関数名 :: 関数の型 ;
| (関数名) [固定性][優先順位] [:: 関数の型] ;
固定性 = infixl
| infixr
| infix
優先順位 = 数字
関数の型 = 型 -> 型 [クラス文脈] [一意型不等式]
型 = {括弧型}+
括弧型 = [全称量化変数] [正格] [一意型属性] 単純型
全称量化変数 = A.{型変数 }+:
関数がエクスポートされる場合や、プログラマが関数適用に追加的な制限を課したい場合には、明示的な仕様が必要である(例えば、より制限的な型を指定できる。10.1で説明されるように、正格性情報を追加できる。7章で説明されるように、多重定義の表現の為に、型変数にクラス文脈を定義できる。3.7.5(正格な引数を持つ関数)で説明されるように、一意性情報を付加できる。)。
CLEAN型システムは、Milner/Mycroft型割当ての組合せを使用している。これは、結果として、ある稀な場合には、型システムは、与えられた型を承認できる(Mycroftシステムを使用している。PlasmeijerとVan Eekelen, 1993参照)が、関数の型を推論できない(Milner/Hindleyシステムを使用している)ということをもたらす。また、ランク2の全称量化型(3.7.4参照)が使用される場合、プログラマによる明示的な型付けを必要とする。
関数の型仕様には、デカルト積(カルテシアン積、直積)が使用される。デカルト積は、[]で囲まれた引数の型の並記によって表される。単一引数の場合には、[]を省略できる。型の仕様内では、型構成子の適用の束縛優先度は、矢->の束縛よりも高い。演算子を定義することを指示する為に、関数名は、左手側で、()によって囲まれる。
- ::の前の関数記号は、それに対応する書換規則の関数記号と同じでなければならない。
- 関数のアリティは、引数の数(そのデカルト積が取られる)に対応していなければならない。従って、CLEANでは、関数の型によって、そのアリティを伝えることができる。
関数のアリティがどのように型に反映されているかを示している
map:: (a->b) [a] -> [b] // mapは、アリティ2を持つ
map f [] = []
map f [x:xs] = [f x : map f xs]
domap:: ((a->b) [a] -> [b]) // domapは、アリティ0を持つ
domap = map
- 関数の引数と結果型は、種Xでなければならない。
- 局所定義関数の型仕様の中では、周囲の関数の型仕様で導入された型変数を参照できない(定義された型の通用範囲規則はまだない)。従って、プログラマは、このような局所関数の型を指定できない。しかし、型システムによって、その型は、(コンパイラによって大域レベルに持ち上げられた後で)推論され、検査される。
反例(不法な型仕様)。関数gは、1つの組を返す。最初の組要素の型は、fの多相引数の型と同じである。
(ここで"^"によって指示される)このような依存関係は指定できない。
f:: a -> (a,a)
f x = g x
where
// g:: b -> (a^,b)
g y = (x,y)
CLEANでは、全ての記号(関数と構成子)は、固定したアリティで定義される。しかし、適用では、それらを任意数の引数に適用することが、勿論認められている。関数のカリー化適用は、そのアリティより少ない引数の数を持つ関数の適用である(CLEANでは、関数のアリティは、その型から導出できることに注意)。定義済み内部関数_APの助けによって、必要とされる数の引数に適用されるカリー化関数は、同等なカリー化関数適用に変換される。
CLEAN型システムの型公理には、アリティnで定義された全てのsに対して、s :: (t1->(t2->(...(tn->tr)...))が、s :: t1 t2 ... tn -> trと同等であるということが含まれる。
演算子(operator)とは、中置で使用できるアリティ2を持つ関数である。演算子は、関数定義の左手側の()の間の演算子名を囲むことによって、定義できる。演算子は、優先順位(precedence)(0から9、デフォルトは9)と固定性(fixity)(infixl、infixr又は単にinfix、デフォルトはinfixl)を持つ。より高い優先順位であれば、よりタイトに束縛される。演算子が等しい優先順位を持つ場合には、固定性がその優先度を決める。式内では、通常の関数適用は、常に最高の優先度(10)を持っている。セクション3.1と3.4も参照。
- 演算子の型は、アリティ2の関数を型付けする為に定義されている要件に従わなければならない。
- 演算子が明示的に型付けされる場合、演算子名はまた、型規則の()の間に置かれなければならない。
- 中置演算子が()の間に囲まれる場合、前置関数として適用できる。右手側に新しく定義された演算子のあり得る再帰定義も、この規約に従う。
演算子定義とその型の例
(o) infix 8:: (x -> y) (z -> x) -> (z -> y) // 関数合成
(o) f g = \x -> f (g x)
パターンとガードは、書換規則を適用できる前に果たされなければならない条件を示している(3.2と3.3参照)。このことによって、部分関数(partial function)、つまり、指定した型の全てのあり得る値に対しては定義されていない関数を定義することが可能となる。
- 部分関数が、その関数の定義域の外側の値に適用される場合、実行時エラーがもたらされる。コンパイラは、部分的かもしれない関数が定義されると、警告を与える。
abort式(StdMisc.dcl参照)によって、あらゆる部分関数を全体関数(total function)に変更できる(abort式はあらゆる型を持つことができる)。abort式は、ユーザー定義の実行時エラーメッセージを与える為に使用できる。
関数を全体的にするabortの使用例
fac:: Int -> Int
fac 0 = 1
fac n
| n>=1 = n * fac (n - 1)
| otherwise = abort "fac called with a negative number"
多相関数の型をCLEANで指定する場合、一般的には、全称量化子を省略する。
関数mapはいつものように定義されており、全称量化子を指定していない。
map:: (a->b) [a] -> [b]
map f [] = []
map f [x:xs] = [f x : map f xs]
反例。再び同一の関数mapだが、ここでは、黙示的に想定されていた全称量化子を可視的にした。
指定した型の意味をより正確に示しているが、型定義も少し長くなっている。
Cleanの現行版は、まだ最上レベルでは全称量化子を認めていない!!!
map:: A.a b:(a->b) [a] -> [b]
map f [] = []
map f [x:xs] = [f x : map f xs]
未実装:Clean 2.0では、全称量化子を明示的に書き下すことが認められている。関数の型定義の::の直後に、量化子A.(for all)を書き下すことができる。この方法では、関数の型定義に使用される型変数を明示的に導入できる。通常は、そのようにして導入された型変数は、通用範囲としては、関数型定義全体を持つ。
関数の型 = 型 -> 型 [クラス文脈] [一意型不等式]
型 = {括弧型}+
括弧型 = [全称量化変数] [正格] [一意型属性] 単純型
全称量化変数 = A.{型変数 }+:
実装済み:Clean 2.0では、ランク2多相性を提供する。つまり、通用範囲として、関数の引数の型又は関数の結果の型に、全称量化子を指定することもできる。これによって、引数として、多相関数を、そうでなければ単相的に扱われていた関数に渡すことができる。ランク2多相性を使用することの利点は、より多くのプログラムが型システムによって承認されるということだが、その場合には、関数の型に、(全称量化子を書き下すことによって)そのような多相関数が引数として取られ、結果として出力されることを明示的に指定しなければならない。
例:関数hは、型(A.a : [a] -> Int)の多相関数を、CharのリストだけでなくIntのリストにも適用するのに使用される。
hの型仕様に全称量化子を明示的に使用することによって、この定義は認められる。
:: (A.a: [a] -> Int) -> Int
h f = f [1..100] + f ['a'..'z']
Start = h length
反例:関数h2は、型([a] -> Int)の関数を、CharのリストだけでなくIntのリストにも適用するのに使用される。
この場合、型単一化エラーによって、この定義は拒絶される。
h2の引数は、[a] -> Intで単一化可能であると想定されるが、h2の引数が(A.a :: [a] -> Int)であるとは想定されない。
従って、型変数aは、IntとCharの両方で単一化され、型エラーをもたらす。
h2:: ([a] -> Int) -> Int
h2 f = f [1..100] + f ['a'..'z']
Start = h2 length
反例:関数h3は、関数を、Charのリストだけではなく、Intのリストにも適用するのに使用される。
型を指定していないので、型推論システムは、fは、型([a] -> Int)であって、型(A.a :: [a] -> Int)ではないと想定するだろう。
この状況は、上と同一であり、再び型エラーを得るだろう。
h3 f = f [1..100] + f ['a'..'z']
Start = h3 length
- CLEANは、ランク2の多相関数を自動的に推論できない!明示的にランク2の全称量化型を指定しなければならない。
- ランク2より高いランクの明示的な全称量化(例えば、関数引数の型仕様の内側のどこかで指定された量化子)は認められていない。
- ランク2の多相関数は、関数が全称量化されている引数に対しては、カリー化した方法では使用できない。
反例:以下の例では、f1は、その最後の引数が全称量化されているので、
全ての引数に適用される場合にのみ使用できることがわかる。
関数f2は、全称量化されていない最後の引数に関してのみ、カリー化して使用できる。
f1:: a (A.b:b->b) -> a
f1 x id = id x
f2:: (A.b:b->b) a -> a
f2 id x = id x
illegal1 = f1 // これは型エラーをもたらすだろう。
illegal2 = f1 3 // これは型エラーをもたらすだろう。
legal1 :: Int
legal1 = f1 3 id where id x = x // ok
illegal3 = f2 // これは型エラーをもたらすだろう。
legal2 :: (a -> a)
legal2 = f2 id where id x = x // ok
legal3 :: Int
legal3 = f2 id 3 where id x = x // ok
関数の型定義では、引数が正格であるとして任意に注釈を付けることができる。関数について推論する際には、関数書換え発生前に、それに対応する引数が強ルート正規形の中にある(2.1参照)ということが常に真であるだろう。一般に、正格性情報は、実行効率性を増大する(10章参照)。
関数の型 = 型 -> 型 [クラス文脈] [一意型不等式]
型 = {括弧型}+
括弧型 = [全称量化変数] [正格] [一意型属性] 単純型
正格注釈を付けられた引数を持つ関数の例
Acker:: !Int !Int -> Int
Acker 0 j = inc j
Acker i 0 = Acker (dec i) 1
Acker i j = Acker (dec i) (Acker i (dec j))
CLEANコンパイラは、抽象簡約(abstract reduction)(Nocker, 1993)に基づく高速で優れた正格性アナライザを持つ。コンパイラは、上記例のような多くの場合に、関数引数の正格性を導出できる。従って、一般的に、自分の手で関数の型に正格性注釈を加える必要は無い。関数がモジュールからエクスポートされる場合(2章参照)、定義モジュール内でその型を指定しなければならない。最適な効率性を得る為には、プログラマは、定義モジュールの型定義にも正格性情報を付けなければならない。ただ、コンパイラに、導出された正格性情報を持つ型を印字し、これを定義モジュールに貼り付けるよう指示することはできる。
- 正格性注釈は、引数の型の最外レベルでのみ認められるということに注意して欲しい。引数の型インスタンスの内側で正格性注釈を付けることはできない(組とリストのようないくつかの定義済みの型は除くが)。データ構造(のあらゆる部分)は、遅延から正格に変更できるが、その場合には、このことを型定義に指定しなければならない(5.1.5参照)。
First Uploaded : December 26, 2002
Last Modified : January 19, 2003
Back