top page > computer > haskell > web_lecture > for_programmer > record_syntax.html
更新日:
文責: 重城良国

構文: レコード構文

はじめに

レコード構文は代数的データ型の構文糖だ。それぞれのフィールドに名前をつけられる。値構築子が引数をたくさんとるときに便利だ。

直交座標上の円は中心の座標と半径で表現できる。座標はふたつの、半径はひとつの実数で表される。円は3つの実数で表現できる。

レコード構文を使わずに

circle1.hs

定義

レコード構文を使わないと

data Circle = Circle Double Double Double deriving Show

となる。順にX座標、Y座標上の値、円の半径だ。

% ghci circle1.hs
*Main> Circle 10 15 7
Circle 10.0 15.0 7.0

面積

面積を計算する関数areaを示す。

area :: Circle -> Double
area (Circle _ _ r) = r ^ 2 * pi

*Main> :reload
*Main> area (Circle 10 15 7)
153.93804002589985

内外の判定

点(x, y)が円の内側か外側かを判定する関数insideを示す。

inside :: Circle -> (Double, Double) -> Bool
inside (Circle cx cy r) (x, y) = (x - cx) ^ 2 + (y - cy) ^ 2 <= r ^ 2

*Main> :reload
*Main> inside (Circle 10 15 7) (13, 17)
True
*Main> inside (Circle 10 15 7) (5, 10)
False

平行移動

円を横向きに移動する。X座標の値を変化させる。移動距離を指定する。

moveH :: Circle -> Double -> Circle
moveH (Circle cx cy r) dx = Circle (cx + dx) cy r

*Main> :reload
*Main> moveH (Circle 10 15 7) 15
Circle 25.0 15.0 7.0
*Main> moveH (Circle 10 15 7) (- 13)
Circle (-3.0) 15.0 7.0

レコード構文を使った型の定義

circle2.hs

Circle型の値は3つのDouble値をもつ。この3つは値構築子Circleに与える順番で指定する。つまりX座標、Y座標、半径の順番を覚えておかなければならない。どの値が何であるかを示したい。レコード構文を使えばフィールドに名前をつけられる。

data Circle = Circle { centerX :: Double, centerY :: Double, radius :: Double }
deriving Show

レコード構文を使わない書きかたとくらべる。

data Circle = Circle Double Double Double deriving Show

両者は本質的には同じものだ。前者ではいくつかの関数と構文糖が導入される。

それでもレコード構文を使わない

関数と構文糖が追加される以外は意味論的には同じだ。レコード構文を使わない定義と同じ使いかたができる。

% ghci circle2.hs
*Main> Circle 10 15 7
Circle {centerX=10.0, centerY=15.0, radius=7.0}

返される値はレコード構文を使わないときの値と違うように見える。単にderiving Showによって定義される表示関数の定義が変わるだけだ。

フィールドの取得関数

レコード構文を使うと暗黙のうちにフィールドをとりだす関数が定義される。例では以下の関数が定義される。

centerX :: Circle -> Double
centerX (Circle x _ _) = x

centerY :: Circle -> Double
centerY (Circle _ y _) = y

radius :: Circle -> Double
radius (Circle _ _ r) = r

このような定義が暗黙のうちに追加されている。

*Main> let c = Circle 10 15 7
*Main> centerX c
10.0
*Main> centerY c
15.0
*Main> radius c
7.0

フィールド名を利用した値構築

値を並べるのではなく名前を明示して値を作ることができる。

*Main> Circle { centerX = 10, centerY = 15, radius = 7 }
Circle {centerX = 10.0, centerY = 15.0, radius = 7.0}

名前でフィールドを指定するので値の順序は問われない。

*Main> Circle { radius = 7, centerX = 10, centerY = 15 }
Circle {centerX = 10.0, centerY = 15.0, radius = 7.0}

部分的な更新

一部のフィールドだけが異なる値を定義できる。

*Main> let c = Circle { centerX = 10, centerY = 15, radius = 7 }
*Main> c
Circle {centerX = 10.0, centerY = 15.0, radius = 7.0}
*Main> c { centerX = 25 }
Circle {centerX = 25.0, centerY = 15.0, radius = 7.0}

部分的な定義

フィールドの値を部分的に指定できる。

*Main> let c2 = Circle { centerX = 10 }

<interactive>:8:10: Warning:
Fields of ‘Circle’ not initialised: centerY, radius
In the expression: Circle {centerX = 10}
In an equation for ‘c2’: c2 = Circle {centerX = 10}
*Main> centerX c2
10.0
*Main> c2
Circle {centerX = 10.0, centerY = *** Exception: :8:10-32: Missing field in record construction Main.centerY

定義しているフィールドを使うまではエラーは出ない。しかし、部分関数と同じで予期せぬエラーの原因となる。このやりかたは推奨されない。処理系が警告してくれる。

パターンマッチ

フィールドの名前でパターンマッチできる。

*Main> c
Circle {centerX = 10.0, centerY = 15.0, radius = 7.0}
*Main> let Circle { centerX = cx, centerY = cy, radius = r } = c in (cx, cy, r)
(10.0,15.0,7.0)
*Main> let Circle cx cy r = c in (cx, cy, r)
(10.0,15.0,7.0)

フィールドの一部だけを指定できる。

*Main> let Circle { centerX = cx } = c in cx
10.0
*Main> let Circle cx _ _ = c in cx
10.0

このようなフィールド名を指定してのパターンマッチも構文糖だ。

円を扱う関数

円を扱う関数をレコード構文で書きなおす。

面積

area :: Circle -> Double
area Circle { radius = r } = r ^ 2 * pi

*Main> :reload
*Main> area $ Center 10 15 7
153.9380400258985

レコード構文は単なる構文糖だ。普通の書きかたで作った値をレコード構文でパターンマッチできる。

内外の判定

inside :: Circle -> (Double, Double) -> Bool
inside Circle { centerX = cx, centerY = cy, radius = r } (x, y) =
(x - cx) ^ 2 + (y - cy) ^ 2 <= r ^ 2

*Main> :reload
*Main> let c = Circle 10 15 7
*Main> inside c (7, 13)
True
*Main> inside c (4, 20)
False

平行移動

(@とレコード構文を併用する定石についての説明が必要)

moveH :: Circle -> Double -> Circle
moveH c@Circle { centerX = cx } dx = c { centerX = cx + dx }

*Main> :reload
*Main> let c = Circle 10 15 7
*Main> moveH c 15
Circle {centerX = 25.0, centerY = 15,0, radius = 7.0}
*Main> moveH c (- 13)
Circle {centerX = -3, centerY = 15.0, radius = 7.0}

構文

型の定義

data 型名 = 値構築子名 {
フィールド名1 :: 型1,
フィールド名2 :: 型2,
...
}

値の構築

値構築子名 {
フィールド名1 = 値1,
フィールド名2 = 値2,
...
}

値の更新

便宜的に「更新」と呼んでいる。本当は値は変化せずに新しい値が作られる。

値 {
フィールド名1 = 値1,
フィールド名2 = 値2,
...
}

パターンマッチ

値構築子名 {
フィールド名1 = 仮引数1,
フィールド名2 = 仮引数2,
...
}

長方形の2つの定義

長方形を表現するのにはいくつかの方法がある。左上の点と右下の点を指定する方法と左上の点と幅と高さを指定する方法とがある。

data Rectangle
= RectBR { topL :: (Double, Double), botR :: (Double, Double) }
| RectWH { topL :: (Double, Double), width :: Double, height :: Double }
deriving Show

値構築子が複数あるときもレコード構文が使える。

% ghci rectanble.hs
*Main> topL $ RectBR (3, 5) (4, 9)
(3,5)
*Main> topL $ RectWH (3, 5) 1 4
(3,5)
*Main> botR $ RectBR (3, 5) (4, 9)
(4,9)
*Main> botR $ RectWH (3, 5) 1 4
*** Exception: No match in record selector botR

値構築子が含まないフィールドをとりだそうとすると例外が発生する。関数botRは全域関数ではない部分関数だ。

まとめ

レコード構文を使うと代数的データ型のフィールドを位置ではなく名前で指定できる。複数の値構築子があるときにはフィールドを取り出す関数は全域関数ではない部分関数になってしまうことがある。レコード構文と普通の記法を場面に合わせて使いわけるとコードが読みやすくなる。

課題

  1. nameとageの2つのフィールドを持つHuman型を定義せよ
  2. レコード構文を使わずにHuman型の値を作成せよ
  3. レコード構文を使ってHuman型の値を作成しletで変数を束縛せよ
  4. 上記の変数を束縛した値のageフィールドだけ更新した値を作成せよ
  5. 関数name, ageを試してみよ
  6. レコード構文によるパターンマッチで人を表現する文字列を返す関数を作成せよ

「リスト」へもどる 「構文: 代数的データ型のインポート」へ

正当なCSSです! HTML5 Powered with CSS3 / styling, and Semantics