パターン照合

一時的な注意事項:本章に取り組まれているようでしたら、2023年11月に第4章と第5章とが入れ替わっていることにお気を付けください。

この章の目標

この章では、代数的データ型とパターン照合という、2つの新しい概念を導入します。 また、行多相というPureScriptの型システムの興味深い機能についても簡単に取り扱います。

パターン照合は関数型プログラミングにおける一般的な手法であり、開発者が簡潔に関数を書けるようになります。 関数の実装を複数の場合に分解することにより、水面下の複雑なアイディアが表現されるのです。

代数的データ型はPureScriptの型システムの機能であり、型のある言語において同等の水準の表現力を可能にしています。 パターン照合とも密接に関連しています。

この章の目的は、代数的データ型やパターン照合を使用して、単純なベクターグラフィックスを記述し操作するためのライブラリを書くことです。

プロジェクトの準備

この章のソースコードはファイル src/Data/Picture.pursで定義されています。

Data.Pictureモジュールは簡単な図形を表すデータ型Shapeやその図形の集合である型Pictureを定義します。 また、これらの型を扱うための関数もあります。

このモジュールでは、データ構造を畳込む関数を提供するData.Foldableモジュールもインポートします。

module Data.Picture where

import Prelude
import Data.Foldable (foldl)
import Data.Number (infinity)

Data.PictureモジュールはNumberモジュールもインポートしますが、こちらはasキーワードを使います。

import Data.Number as Number

こうすると型や関数をモジュール内で使用できるようになりますが、 Number.maxのように修飾名を使ったときに限定されます。 重複したインポートを避けたり、どのモジュールからインポートされたのかを明らかにするのに役立ちます。

補足:元のモジュールと同じモジュール名を修飾名に使用する必要はありません。 import Math as Mなどのより短い名前にできますし、かなりよく見掛けます。

単純なパターン照合

例を見ることから始めましょう。 パターン照合を使用して2つの整数の最大公約数を計算する関数は、次のようになります。

gcd :: Int -> Int -> Int
gcd n 0 = n
gcd 0 m = m
gcd n m = if n > m
            then gcd (n - m) m
            else gcd n (m - n)

このアルゴリズムはユークリッドの互除法と呼ばれています。 その定義をオンラインで検索すると、恐らく上記のコードによく似た数学の方程式が見つかるでしょう。 パターン照合の利点の1つは、コードを場合分けして定義でき、数学関数の仕様に似た単純で宣言型なコードを定義できることです。

パターン照合を使用して書かれた関数は、条件と結果の組み合わせによって動作します。 この定義の各行は選択肢場合と呼ばれています。 等号の左辺の式はパターンと呼ばれており、それぞれの場合は空白で区切られた1つ以上のパターンで構成されています。 場合の集まりは、等号の右側の式が評価され値が返される前に、引数が満たさなければならない条件を表現しています。 それぞれの場合は上からこの順番に試されていき、最初にパターンが入力に照合した場合が返り値を決定します。

例えばgcd関数は次の手順で評価されます。

  • まず最初の場合が試されます。 第2引数がゼロの場合、関数は n(最初の引数)を返します。
  • そうでなければ、2番目の場合が試されます。 最初の引数がゼロの場合、関数は m(第2引数)を返します。
  • それ以外の場合、関数は最後の行の式を評価して返します。

なお、パターンでは値を名前に束縛できます。 この例の各行では nmという名前の何れかまたは両方に入力された値を束縛しています。 様々な種類のパターンについて学んでいくうちに、それぞれの種類のパターンが入力の引数から名前を選ぶ様々な方法に対応することがわかるでしょう。

単純なパターン

上記のコード例では、2種類のパターンを示しました。

  • Int型の値が正確に一致する場合にのみ照合する、整数直値パターン
  • 引数を名前に束縛する、変数パターン

単純なパターンには他にも種類があります。

  • NumberStringChar、そしてBooleanといった直値
  • どんな引数とも照合するが名前に束縛はしない、アンダースコア (_) で表されるワイルドカードパターン

これらの単純なパターンを使用する実演として、もう2つの例が以下です。

fromString :: String -> Boolean
fromString "true" = true
fromString _      = false

toString :: Boolean -> String
toString true  = "true"
toString false = "false"

PSCiでこれらの関数を試してみてください。

ガード

ユークリッドの互除法の例では、m > nのときとm <= nのときの2つの選択肢の間を切り替えるためにif .. then .. else式を使いました。 こういうときにはガードを使うという他の選択肢もあります。

ガードとは、パターンにより課された制約に加えて満たされなくてはいけない真偽値の式です。 ガードを使用してユークリッドのアルゴリズムを書き直すと、次のようになります。

gcdV2 :: Int -> Int -> Int
gcdV2 n 0 = n
gcdV2 0 n = n
gcdV2 n m | n > m     = gcdV2 (n - m) m
          | otherwise = gcdV2 n (m - n)

この場合、3行目ではガードを使用して、最初の引数が第2引数よりも厳密に大きいという条件を課しています。 最後の行でのガードは式otherwiseを使っています。 これはキーワードのようにも見えますが、実際はただのPreludeにある普通の束縛です。

> :type otherwise
Boolean

> otherwise
true

この例が示すように、ガードは等号の左側に現れ、パイプ文字 (|) でパターンのリストと区切られています。

演習

  1. (簡単)パターン照合を使用して、階乗関数factorialを書いてみましょう。 手掛かり:入力がゼロのときとゼロでないときの、2つの特殊な場合を考えてみてください。 補足:これは前の章の例の反復ですが、ここでは自力で書き直せるかやってみてください。
  2. (普通)\( (1 + x) ^ n \)を多項式展開した式にある\( x ^ k \)の項の係数を求める関数binomialを書いてください。 これはn要素の集合からk要素の部分集合を選ぶ方法の数と同じです。 数式\( n! / k! (n - k)! \)を使ってください。 ここで \( ! \) は前に書いた階乗関数です。 手掛かり:パターン照合を使って特殊な場合を取り扱ってください。 長い時間が掛かったりコールスタックのエラーでクラッシュしたりしたら、特殊な場合を更に追加してみてください。
  3. (普通)パスカルの法則を使って前の演習と同じ2項係数を計算する関数pascalを書いてください。

配列パターン

配列直値パターンは、固定長の配列に対して照合する方法を提供します。 例えば空の配列であるか判定する関数isEmptyを書きたいとします。 最初の選択肢に空の配列パターン ([]) を用いるとこれを実現できます。

isEmpty :: forall a. Array a -> Boolean
isEmpty [] = true
isEmpty _ = false

次の関数では、長さ5の配列と照合し、配列の5つの要素をそれぞれ違った方法で束縛しています。

takeFive :: Array Int -> Int
takeFive [0, 1, a, b, _] = a * b
takeFive _ = 0

最初のパターンは、第1要素と第2要素がそれぞれ0と1であるような、5要素の配列にのみ照合します。 その場合、関数は第3要素と第4要素の積を返します。 それ以外の場合は、関数は0を返します。 例えばPSCiで試してみると次のようになります。

> :paste
… takeFive [0, 1, a, b, _] = a * b
… takeFive _ = 0
… ^D

> takeFive [0, 1, 2, 3, 4]
6

> takeFive [1, 2, 3, 4, 5]
0

> takeFive []
0

配列の直値パターンでは、固定長の配列と一致させることはできます。 しかしPureScriptは不特定の長さの配列を照合させる手段は全く提供していません。 そのような類の方法で不変な配列を分解すると、実行速度が低下する可能性があるためです。 このように照合できるデータ構造が必要な場合は、Data.Listを使うことをお勧めします。 その他の操作について、より優れた漸近性能を提供するデータ構造も存在します。

レコードパターンと行多相

レコードパターンは(ご想像の通り)レコードに照合します。

レコードパターンはレコード直値にほぼ見た目が似ていますが、コロンの右に値を置くのではなく、それぞれのフィールドで束縛子を指定します。

例えば次のパターンはfirstlastという名前のフィールドが含まれた任意のレコードに照合し、これらのフィールドの値はそれぞれ xyという名前に束縛されます。

showPerson :: { first :: String, last :: String } -> String
showPerson { first: x, last: y } = y <> ", " <> x

レコードパターンはPureScriptの型システムの興味深い機能である行多相の良い例となっています。 もし上のshowPersonを型シグネチャなしで定義していたとすると、この型はどのように推論されるのでしょうか。 面白いことに、推論される型は上で与えた型とは同じではありません。

> showPerson { first: x, last: y } = y <> ", " <> x

> :type showPerson
forall (r :: Row Type). { first :: String, last :: String | r } -> String

この型変数 rは何でしょうか。 PSCiでshowPersonを使ってみると、面白いことがわかります。

> showPerson { first: "Phil", last: "Freeman" }
"Freeman, Phil"

> showPerson { first: "Phil", last: "Freeman", location: "Los Angeles" }
"Freeman, Phil"

レコードにそれ以外のフィールドが追加されていても、showPerson関数はそのまま動作するのです。 レコードに少なくとも型がStringであるようなフィールドfirstlastが含まれていれば、関数適用は正しく型付けされます。 しかし、フィールドが不足していると、showPersonの呼び出しは不正となります。

> showPerson { first: "Phil" }

Type of expression lacks required label "last"

showPersonの新しい型シグネチャを読むと、「Stringfirstlastフィールド と他のフィールドを何でも 持つあらゆるレコードを取り、Stringを返す」となります。なお、この挙動は元のshowPersonのものとは異なります。行変数rがなければshowPerson厳密に firstlastフィールドしかないレコードのみを受け付けます。

なお、次のように書くこともできます。

> showPerson p = p.last <> ", " <> p.first

そしてPSCiは同じ型を推論することでしょう。

レコード同名利用

showPerson関数は引数内のレコードと照合し、firstlastフィールドをxyという名前の値に束縛していたのでした。 別の方法として、フィールド名自体を再利用してこのような類のパターン照合を次のように単純化できます。

showPersonV2 :: { first :: String, last :: String } -> String
showPersonV2 { first, last } = last <> ", " <> first

ここでは、プロパティの名前のみを指定し、名前に導入したい値を指定する必要はありません。これは レコード同名利用 (record pun) と呼ばれます。

レコード同名利用はレコードの構築にも使用できます。 例えば、スコープに firstlastという名前の値があれば、{ first, last }を使って人物レコードを作ることができます。

unknownPerson :: { first :: String, last :: String }
unknownPerson = { first, last }
  where
    first = "Jane"
    last  = "Doe"

こうすると、状況によってはコードの可読性が向上します。

入れ子になったパターン

配列パターンとレコードパターンはどちらも小さなパターンを組み合わせることで大きなパターンを構築しています。 これまでのほとんどの例では配列パターンとレコードパターンの内部で単純なパターンを使用していました。 しかし特筆すべきこととして、パターンは自由に入れ子にできます。 これにより潜在的に複雑なデータ型についての条件を使って関数を定義できます。

例えばこのコードは2つのレコードパターンを組み合わせています。

type Address = { street :: String, city :: String }

type Person = { name :: String, address :: Address }

livesInLA :: Person -> Boolean
livesInLA { address: { city: "Los Angeles" } } = true
livesInLA _ = false

名前付きパターン

入れ子のパターンを使う場合、パターンには名前を付けてスコープに名前を追加で持ち込むことができます。 任意のパターンに名前を付けるには、 @記号を使います。

例えば次の関数は2要素配列を整列するもので、2つの要素に名前を付けていますが、配列自身にも名前を付けています。

sortPair :: Array Int -> Array Int
sortPair arr@[x, y]
  | x <= y = arr
  | otherwise = [y, x]
sortPair arr = arr

このようにすれば対が既に整列されているときに新しい配列を割り当てなくて済みます。 なお、もし入力の配列が厳密に2つの要素を含んでいなければ、たとえ整列されていなかったとしても、この関数は単に元のまま変えずに返します。

演習

  1. (簡単)レコードパターンを使って、2つの Personレコードが同じ都市にいるか調べる関数 sameCityを定義してみましょう。
  2. (普通)行多相を考慮すると、 sameCity関数の最も一般的な型は何でしょうか。 先ほど定義したlivesInLA関数についてはどうでしょうか。 補足:この演習にテストはありません。
  3. (普通)配列直値パターンを使って、1要素の配列の唯一のメンバーを抽出する関数fromSingletonを書いてみましょう。 1要素だけを持つ配列でない場合、関数は与えられた既定値を返します。 この関数はforall a. a -> Array a -> aという型を持ちます。

case式

パターンが現れるのは最上位にある関数宣言だけではありません。 case式を使う計算中の途中の値に対してパターン照合を使えます。 case式には無名関数に似た便利さがあります。 関数に名前を与えることがいつも望ましいわけではないように、パターンを使いたいためだけに関数に名前をつけるようなことを避けられるようになります。

例を示しましょう。 次の関数は、配列の「最長ゼロ末尾」(和がゼロであるような、最も長い配列の末尾)を計算します。

import Data.Array (tail)
import Data.Foldable (sum)
import Data.Maybe (fromMaybe)

lzs :: Array Int -> Array Int
lzs [] = []
lzs xs = case sum xs of
           0 -> xs
           _ -> lzs (fromMaybe [] $ tail xs)

以下は一例です。

> lzs [1, 2, 3, 4]
[]

> lzs [1, -1, -2, 3]
[-1, -2, 3]

この関数は場合毎の分析によって動作します。 もし配列が空なら、唯一の選択肢は空の配列を返すことです。 配列が空でない場合は、更に2つの場合に分けるためにまずcase式を使用します。 配列の合計がゼロであれば、配列全体を返します。 そうでなければ、配列の残りに対して再帰します。

パターン照合の失敗と部分関数

case式のパターンを順番に照合していって、どの選択肢の場合も入力が照合しなかった時はどうなるのでしょう。 この場合、パターン照合失敗によって、case式は実行時に失敗します。

簡単な例でこの動作を見てみましょう。

import Partial.Unsafe (unsafePartial)

partialFunction :: Boolean -> Boolean
partialFunction = unsafePartial \true -> true

この関数は単一の場合しか含んでいません。 そしてその場合は単一の入力であるtrueにのみ照合します。 このファイルをコンパイルしてPSCiでそれ以外の値を与えて試すと実行時エラーが発生します。

> partialFunction false

Failed pattern match

どんな入力の組み合わせに対しても値を返すような関数は全関数と呼ばれ、そうでない関数は部分的であると呼ばれます。

一般的には、可能な限り全関数として定義したほうが良いと考えられています。 もし関数が何らかの妥当な入力の集合について結果を返さないことがわかっているなら、大抵は失敗であることを示すことができる値を返すほうがよいでしょう。 例えば何らかのaについての型Maybe aで、妥当な結果を返せないときはNothingを使います。 この方法なら、型安全な方法で値の有無を示すことができます。

PureScriptコンパイラは、パターン照合が不完全であるために関数が全関数ではないことが検出されると、エラーを出します。 unsafePartial関数を使うとこうしたエラーを抑制できます(ただしその部分関数が安全だと言い切れるなら)。 もし上記のunsafePartial関数の呼び出しを取り除くと、コンパイラは次のエラーを出します。

A case expression could not be determined to cover all inputs.
The following additional cases are required to cover all inputs:

  false

これは値falseが、定義されたどのパターンとも一致しないことを示しています。 一般にこれらの警告には、複数の不一致な場合が含まれることがあります。

上記の型シグネチャも省略した場合は、次のようになります。

partialFunction true = true

このとき、PSCiは興味深い型を推論します。

> :type partialFunction

Partial => Boolean -> Boolean

本書では以降、=>記号が絡む(型クラスに関連する)型をもっと見ていきます。 しかし現時点では、PureScriptは型システムを使って部分関数を把握していることと、安全な場合に型検証器に明示する必要があることを確認すれば充分です。

コンパイラは、冗長な場合を検出したとき(つまり、その場合より前の方に定義された場合にのみ一致するとき)などにも警告を出します。

redundantCase :: Boolean -> Boolean
redundantCase true = true
redundantCase false = false
redundantCase false = false

このとき、最後の場合は冗長であると正しく検出されます。

A case expression contains unreachable cases:

  false

補足:PSCiは警告を表示しません。 そのため、この例を再現するには、この関数をファイルとして保存し、spago buildを使ってコンパイルします。

代数的データ型

この節では 代数的データ型 (algebraic data type, ADT) と呼ばれる、PureScriptの型システムの機能を導入します。この機能はパターン照合と地続きの関係があります。

しかしまずは切り口となる例について考えていきます。この例では単純なベクターグラフィックスライブラリの実装というこの章の課題を解決する基礎を与えます。

直線、矩形、円、テキストなどの単純な図形の種類を表現する型を定義したいとします。 オブジェクト指向言語では、恐らくインターフェースもしくは抽象クラス Shapeを定義し、使いたいそれぞれの図形について具体的なサブクラスを定義するでしょう。

しかし、この方針は大きな欠点を1つ抱えています。 Shapeを抽象的に扱うためには、実行したいと思う可能性のある全ての操作を事前に把握し、Shapeインターフェースに定義する必要があるのです。 モジュール性を壊さずに新しい操作を追加することが難しくなります。

もし図形の種類が事前にわかっているなら、代数的データ型はこうした問題を解決する型安全な方法を提供します。 モジュール性のある方法で Shapeに新たな操作を定義しつつ、型安全性を維持できます。

代数的データ型としてどのようにShapeが表現されるかを次に示します。

data Shape
  = Circle Point Number
  | Rectangle Point Number Number
  | Line Point Point
  | Text Point String

type Point =
  { x :: Number
  , y :: Number
  }

この宣言ではShapeをそれぞれの構築子の直和として定義しており、各構築子では含まれるデータを指定します。 Shapeは、中央 Pointと半径(数値)を持つ Circleか、RectangleLineTextの何れかです。 他にShape型の値を構築する方法はありません。

代数的データ型 (algebraic data type; ADT) の定義はキーワードdataから始まり、それに新しい型の名前と任意個の型引数が続きます。 その型の構築子(これをデータ構築子と言います)は等号の後に定義され、パイプ文字 (|) で区切られます。 ADTの構築子が持つデータは原始型に限りません。 構築子にはレコード、配列、また他のADTさえも含められます。

それではPureScriptの標準ライブラリから別の例を見てみましょう。 省略可能な値を定義するのに使われる Maybe型を本書の冒頭で扱いました。 maybeパッケージでは Maybeを次のように定義しています。

data Maybe a = Nothing | Just a

この例では型引数 aの使用方法を示しています。パイプ文字を「または」と読むことにすると、この定義は「Maybe a型の値は、無い (Nothing) か、ただの (Just) 型 aの値だ」とほぼ英語のように読むことができます。

なお、データ定義のどこにも構文forall aを使っていません。 forall構文は関数には必須ですが、dataによるADTやtypeでの型別称を定義するときは使われません。

データ構築子は再帰的なデータ構造を定義するためにも使用できます。更に例を挙げると、要素が型 aの単方向連結リストのデータ型の定義はこのようになります。

data List a = Nil | Cons a (List a)

この例は listsパッケージから持ってきました。 ここで Nil構築子は空のリストを表しており、Consは先頭となる要素と尾鰭から空でないリストを作成するために使われます。 Consの2つ目のフィールドでデータ型 List aを使用しており、再帰的なデータ型になっていることに注目してください。

ADTの使用

代数的データ型の構築子を使用して値を構築するのはとても簡単です。 対応する構築子に含まれるデータに応じた引数を用意し、その構築子を単に関数のように適用するだけです。

例えば、上で定義した Line構築子は2つの Pointを必要としていますので、Line構築子を使って Shapeを構築するには、型 Pointの2つの引数を与えなければなりません。

exampleLine :: Shape
exampleLine = Line p1 p2
  where
    p1 :: Point
    p1 = { x: 0.0, y: 0.0 }

    p2 :: Point
    p2 = { x: 100.0, y: 50.0 }

さて、代数的データ型で値を構築することは簡単ですが、これをどうやって使ったらよいのでしょうか。 ここで代数的データ型とパターン照合との重要な接点が見えてきます。 代数的データ型の値を消費する唯一の方法は構築子に照合するパターンを使うことです。

例を見てみましょう。 ShapeStringに変換したいとします。 Shapeを構築するのにどの構築子が使用されたかを調べるには、パターン照合を使用しなければなりません。 これには次のようにします。

showShape :: Shape -> String
showShape (Circle c r) =
  "Circle [center: " <> showPoint c <> ", radius: " <> show r <> "]"
showShape (Rectangle c w h) =
  "Rectangle [center: " <> showPoint c <> ", width: " <> show w <> ", height: " <> show h <> "]"
showShape (Line start end) =
  "Line [start: " <> showPoint start <> ", end: " <> showPoint end <> "]"
showShape (Text loc text) =
  "Text [location: " <> showPoint loc <> ", text: " <> show text <> "]"

showPoint :: Point -> String
showPoint { x, y } =
  "(" <> show x <> ", " <> show y <> ")"

各構築子はパターンとして使用でき、構築子への引数はそのパターンで束縛できます。 showShapeの最初の場合を考えてみましょう。 もし ShapeCircle構築子に照合した場合、2つの変数パターン crを使ってCircleの引数(中心と半径)がスコープに導入されます。 その他の場合も同様です。

演習

  1. (簡単)Circle(型はShape)を構築する関数circleAtOriginを書いてください。 中心は原点にあり、半径は10.0です。
  2. (普通)原点を中心としてShapeの大きさを2.0倍に拡大する関数doubleScaleAndCenterを書いてみましょう。
  3. (普通)Shapeからテキストを抽出する関数shapeTextを書いてください。 この関数はMaybe Stringを返しますが、もし入力がTextを使用して構築されたのでなければ、返り値にはNothing構築子を使ってください。

newtype

代数的データ型の特殊な場合として、 newtype と呼ばれるものがあります。newtypeはキーワード dataの代わりにキーワード newtypeを使用して導入します。

newtype宣言では過不足なく1つだけの構築子を定義しなければならず、その構築子は過不足なく1つだけの引数を取る必要があります。 つまり、newtype宣言は既存の型に新しい名前を与えるものなのです。 実際、newtypeの値は、元の型と同じ実行時表現を持ってるので、実行時性能のオーバーヘッドがありません。 しかし、これらは型システムの観点から区別されます。 型安全性に追加の層を与えるのです。

例として、ボルト、アンペア、オームのような単位を表現するために、Numberの型レベルの別名を定義したくなる場合があるかもしれません。

newtype Volt = Volt Number
newtype Ohm = Ohm Number
newtype Amp = Amp Number

それからこれらの型を使う関数と値を定義します。

calculateCurrent :: Volt -> Ohm -> Amp
calculateCurrent (Volt v) (Ohm r) = Amp (v / r)

battery :: Volt
battery = Volt 1.5

lightbulb :: Ohm
lightbulb = Ohm 500.0

current :: Amp
current = calculateCurrent battery lightbulb

これによりつまらないミスを防ぐことができます。 例えば電源なし2つの電球により生み出される電流を計算しようとするなどです。

current :: Amp
current = calculateCurrent lightbulb lightbulb
{-
TypesDoNotUnify:
  current = calculateCurrent lightbulb lightbulb
                             ^^^^^^^^^
  Could not match type
    Ohm
  with type
    Volt
-}

もしnewtypeなしに単なるNumberを使っていたら、コンパイラはこのミスを捕捉できません。

-- これもコンパイルできますが、型安全ではありません。
calculateCurrent :: Number -> Number -> Number
calculateCurrent v r = v / r

battery :: Number
battery = 1.5

lightbulb :: Number
lightbulb = 500.0

current :: Number
current = calculateCurrent lightbulb lightbulb -- 捕捉されないミス

なお、newtypeは単一の構築子しかとれず、構築子は単一の値でなくてはなりませんが、newtypeは任意の数の型変数を取ることができます。 例えば以下のnewtypeは妥当な定義です(erraは型変数で、CouldError構築子は型Either err a単一の値を期待します)。

newtype CouldError err a = CouldError (Either err a)

また、newtypeの構築子はよくnewtype自身と同じ名前を持つことがあります。 ただこれは必須ではありません。 例えば別個の名前であっても正しいものです。

newtype Coulomb = MakeCoulomb Number

この場合Coulombは(引数ゼロの)型構築子で、MakeCoulombデータ構築子です。 これらの構築子は異なる名前空間に属しており、Voltの例でそうだったように、名前には一意性があります。 これは全てのADTについて言えることです。 なお、型構築子とデータ構築子には異なる名前を付けられますが、実際には同じ名前を共有するのが普通です。 前述のAmpVoltの場合がこれです。

newtypeの別の応用は、実行時表現を変えることなく、既存の型に異なる 挙動 を加えることです。その利用例については次章で 型クラス をお話しするときに押さえます。

演習

  1. (簡単)WattNumbernewtypeとして定義してください。それからこの新しいWatt型と前述のAmpVoltの定義を使ってcalculateWattage関数を定義してください。
calculateWattage :: Amp -> Volt -> Watt

Watt中のワット数は与えられたAmp中の電流と与えられたVoltの電圧の積で計算できます。

ベクターグラフィックスライブラリ

これまで定義してきたデータ型を使って、ベクターグラフィックスを扱う簡単なライブラリを作成していきましょう。

Pictureという型同義語を定義しておきます。 これはただのShapeの配列です。

type Picture = Array Shape

不具合を修正しているとPictureStringとして表示できるようにしたくなることもあるでしょう。 これはパターン照合を使用して定義されたshowPicture関数でできます。

showPicture :: Picture -> Array String
showPicture = map showShape

試してみましょう。 モジュールを spago buildでコンパイルし、 spago replでPSCiを開きます。

$ spago build
$ spago repl

> import Data.Picture

> showPicture [ Line { x: 0.0, y: 0.0 } { x: 1.0, y: 1.0 } ]

["Line [start: (0.0, 0.0), end: (1.0, 1.0)]"]

外接矩形の算出

このモジュールのコード例には、 Pictureの最小外接矩形を計算する関数 boundsが含まれています。

Bounds型は外接矩形を定義します。

type Bounds =
  { top    :: Number
  , left   :: Number
  , bottom :: Number
  , right  :: Number
  }

Picture内の Shapeの配列を走査し、最小の外接矩形を累算するため、boundsには Data.Foldablefoldl関数を使用しています。

bounds :: Picture -> Bounds
bounds = foldl combine emptyBounds
  where
  combine :: Bounds -> Shape -> Bounds
  combine b shape = union (shapeBounds shape) b

基底の場合では、空の Pictureの最小外接矩形を求める必要がありますが、emptyBoundsで定義される空の外接矩形がその条件を満たしています。

累算関数combinewhereブロックで定義されています。 combinefoldlの再帰呼び出しで計算された外接矩形と、配列内の次の Shapeを引数に取り、ユーザ定義の演算子unionを使って2つの外接矩形の和を計算しています。 shapeBounds関数は、パターン照合を使用して、単一の図形の外接矩形を計算します。

演習

  1. (普通)ベクターグラフィックライブラリを拡張し、Shapeの面積を計算する新しい操作 areaを追加してください。 この演習の目的上は、線分やテキストの面積は0であるものとしてください。
  2. (難しい)Shape型を新しいデータ構築子 Clippedで拡張してください。 Clippedは他の Pictureを矩形に切り抜きます。 切り抜かれた図形の境界を計算できるよう、shapeBounds関数を拡張してください。 なお、これによりShapeは再帰的なデータ型になります。 手掛かり :コンパイラは必要に応じて他の関数を拡張するのに付き添ってくれるでしょう。

まとめ

この章では、関数型プログラミングから基本的ながら強力なテクニックであるパターン照合を扱いました。複雑なデータ構造の一部分と照合するために、簡単なパターンの使い方だけではなく、配列パターンやレコードパターンを使った深さのあるデータ構造の一部分との照合方法を見てきました。

また、この章ではパターン照合に密接に関連する代数的データ型も紹介しました。 代数的データ型のおかげでデータ構造を簡潔に記述でき、新たな操作でデータ型を拡張する上で、モジュール性のある方法が得られるのでした。

最後に行多相を扱いました。 これは強力な抽象化をする型であり、これにより多くの既存のJavaScript関数に型を与えられます。

本書では今後も代数的データ型とパターン照合をいろんなところで使用するので、今のうちにこれらに習熟しておくと後で実を結ぶことでしょう。これ以外にも独自の代数的データ型を作成し、パターン照合を使用してそれらの型を使う関数を書いてみてください。