やさしい Haskell 入門 (バージョン 98 )
back next top


6  再び、型について

ここでは、型宣言のより進んだ側面のいくつかを詳しくみていくことにします。

6.1  Newtype 宣言

プログラミングの際によくやるのが,表現が既存の型と同一だが,型システムの 中では別の型として識別されるような型を定義することです。Haskell では newtype 宣言が既存の型から新しい型をつくりだします。たとえば、 Integer 型を次のような宣言で用いて、自然数を表現することができ ます。

newtype Natural = MakeNatural Integer

この宣言は、完全に新しい型 Natural を生成し、その構築子はひとつ の Integer だけを引数にとります。この構築子 MakeNaturalNaturalInteger との間の変 換をおこないます。

toNatural               :: Integer -> Natural
toNatural x | x < 0     = error "Can't create negative naturals!" 
            | otherwise = MakeNatural x

fromNatural             :: Natural -> Integer
fromNatural (MakeNatural i) = i

次のインスタンス宣言は NaturalNum クラスに所属させ るものです。

instance Num Natural where
    fromInteger         = toNatural
    x + y               = toNatural (fromNatural x + fromNatural y)
    x - y               = let r = fromNatural x - fromNatural y in
                            if r < 0 then error "Unnatural subtraction"
                                     else toNatural r
    x * y               = toNatural (fromNatural x * fromNatural y)

この宣言なしでは、NaturalNum には属することができ ません。元の型に対するインスタンス宣言内容が、新しい型に持ち越されることはあ りません。実際、この型の目的は、別の Num のインスタンスを導入す ることなのです。もし、NaturalInteger の型シノニム として宣言されていれば、これはできません。

これらはすべて、newtype 宣言ではなく data 宣言を用いて も機能します。しかし、data 宣言では、Natural の値の表 現にオーバヘッドが生じます。newtype を用いることで data 宣言によって間接参照のレベルが深くなる(これは遅延性の故に 生じる)のを避けることができます。newtypedata および type 宣言の間の関係についてのさらに詳しい議論は、レポートの 4.2.3 節を参照してくだ さい。

[キーワードをのぞけば、newtype 宣言は、単一フィールドをもつ単一 構築子の data 宣言と同じ構文を用います。newtype を用い て定義された型は通常の data 宣言で生成された型とほとんど同一な のですから、これは当然でしょう。]

6.2  フィールドラベル

Haskell のデータ型内のフィールドは、位置を手がかりにアクセスすることも、 フィールドラベルを用いて名前でアクセスすることもできます。2 次 元の点に対する型について考察しましょう。

data Point = Pt Float Float

Point の構成要素は、構築子 Pt の第一引数と第二引数です。 次のような関数

pointx                  :: Point -> Float
pointx (Pt x _)         =  x

は、第一の構成要素をいくぶん説明的に参照するのに使われていますが、もっと、 大きな構造に対して、このような関数を手でつくるのは面倒です。

data 宣言における構築子は、フィールド名(波括弧でくくら れて)付で宣言されることもあります。フィールド名は構築子の構成要素を位置 によってではなく、名前で同定します。以下はもうひとつの Point の 定義です。

data Point = Pt {pointx, pointy :: Float}

このデータ型はその前で定義した Point と同一のものです。構築子 Pt は両方でおなじです。しかし、この宣言ではふたつのフィールド名、 pointxpointy とが定義されています。これらのフィー ルド名は構造の中から構成要素を取り出すための選択関数 ( selector function ) としても使うことができます。この例では 選択関数は、

pointx                  ::   Point -> Float 
pointy                  ::   Point -> Float 

です。つぎは、これらの選択関数をつかった関数定義です。

absPoint                :: Point -> Float
absPoint p              =  sqrt (pointx p * pointx p + 
                                 pointy p * pointy p)

フィールドラベルは新しい値を構築するときにも使えます。式 Pt {pointx=1, pointy=2} は式 Pt 1 2 と同一です。このデータ構築子の宣言でのフィール ド名の使い方は、位置をよりどころとしたフィールドへのアクセスを排除するも のではありません。Pt {pointx=1, pointy=2}Pt 1 2 とはともに可能です。フィールド名を使って値を構 築する場合、フィールドをいくつか省略することができます。

省略されたフィールドは定義されません。

フィールド名を使ったパターンマッチングは構築子 Pt のときと同じ ような構文で使います。

absPoint (Pt {pointx = x, pointy = y}) = sqrt (x*x + y*y) 

更新関数は、既存の構造中のフィールド値を使って、新しい構造の構成要素を埋 めます。もし、pPoint ならば、 p {pointx=2}p と同じ pointy をもちま すが、pointx2 に置き換ります。これは、破壊的更新で はありません。この更新関数は、指定されたフィールドを新しい値で埋めること で、オブジェクトの新しいコピーを生成しているにすぎません。

[フィールドラベルと併せて使用した波括弧はいくぶん特殊な使い方です。 Haskell の構文では、レイアウトルールを無視するのに波括 弧を使うのが普通です。(レイアウトルールについては 4.6 節であつかっています。) しかし、 フィールド名とむすびついた波括弧は明示的に書かなければなりません。]

フィールド名は単一構築子の型(レコード型とよばれることが多い)に限るも のではありません。複数の構築子をもつ型では、フィールド名を用いての選択や 更新の操作は実行時エラーを引き起こす可能性があります。これは、空リストに head を適用したときと似ています。

フィールドラベルは一般の変数やクラスメソッドとトップレベルの名前空間を共 有します。スコープのなかで同じ名前のフィールド一つ以上のデータ型で使うこ とはできません。しかし、一つのデータ型の中でなら、型付けが同じあるかぎり、 一つ以上の構築子のなかで、使うことができます。たとえば、次のようなデータ 型のなかで、

data T = C1 {f :: Int, g :: Float}
       | C2 {f :: Int, h :: Bool}

フィールド名 f は型 T の両方の構築子に適用されています。 つまり、もし、x の型が T ならば、x {f=5}T 内のどちらの構築子が生成した値に対しても機能します。

フィールド名が代数的データ型の基本性質を変えることはありません。フィール ド名は、データ構造の構成要素にアクセスするのに位置を手がかりにするより、 名前を手がかりにするために用意された、便宜的な構文にすぎません。この構文 のおかげで、多くの構成要素をもつ構築子の扱いが簡単になります。それは、フィー ルドは構築子へのそれぞれの参照を変更することなく追加したり削除したりする ことができるからです。フィールドラベルの詳細とその意味論についてはレポー トの §4.2.1 を参照 してください。

6.3  正格データ構築子

Haskell ではデータ構造は一般的に遅延性をもちます。構成要素は必 要になるまで評価されません。この性質のおかげで、データ構造はもし評価され るとエラーあるいは停止できないような要素を含むことができます。遅延データ 構造は Haskell の表現力を強化し、Haskell のプログラミングスタイルの要に なります。

内部的には、遅延データオブジェクトの各フィールドはサンク ( thunk ) とよくいわれる構造に包みこまれています。サンクはフィー ルド値を定義した計算をカプセル化したものです。値が必要にならないかぎり、 サンクの中へ入ることはありません。エラー ( _|_ ) を含むサンクが あってもデータ構造の他の要素には影響しません。たとえば、タプル ('a',_|_) は完全に正しい Haskell の値です。 この 'a' はこのタプルのもう一方の要素にかまうことなく、使うことが できます。ほとんどのプログラミング言語は遅延性ではなく正格性をもっていま す。すなわち、データ構造のすべての要素はそのデータ構造に入れる前に値にま で簡約されます。

サンクにはいろいろなオーバヘッドがついてまわります。サンクを構成するにも、 それを評価するにも時間がかかります。ヒープの領域をとりますし、そのサンク の評価のために必要な別の構造を確保するため、ガーベッジコレクタを起動する こともあります。こうしたオーバヘッドを回避するために、 正格性フラグdata 宣言で使います。こうして、選択的に 遅延性を抑制することで、指定した構築子のフィールドをただちに評価します。 data 宣言の中で、! でマークされたフィールドは、サンク に入れて遅延するのではなく、構造が生成される際に、直ちに評価されます。こ の正格性フラグを用いるほうがよい状況はいくつかあります。

たとえば、複素数のライブラリが定義している Complex 型はこんなふ うになっています。

data RealFloat a => Complex a = !a :+ !a

[構築子 :+ の中置定義に注意してください。] この定義では、ふたつ の構成要素にマークがついています。複素数の実数部と虚数部は正格性をもつよ うにマークされています。これは、複素数のよりコンパクトな表現ですが、たと えば、1 :+  _|_ のように未定義の構成要素があっ た場合、全体が未定義 (_|_) になってしまうという代償をはらったうえ でのことです。しかし、部分的にしか定義されていない複素数が必要になること は実際にはありませんから、効率のよい表現を達成するために正格性フラグを使 用するのは意味のあることです。

正格性フラグはメモリリーク(もはや使うことはないのにガーベッジコレクタに 回収されない構造)を解決するために使うこともできます。

正格性フラグ !data 宣言のなかでのみ使われます。そ のほかの型シグネチャーや型定義では使えません。関数の引数に対して正格性を もつようにマークをつける方法はありません。しかし、 seq あるいは !$ という関数を使用することで同じ効果が得られます。詳細について はレポート §4.2.1 を参照して ください。

正格性フラグの使い方の正確なガイドラインを示すのは難しいことです。注意し て使用しなければなりません。遅延性は Haskell の基本的な特徴のひとつであっ て、そこへ正格性フラグをつけ加えることは無限ループの発見を困難なものにし、 ほかの予期せぬ結果をまねきかねないということです。


A Gentle Introduction to Haskell, Version 98
back next top