[index]

CLEAN による関数プログラミング
準備体操編

Pieter Koopman
HILT BV, The Netherlands

オリジナルは Clean チーム (HILT および ナイメーヘン大学) の Pieter Koopman 氏作成の Tutorial introduction です。

あらゆる意味におけるコメントを強く強く希望します。


このテキストの意図するところは、いくつかの小さな例題を通して CLEAN による関数プログラミングの主要な特徴を示すことにあります。 言語の実際の偉力というものは実際的なアプリケーションの中でのみ浮かび上がるものだとは言え、一連の例題によって、簡潔でしかも精密な言語構造が印象付けられることでしょう。 言語の詳解は CLEAN reference manual を参照してください。 完全な CLEAN による関数プログラミング入門は、 CLEAN book (工事中) に収められます。

このドキュメントは以下の節から成ります。

目次

上記以外にも、 遅延評価無限データ構造局所的な定義 にも簡単に触れます。

馴染みのない言語というものは、みな特別な構文と言語構造を持っているものです。 例題のコードが判じ物のように感じられるなれば、ちょっと時間を取って解説を読んでみてください。 この言語の性質は C, C++ や Java のような性格の言語とはいささか異なっています。 最初に一瞥してその造りが理解できなかったとしても、この言語のせいにしないでください。 CLEAN による関数プログラミングを学ぶことは、 C++ や Java で同じようなものを書くことができるプログラマになることよりも、ずっと簡単なのです。

プログラミングにおける基本的な記法に関しては、すでに慣れているものとします。

基本的な関数の構造

CLEAN のような関数型言語における関数定義は、数学での関数の定義とたいへんに佳く似ています。 多くのプログラミング言語で習慣的になっている構文的なバラスト (ballast) のほとんどは、用いなくても済みます。 引数を囲む括弧すらも、用いる必要はありません。 数値を二乗する square 関数はこのように定義されます。

    square :: Int -> Int // 関数の型: Int から Int へ
    square n = n * n     // 値は引数に引数自身を掛けた値

最初の行は、この関数の型 type を与えています。引数としてひとつの整数、その結果として一つの整数 (を取る関数) です。 二行目は、この関数 square が任意の引数 n を用いて適用された結果が、どのように計算されなければならないのかを定義しています。つまり、引数に引数自身を掛けた値です。

選択を表わすには、関数の中でいくつか選択肢 function alternatives を定義することができます。 例えば、階乗関数は、以下のように定義されます。

    fac :: Int -> Int 
    fac 0 = 1
    fac n = n * fac (n - 1) 

このような定義では、実引数に当てはまる最初の選択肢が用いられます。 この戦略は、二番目の選択肢にもまた当てはまるとしても、まず最初の選択肢が式 fac 0 に適用されることを強制します。

CLEAN のプログラムは、 Start 式を計算します。 Start 関数の適切な定義を与えれば、どんな式の値も計算することができます。 6 の階乗は、以下のようなプログラムによって計算されます。

    Start :: Int 
    Start = fac 6 

Start 式の値がユーザに示されます。この例題では、プログラムはコンソールに 720 と出力されるでしょう。

Start 式の値がユーザに示されるということから、かの有名な "Hello world" プログラムは以下のようになります。

    Start :: String 
    Start = "Hello world" 

もちろん、一つ以上の引数を要求する関数はたくさんあります。 ある整数 xn を受け取って、 xn 乗を計算する簡単な例題を示します。

    power :: Int Int -> Int 
    power x 0 = 1
    power x n = x * power x (n-1) 

この関数は、たいていのプログラミング言語では中置演算子 ^ として表されます。 一つの中置演算子は、実際、二つの引数を取る関数であり、x ^ n のように二つの引数の間に書かれます。 CLEAN では、ユーザが中置演算子を定義することができます。 中置演算子は、他の関数と同じように定義されますが、わずかな違いが 2 点あります。 一つは、ここで示した関数の型が中置演算子であること、そして、中置演算子は定義の左側に括弧がある関数として書かれる、ということです。

    (^) infixr 8 :: Int Int -> Int 
    (^) x 0 = 1
    (^) x n = x * x ^ (n - 1) 

型宣言 の中に置かれる infixr キーワードは、それが書かれた関数が右結合を行う中置演算子であることを表しています。 数字 8 は、べき乗の束縛 (優先度レベル) を示します。 この、べき乗の束縛 (優先度レベル) は、数学の慣習に従って選択されます。 これは、 (x * x) ^ (n - 1)x * (x ^ (n - 1)) を識別することに、この演算子本体の二番目の選択肢で括弧が必要ではないことを意味します。 実際には、 CLEAN の標準環境では、この演算子は型クラスとして定義されています。

[目次]

ガード

関数定義におけるさまざまなケースでのパターンによる識別がいかに強力であるとはいえ、それだけでは充分ではありません。 例えば、パターンを使うだけでは、ある整数引数が正であるか、別の引数より大きいか否かを判断することは不可能です。 このような場合は、ガードを用いることで問題を解きます。 ガード guard は、関数選択肢のパターンと、そのシンボル = の間に挿入された論理式です。 シンボル | が、パターンと、そのガードを分けます。選択肢は、ガードが True を答える場合にのみ適用されます。 各関数選択肢は右辺に一連のガードを持つことができます。 関数 maximum を例にとってみましょう。この関数は、二つの引数のうち大きい方を答えます。

    maximum :: Int Int -> Int
    maximum n m
        | n < m = m
        | n >= m = n

この例題のように、このような関数定義の内部で、最後のガードを含む関数選択肢が必ず True を答えることが判っているならば、最後のガードは省略するか、キーワード otherwise で置き換えることもできます。 この関数の型は、比較されるべき引数が同じ型 t として与えられるのであれば、二つのどんな要素を用いてもよいことを示しています。

[目次]

高階関数

関数は他の関数の引数になることもできますし、他の関数の結果として返されることもあります。 いささか極端な例題として、関数 twice を考えてみましょう。 関数 twice は、ある一つの関数 f と、 f に渡す引数 x の二つを引数として受け取ります。 関数 f は引数 x を 2 回適用されます。

    twice f x = f (f x)

この関数を用いることで、 2 の二乗の二乗を計算するプログラムを以下のように書くことができます。

    Start = twice square 2

このプログラムは 16 を答えます。 同様に、 square (square (square (square 2))) を計算するプログラムは以下のように書くことができます。

    Start = twice twice square 2

このプログラムは 65536 を答えます。

多相性やクラス型と同じように、高階関数はプログラムを 理解しやすく、変更しやすく、再利用しやすく します。

[目次]

多相性

多くの関数では、引数の型は特定の型に限定されません。 このもっとも簡単な例題として、恒等関数 I を考えてみましょう。 この関数の引数は、その結果です。 この関数の引数の型は、特定の型に限定されません。 このことを示すために、このような (多くのフォームを持つ) 多相型関数の定義において、型変数 type variable を用います。

    I :: t -> t  // Type: from type t to type t.
    I x = x

前述した関数 twice も多相型です。 この例題では、 Int -> Int 型と同じように Int 型の x を用いました。

実際のところ、 先に定義された関数 square の型は、必要以上に制限されています。 この関数は、型 t において乗算が定義されているならば、どのような型 t にも用いることができます。 このことは form | * t の型制限 type restriction によって示されます。 より一般的な型は、次の関数 square の定義で用いられています。

    square :: t -> t | * t // Type: from t to t provided that there is a * for t.
    square n = n * n

関数 maximum も、上で定義されたものより一般的な型を持つように書くことができます。

    maximum :: t t -> t | < t
    maximum n m
        | n < m  = m
        | n >= m = n

CLEAN システムは「より大きいか等しい」 >= をどうやって計算するかを知っています。「より小さい」 < を見てください。

さまざまな型の引数を処理することができる関数は、多相 polymorph と呼ばれます。 * t のような、一つ以上の型変数を伴った型制限があるときは、多相型関数よりもむしろ暗黙の型クラス implicit type class を用います。

[目次]

リスト

リストは CLEAN の基本的な複合データ構造の一つです。 リストはどんなに多くの要素でも持つことができます。 リストの要素はすべて同じ型でなければなりません。 関数プログラミングではごくふつうに用いられるので、リストには特殊構文があります。 まず、整数からなるリストは [7, 12] or [1, 2, 3, 4, 5] のように列挙 enumeration として書くことができます。 式の中で [1..5] のようにリスト生成式を用いることもできます。 このような句はドット・ドット式 dot dot expression と呼ばれます。 空リストは [] と書かれます。 最後に、先頭要素 x と尾部 xs を持つリストは [x:xs] のように書かれます。二つの要素を持つリスト [x, xs][x:xs] の違いに注意してください。

このリスト記法は、関数のパターン内でも用いることができます。 例えば、リストの要素の積を計算する関数 product を考えてみます。 型宣言は、型クラス one と型クラス * がメンバーであるような型 t があるとき、この関数は引数として任意の型 t のリストを取り、型 t の値を計算する、と述べています。 このことは、ちょうど、乗算と単位型の要素 one が、型 t において、定義されていなければならないということを意味します。

    product :: [t] -> t | one, * t 
    product []    = one 
    product [x:r] = x * product r

この関数の最初の選択肢は、空リストの積は one であることを言明しています。 第二の選択肢は、リストを成立させている要素 x と尾部 r の積が、リストの最初の要素と尾部の積の乗算に等しいことを言明しています。 コンパイラは各々の適用において、クラス zero and * の適切なインスタンスを選択します。

階乗関数での選択肢の定義でも、同じように用いることができます。 ある数 n の階乗は、 1 から n までの数のリストの積に等しいのです。

    fac :: Int -> Int 
    fac n = product [1..n]

例えば、実数のリストの積を求めるなど、他のいろいろな型にも、同じ関数 product を用いることができます。

[目次]

リストの内包表記

関数型プログラミング言語 CLEAN のリストの内包表記は、リスト操作を表現するきわめて簡潔で強力な方法です。 例えば、 1 から 10 までの 3 で割り切れないすべての整数の二乗は、以下のように求められます

    Start = [n * n \\ n <- [1..10] | n rem 3 <> 0]

期待されるとおり、このプログラムは [1, 4, 16, 25, 49, 64, 100] を答えます。 一般的にリストの内包表記は、 [\\ との間に示された値のリストを答えます。 上記の値のリストは、 \\| との間に書かれた生成式が生み出した各要素を、 |] との間に書かれた条件式に従って計算した値です。 もっと実際的なプログラムとして、組のリストとして距離表を定義するプログラムをあげてみましょう。

    :: From :== String
    :: To   :== String
    :: Km   :== Int

    table :: [(From, To, KM)]
    table = [ ("Amsterdam", "Nijmegen"  , 122 )
            , ("Paris"    , "Amsterdam" , 490 )
            , ("Paris"    , "Rome"      , 1140)
            , ("Berlin"   , "Amsterdam" , 705 )
            , ("Amsterdam", "Kobenhaven", 764 )
            , ("Amsterdam", "Rome"      , 1640)
            , ("Moscow"   , "Amsterdam" , 2523)
            ]

リストの内包表記と、プログラム中に埋め込まれた表を用いることで、アムステルダムから 1000km 未満にあるすべての都市を選び出すことができます。

   Start
     = [  (to, dist)
       \\ ("Amsterdam", to, dist) <- table
       |  dist < 1000
       ]

このリスト式における生成式 generator のなかで、パターン ("Amsterdam", to, dist) を用いていることに注意してください。 このパターンの合致する要素だけが、このリストの内包表記の結果となります。

実際のところ、このプログラムには満足できないかも知れません。 たいてい場合は、距離表を二方向 (双方向) と読むでしょう。 ふつう、パリとアムステルダム間の距離が 490km であるという事実は、アムステルダムとパリ間の距離もまた 490km であることを意味します。 それにも関わらず、組 ("Paris", "Amsterdam", 490) は、このパターンには合致しません。 この問題は、出発地と目的地が収められた表の各組の反転も加えて考えることで解けます。

   Start
     = [  (to, dist)
       \\ ("Amsterdam", to, dist) <- table ++ [(t2, t1, d) \\ (t1, t2, d) <- table]
       |  dist < 1000
       ]

これらの例題は、リストの要素を計算する値のソースとして、単一の生成式を持っています。 一般的には、多くの生成式がありえます。 例えば、アムステルダムから他の都市を経由して到達できるような都市を計算することができます。

    Start
     = [  (to, dist1 + dist2)
       \\ ("Amsterdam", t2, dist1) <- connections
       ,  (t3         , to, dist2) <- connections
       |  t2 == t3
       && to <> "Amsterdam"
       ]
     where connections = table ++ [(t2, t1, d) \\ (t1, t2, d) <- table]

ここでは、 つながっていると見做される定義を共有するために、局所的な定義local definition を用いています。 局所的な定義の有効範囲は、それが定義された関数の選択肢です。 局所的な定義は、ある式の名前や値を共有して、定義の有効範囲を制限するために用いられます。 このテキストで使われた局所的な定義には、 キーワード where が先行しています。 CLEAN は局所的な定義の豊富なパレットを持っています。

リストの内包表記内の最後の条件式はアムステルダムで終わる経路を除外します。 この例題では、生成式はセミコロンで分けられ、 生成式が産み出したすべての可能な要素の組み合わせであると見做されることを意味します。 生成式を &- シンボルで分けることも可能です。 このことは生成式が同期的に用いられることを意味します。 例えば、アムステルダムから到達可能な都市に連続的な数値のラベルを付けることに役立ちます。

The last condition in the list comprehension excludes routes that ends in Amsterdam. In this example the generators are separated by a semicolon, this implies that all possible combinations of elements from the generators are considered. It is also possible to separate the generators by a &-symbol. This implies that the generators are used synchronously. This is for instance useful to label the towns reachable from Amsterdam with consecutive numbers.

    Start
     = [  (n, to, dist)
       \\ (to, dist) <- fromAmsterdam
       &  n <- [1..]
       ]
    where
       fromAmsterdam
        = [  (to, dist)
          \\ ("Amsterdam", to, dist) <- table ++ [(t2, t1, d) \\ (t1, t2, d) <- table]
          ]

ここではドット・ドット式 [1..] の上界 (upperbound) を省略したので、アムステルダムから到達可能な都市の数の上界 (upperbound) が何かということに関しては判りません。 このドット・ドット式は、ある整数の無限リストを意味します。 ありがたいことに CLEAN には遅延評価戦略があるので、このような潜在的な 無限データ構造 infinite datastructures を扱うことができます。 遅延評価 Lazy evaluation は、ある式は、プログラムが結果を必要とするときに限り、評価されるということを意味します。

リストの内包表記の使い方は、のちほどいくつかの例題で示します。

[目次]

レコード

CLEAN はデータベース・システムやモダンなプログラミング言語に似たレコードの記法を持っています。 簡単な例題として、レコード型 Product をあげてみます。 この型のレコードは 3 つのフィールドを含んでいます。文字型のプロダクトの名前、実数型の価格、そして文字型の供給者です。

    :: Product 
      = { name     :: String 
        , price    :: Real 
        , supplier :: String 
        }
この型の具体的なレコードは、すべてのフィールドに明示的な値を定義することによって生成することができます。 例えば、
    beer :: Product 
    beer 
      = { name     = "Grolsch Beer" 
        , price    = 0.80 
        , supplier = "Groenlo Brewery" 
        }

もちろん、あるレコードのフィールドにはどんな型でも用いることができます。 コンパイラはプログラム中に現れるどんなレコードのフィールドも型チェックを行っています。 複合型のレコードの簡単な例題として Order をあげてみましょう。 Order 型のレコードのフィールド contents の型は、 Item 型のレコードのリストです。 データベース用語では、これは非第1正規形 non-first normal form (NFNF) と呼ばれています。 (RDB ではテーブル構造は正規形 (normal form) を満たさなければならない。列の値として, 集合や値の組み合わせを使用することはできない。このような非第1正規形テーブルは、繰返項目などの集合を持たない正規形テーブルに変換しなければならない。第1正規形、第2正規形、第3正規形、ボイスコッド正規形、第4正規形、第5正規形がある)

    Order 
      = { customer :: String 
        , date     :: Date 
        , contents :: [Item] 
        } 

    :: Item 
      = { product  :: String 
        , quantity :: Int 
        }
複合型 small order の例です。
    myOrder :: Order 
    myOrder 
     = { customer = "Pieter" 
       , date     = "30 jan '98" 
       , contents = [ { product  = "Grolsch Beer" 
                      , quantity = 5 
                      } 
                    , { product  = "Pizza" 
                      , quantity = 1 
                      } 
                    ] 
       }
リストの内包表記はこのようなレコードのリストを扱うのにたいへんに有効です。 関数 total が、このことを佳く示しています。 この関数は、引数として注文と商品リストを受け取り、受注商品の総額を計算します。
    total :: Order [Product] -> Real 
    total order products 
     = sum 
       [   toReal i.quantity * p.price 
       \\  i <- order.contents 
       ,   p <- products 
       |   p.name == i.product 
       ]
これは、以下の SQL のクエリー文と同等です。
    SELECT sum (i.quantity * p.price) 
    FROM   myOrder.contents i, 
           Product p 
    WHERE  p.name = i.product

どのようなホスト言語の埋込 SQL と比べてみても、 CLEAN におけるリストの内包表記は完全に統合化されています。 このテキストでの例題は、リストの内包表記が再帰関数の一部分にもなりうることを示しています。 リストの内包表記の結果は、ふつうのリストです。 リストを処理するどんな関数は、どのようなリストでも扱うことができます。 SQL の有名な 5 つの集合関数に制限されることはありません。

一方で、 CLEAN はデータベース・システムではないことを体得しておくことは重要です。 例えば、トランザクション管理のプリミティヴもありませんし、極端に大きなリストを効率よく扱うこともできません。

[目次]

クラスと多重定義

クラスの定義

CLEAN は強力なクラス記法を持っています。 ここでのクラスの概念と、オブジェクト指向言語でのクラスのそれとの差異を認識することは重要です。 CLEAN では、クラス class は同じ名前を持つ関数の一族です。 この二つの違いは、the type processed です。 増加関数を持つクラスを例題にとって考えてみましょう。

    class inc t :: t -> t

ここでは、クラス inc は型変数 t を持つと言っています。 このクラスには操作関数一つしかなく、それもまた inc と名付けられています。 この増加関数の型は t -> t です。 このクラスの、整数と実数に対するインスタンス Instances は以下のように定義されます。

    instance inc Int 
    where inc i = i + 1 

    instance inc Real 
    where inc r = r + 1.0
Order 型の要素であるレコード Item でさえも加算可能です。
    instance inc Item 
    where inc i = {i & quantity = inc i.quantity}

これは読みます item i の増加とは、フィールド quantity を持つ item 引数のレコードから、そのフィールドの増加に設定する フィールド quantity を持つ item が 引数のレコードの当該フィールドの加算にセットする

クラスの拡張

+, ==< のような基本的な演算は、型クラスとして定義されています。前述したように、関数 square や関数 product で、これらを用いています。 このことは、これら 3 つの基本的演算を、自分が作ったデータ型において定義することも可能であることを意味しています。 例えば、 (上で触れたレコード Product を見てください) Product の比較を、その名前の比較として定義することが可能です。

    instance < Product 
    where (<) p1 p2 = p1.name < p2.name
リストの内包表記を用いれば、クラス < の要素のリストに、有名なクィックソートのアルゴリズムを簡単に定義することができます。 クィックソートのアルゴリズムは、空リストはすでにソートされているということを表明しています。 空でないリストは、分割されます。 最初の要素よりも小さい要素の部分 最初の要素より大きいか等しい要素の部分 部分リストは、それぞれ別にソートされ、その結果のリストはアペンド演算子 ++ によって、一緒に糊付けされます。
    qsort :: [t] -> [t] | < t 
    qsort [] = [] 
    qsort [e:r] = qsort [x \\ x <- r | x < e] ++ [e] ++ qsort [x \\ x <- r | x >= e]
演算子 >= は、演算子 < から導出されています。
    class >= a 
    where (>=) infix 4 :: a a -> Bool | < a
          (>=) x y :== not (x < y)

このことは、あるクラスにおいて、演算子 < が定義されれば、直ちに >= も定義されることを意味します。 同じクラスやデータ型のメンバーには、なんの関連もありません。 そのデータ型で、適切な関数が定義されていれば、それで十分なのです。 お判りのように、 CLEAN のクラスは、大半のオブジェクト指向言語におけるクラス記法よりも、一般的です。 CLEAN のクラスの概念を用いることで、そのようなオブジェクト指向言語のクラス記法をエミュレートすることも容易になっています。

暗黙のクラス

これまで、型制限を伴った多相型関数を定義することで、クラスを定義してきました。 付加価値税 (VAT) を価格に、整数も実数も同じように加算する関数を考えてみましょう。 この関数は、引数を実数に変換し、価格と付加価値税を計算し、得られた実数を引数が持っていた元の型に戻します。

    addVAT :: t -> t | toReal, fromReal t
    addVAT p = fromReal ((1.0 + VAT) * toReal p)

    VAT :== 0.175

厳密な型変換には、関数の使用法による決定が必要です。 下のプログラムでは、

    Start = (addVAT 100, addVAT 100.0)

このプログラムは、 (118, 117.5) を答えます。

[目次]

代数データ型

代数データ型のもっとも簡単な例は、取り得る値の列挙 enumeration です。 例えば、代数データ型 Day は、 7 つの明白な構成子を含んでいます。 

    :: Day = Sun | Mon | Tue | Wed | Tur | Fri | Sat

数値または文字列によって曜日を表現するような代数データ型の利点は、プログラムの実行中には、有効な値のみが生じるであろうことを、コンパイラが保証できるということです。

代数データ型は、列挙型よりも、もっと一般的です。 そのような代数型の構成子は、いくつもの数値を引数として取ることができます。 引数はどんな型であってもよく、構成子それ自身の型を含んでいてもかまいません。 というわけで、代数型は、再帰的であリ得ます。 代数型に引数として一つ以上の型変数を与えることで、この型は多相にもなります。 多相代数データ型の明白な例は、型リストです。

    :: List t = Nil | Cons t (List t)
リストは関数型プログラミング言語ではしょっちゅう使われるので、リストの内包表記のように、リストに適した特殊構文と言語構造などと同じように、この型はすでに定義済みです。 もう一つの例として、どんな型の木でも表現できる型 Tree をあげてみましょう。 このような木は、空 Empty であるか、ある部分木を含んだ節 Node であるか、のいずれかです。 節 Node は、型 Tree a の左部分木か、型 a の要素、ある右部分木から成ります。
    :: Tree a = Empty | Node (Tree a) a (Tree a)
よって Tree は、どんな型の要素でも一つの木の中に保持できる、多相データ型です。 しかしながら、型システムは、木の各インスタンスは与えられた型の要素のみを含むことを保証します。 例題として、整数のリストを含む木をあげてみます。
    a_tree :: Tree [Int] 
    a_tree = Node (Node Empty [] Empty) [1..3] Empty

この木の最上節は、整数 1, 2, 3 からなるリスト その左部分木は整数の空リストからなる一つの節だけ、 最上節の右部分木は、空です。 を含みます。

木はふつうの関数によっても操作することができます。 さまざまな構成子からなる異なる関数選択肢を用いることや、構成子の引数を選択するパターン照合を用いることは、ときとして便利なものです。 例えば、木を反転させる関数は以下のように定義されます。

    Flip :: (Tree a) -> Tree a 
    Flip Empty = Empty 
    Flip (Node left elem right) = Node (Flip right) elem (Flip left)

空リスト反転は空リストです。 左部分木 left、要素 elem、右部分木 right からなる節を含む木を反転すると、もう一つの節になります。 新しい節の左部分木は、部分木 right を反転されることで獲得します。 節の要素は、元の木の要素であり、右部分木は、元の木の左部分木を反転させたものです。

木の preorder traversal (左から右へ、深さ優先など) によって、木をリストに変換する関数を定義するのも簡単です。

    toList :: (Tree t) -> [t] 
    toList Empty = [] 
    toList (Node left elem right) = toList left ++ [elem] ++ toList right
リストを整列木に変換したり、木を探索するためには、まず、ある、整列木に一つの要素を挿入する関数を定義します。 もちろん、木の要素は比較可能でなければなりません。 よって、型文脈 < a をもちます。
    insertTree :: a (Tree a) -> Tree a | < a 
    insertTree x Empty = Node Empty x Empty 
    insertTree x (Node l y r) 
       | x < y     = Node (insertTree x l) y r 
       | otherwise = Node l y (insertTree x r)
要素のリストは、関数 toTree によって、整列木に変換されます。 空リストは空の木になります。 先頭要素 hd と尾部 tl からなるリストは、 hd を、tl を変換した木に挿入した木です。
    toTree :: [a] -> Tree a | < a 
    toTree []      = Empty 
    toTree [hd:tl] = insertTree hd (toTree tl)
木を整列する関数 tsort は、 (中置演算子 o で判るように) 関数 toListtoTree の関数合成です。 関数合成は、数学のそれ (f o g) x = f (g x) と同じように定義されます。
    tsort :: ([a] -> [a]) | < a 
    tsort = toList o toTree

このような多相代数データ型の威力は C++ のような言語でのテンプレートに近しいものと見ることもできます。 しかしながら、関数型言語での代数データ型は、理解するのにも使うのにも、ずっと簡単です。 ポインタを扱うことがないので、ぶらぶらポインタもありません。 もっと一般的で、完全に型安全を実現したものです。

[目次]

最後に一言

数値を扱ういくつかの例題を使って、 CLEAN による関数プログラミングを特徴が得られたことと思います。 明晰かつ簡潔な記法に馴染みがなくとも、単に面白そうなおもちゃ以上のものがあります。 簡潔な構文のため、関数の中で実際に何が行われているかということに、はっきりとした見とおしが持てます。

お気づきのように、ここには何の副作用もありません。 この参照透過性 referential transparency が、関数型言語の主要な特性です。 式の値は、この項を形成する関数と、その引数だけに依存します。 このことが等式証明推論によるプログラムに関する議論を可能にしています。

CLEAN の静的な型システムは、実行時の型エラーが起こり得ないこと runtime type errors cannot occur を保証します。 このことが、この種のエラーがないかどうかを広範囲に渡って試験する必要性を取り除いています。 コンパイラによる型の無矛盾性の検査は、より速く、より安全です。 プログラムの振るまいを確認したり改良したりすることに時間をかけることができます。

これらの特性が、佳いプログラムを書き、他の人が書いたものを理解し、すでにある関数を変更することを、ずっと容易にしています。 このことによって、 CLEAN のような関数型言語は、段階的または進化する開発というものによく適合します。 保守はより容易になり、再利用は最終的には絵に描いた餅ではなくなるのです。

[目次]


Created: Dec 12, 1999.
[index]