- はじめに
============
Haskell/GHCの型システムを使って、「混ぜることのできない数」のグループを手軽に作る方法を紹介します。
Haskellの基本的な機能と、GHC拡張を1つだけ使用しています。
なお、ここで紹介する方法は、Don Stewartさんが昔に、"Haskell in the Large"という資料(現在、オリジナル資料はリンク切れ)で紹介していたものを元にしています。
また、本記事で出てくるnewtype宣言やphantom型や数値型については、以下の記事などが参考になります。
- すごいH本で見落としがちだが実は重要な機能:newtype
- 本当はすごい newtype
- で、出たー!幽霊型だー!(Phantom Type)
- Haskellらしさって?「型」と「関数」の基本を解説! (数値型と型クラスについて簡単に)
では、順に試しながら機能を追加していきます。
最初に基本的な型を包む独自の数値型をつくり、その後、それらを互いに区別できるように改善します。
(なお、各節に対応する短いソースファイルをGitHubのリポジトリに置いておきました。)
- まず、独自の数値型の基本を作る
==================================
まずは、基本的な独自の数値型を作ります。
以下、GHCの対話環境であるghciコマンド上で試していきます。(本記事では、ghci>
の表記により、ghciコマンドの入力プロンプトを表しています。ghciのデフォルトでは、入力プロンプトをPrelude>
と表示しています。)
2.1 まずは、newtype宣言で独自の数値型を作る
まずは次のように、newtype
宣言を使用して、既存のInt
型を包む新しい型Value
を作ってみます。1
ghci> newtype Value = Val Int
新しく作った型を試してみましょう。Value
型の値(Val 1
)を作って、変数x
に対応づけてみます。
ghci> x = Val 1
変数x
の型を、ghciの:type
コマンドを使って確認してみます。
ghci> :type x
x :: Value
変数x
は、Value
型として認識されています。新しい、独自の数値型を作れました。
2.2 独自の数値型を、表示や、同値比較や、大小比較できるようにする
続いて、独自に作った数値型を、少し便利にしてみます。
Haskellのderiving(自動導出)機能を使って、表示(Show
)、同値比較(Eq
)、大小比較(Ord
)のための関数を、独自に作った数値型に導入します。2
以下のように、newtype
宣言にderiving (Show, Eq, Ord)
節を追加します。
ghci> newtype Value = Val Int deriving (Show, Eq, Ord)
試してみます。まずは、テストのために先程と同様に、変数x
をValue
型の値に割り当てておきます。
ghci> x = Val 2
Show
クラスをderivingしているので、次のように、変数の値を表示できるようになっています。
ghci> x
Val 2
Eq
とOrd
クラスをderivingしているので、==
や<
演算子を使えるようになっています。これにより、同値比較や大小比較ができるようになります。
ghci> x == x
True
ghci> x < x
False
2.3 独自の数値型を、加減乗算できるようにする
さらに、独自に作った数値型を、基本的な算術演算(加減乗算)ができるようにしていきます。
このために1つだけ、GHC拡張を使用します。まず、ghciに次のように入力して、GeneralizedNewtypeDeriving
言語拡張を有効にします。3
これにより、newtype
宣言のderiving
節に、Show
やEq
などの基本的な型クラス以外をも追加できるようになります。4
ghci> :set -XGeneralizedNewtypeDeriving
さらに以下のように、加減算などの算術演算が定義されたNum
クラスを、deriving
節に追加します。5
ghci> newtype Value = Val Int deriving (Show, Eq, Ord, Num)
試してみます。先程と同様に、変数x
をValue
型の値に割り当てておきます。
ghci> x = Val 3
Num
クラスをderivingしているので、作成した型において、+
や*
の演算子を使用できるようになっています。(つまり、自分で+
や*
に相当する演算のコードを実装することなく、+
や*
の機能を実現できます。+
や*
に相当するコードは、自動で導出(deriving)されます。便利ですね。)
ghci> x + x
Val 6
ghci> x * 3
Val 9
なお、Num
クラスをderivingしているので、fromInteger
関数をも暗黙的に使用できるようになります。
これにより、型が明確であれば、値コンストラクタのVal
なしで、直接的に数値リテラルのみを記述できます。
(つまり、x = Val 1
でなく、x = 1
のように記述できます。6)
ghci> x = 1 :: Value
以上のようにして、整数型(Int
)の数値のように振る舞う独自の型Value
型をつくれました。
参考までに、以上の内容をプログラムファイルの形でまとめてみると、次のようになります。
わずかこれだけの記述で、整数型のように振る舞う独自の型を、演算子や関数などを別途自作することなく実現できます。
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
newtype Value = Val Int deriving (Show, Eq, Ord, Num)
- さらに、独自の数値型を、互いに区別できるようにする
=======================================================
続いて、独自の数値型を、互いに区別できるようにしてみます。
例えば、数値のグループの例として、「通貨の円を表すグループ」と「通貨のUSドルを表すグループ」を想定して、それぞれを区別して扱えるようにしてみます。
3.1 まずは、phantom型を使って、独自の型を互いに区別できる仕組みを入れる
これまでのValue
型を改造し、次のように、左辺をValue a
に変更します。
これまでの定義との違いは、a
が追加されていることだけです。
ghci> newtype Value a = Val Int deriving (Show, Eq, Ord, Num)
このa
は型変数です。ただし、右辺の定義には現れない型変数です。右辺に現れない不思議な型の表現のため、phantom型(ファントム型、幽霊型)と呼ばれます。
定義したValue a
型を使ってみましょう。
ghci> x = 10 :: Value a
ghci> :type x
x :: Value a
ghci> x + 1
Val 11
使い勝手は、これまでと同じですね。
さらに、型変数a
の位置には、次のように任意の具体的な型を記述することもできます。
ghci> y = 1 :: Value Float
ghci> z = 1 :: Value Char
とても紛らわしいのですが、上記でa
の位置に記述した型(Float
やChar
)は、「型の目印」として使用されるだけで、式の値がFloat
やChar
になるわけではありません。
式の値は、あくまで、Val Int
です。つまり、Int
型を値コンストラクタVal
で包んだものです。
しかし、この「型の目印」の特徴を使うことで、それぞれの値のグループを区別することができます。
3.2 さらに、区別のための補助的なデータ型を用意する
上記のように、Value a
型を互いに区別するために、型変数a
の位置に既存の型(Float
やChar
)を用いることもできます。
しかし、既存の意味を持ったFloat
やChar
を使用すると誤解のもとなので、データ区別用の補助的なデータ型を用意してみましょう。
次のようにdata
宣言を使用して、補助的な2つのデータ型(ValYen
とValUSD
)を用意します。ここでは、型の世界での区別のみに使用するため、右辺の値を持たないデータ型として定義しています。
それでは、3.1節のnewtype
宣言に加えて、次の2行も入力します。
ghci> data ValYen
ghci> data ValUSD
この補助的なデータ型を使って、Value a
型のa
をそれぞれ具体化してみましょう。以下のように使います。
ghci> y = 100 :: Value ValYen
ghci> z = 1 :: Value ValUSD
Value Float
やValue Char
よりは、分かりやすくなりました。
では、数値のように扱えるか試してみます。
ghci> y * 2
Val 200
ghci> z * 2
Val 2
ghci> y + y
Val 200
ghci> z + z
Val 2
これまでの様に、数値のように使えました。
さらにここで、この記事において最も重要な特徴である、「混ぜられないこと」について試してみます。
上記の変数y
と変数z
は、各々を数値のように扱えますが、互いを混ぜることはできません。以下のように誤って混ぜてしまった場合には、実行時ではなくコンパイル時に、適切に型エラーとして検出してくれます。
ghci> y + z
<interactive>:11:5: error:
• Couldn't match type ‘ValUSD’ with ‘ValYen’
Expected type: Value ValYen
Actual type: Value ValUSD
• In the second argument of ‘(+)’, namely ‘z’
In the expression: y + z
In an equation for ‘it’: it = y + z
このようにして、数値のように使うことができ、かつ、各々のグループを混ぜては使えない、数値のような型を作ることができました。
3.3 最後に、データ型を短く書けるように別名をつける
さらに、型の使い勝手を少し良くしてみます。なお、この節の内容は必須ではありません。
Value ValYen
型やValue ValUSD
型について、type
宣言を用いることで、より短い別名(シノニム)を付けてみます。
3.1節のnewtype
宣言と、3.2節のdata
宣言に加えて、さらに次の2行を入力します。
ghci> type Yen = Value ValYen
ghci> type USD = Value ValUSD
これにより、各々の型名を、Value ValYen
ではなく、Yen
のように簡潔に記述できます。
ghci> y = 100 :: Yen
ghci> z = 1 :: USD
数値のように扱えるか試してみます。
ghci> y + 2*y
Val 300
ghci> z * 2 + 1
Val 3
これまでの様に、数値のように使えました。
なお、Haskellの型推論の機能により、独自に作ったデータ型も正しく伝搬していきます。以下のように、変数y
を用いた式の型は、変数y2
に正しく伝わります。ここでは、変数y
の型Yen
が、変数y2
に伝わっています。
ghci> y2 = y + 30
ghci> :type y2
y2 :: Yen
そして最も重要な特徴ですが、異なる数のグループを混ぜて使った場合には、コンパイル時に正しく型エラーとして検出できます。
ghci> y + z
<interactive>:17:5: error:
• Couldn't match type ‘ValUSD’ with ‘ValYen’
Expected type: Yen
Actual type: USD
• In the second argument of ‘(+)’, namely ‘z’
In the expression: y + z
In an equation for ‘it’: it = y + z
上記では、y
はYen
型であり、z
はUSD
型です。演算子+
は2つの型が同じであることを要求しており、このように型が混在している場合には、エラーとして検出できます。
以上の内容を、ファイルの形でまとめると次のようになります。
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
newtype Value a = Val Int deriving (Show, Eq, Ord, Num)
data ValYen
data ValUSD
type Yen = Value ValYen
type USD = Value ValUSD
このように、わずかな行数、かつ、シンプルな記述により、目的とする機能を実現できました。
以上、Haskell/GHCの基本的な型システムの機能(newtype
, data
, type
, など)を使うだけで、数のように振る舞い、かつ、互いを区別できる「混ぜられない数」を手軽に実現できました。便利ですね!
では、Happy Haskelling!
- 補足
=======
以下は、いくつかの補足情報です。長いので、読み飛ばしてもらっても構いません
4.1 関連仕様書
以下に、関連するいろいろな情報があります(英語版です)。
- Haskell 2010 Language Report (Haskell言語仕様):
- GHC User's Guide (GHCユーザーズガイド):
- 各々の型クラスの実装説明 (ライブラリのドキュメント):
- GHCにおけるderiving機能の実装コード (GitHubのGHCリポジトリ):
4.2 作成した型の使用
作成した型の使用例についての補足です。
次のように、関数の型シグネチャ(型宣言)の部分に、型変数a
の表現を残したValue a
型を使うことにより、Value a
型に共通の関数を定義できます。
次のadd
関数では、「2つの引数と1つの出力が、全て同じ型であること」を、型変数a
を共通にすることにより表現しています。
そのため、Value ValYen
型とValue ValUSD
型を混ぜて使うことを、コンパイルエラーとして検出できます。
add :: Value a -> Value a -> Value a
add x1 x2 = x1 + x2
一方で、次のようにして、対象とする型を限定することもできます。ここでは、Value a
型の型変数a
を、ValYen
にすることで型を具体的にしています。
toYen :: Int -> Value ValYen
toYen x = Val x
4.3 除算の追加
除算の追加についての補足です。
これまでに紹介したNum
クラスには、加減乗算(+
, -
, *
)のための演算子が含まれていますが、除算(div
)の機能は含まれていません。
自作の型において除算(div
)を使用できるようにするには、以下のように、Enum
, Real
, Integral
をもderivingする必要があります。7
newtype Value a = Val Int deriving (Show, Eq, Ord, Num, Enum, Real, Integral)
以下は、使用例です。div
関数を使用できるようになっています。
ghci> x = 100 :: Yen
ghci> x `div` 2
Val 50
4.4 小数を扱う場合
小数の扱いについての補足です。
これまでの例では、newtype
で包む対象の数値型として、整数型のInt
を用いる例を示しました。しかしながら、整数のみではなく、小数点以下の数をも扱いたい場合があると思います。
例4.4.1 : Double型の使用
そのような場合には次のように、整数型のInt
ではなく、浮動小数点型のDouble
を使う方法があります。
なお、包む対象の型としてDouble
型を選択する際に、除算(/
)をも使えるようにしたい場合には、deriving
節にFractional
を追加します。
newtype Value a = Val Double deriving (Show, Eq, Ord, Num, Fractional)
以下は、使用例です。
ghci> x = 100 :: Yen
ghci> x / 3
Val 33.333333333333336
例4.4.2 : Rational型の使用
上記の浮動小数点のDouble
型は、速度とメモリ効率が良い型ですが、小数点以下の桁を正確には表現できません。
そのような場合には次のように、有理数のRational
型を使う方法があります。
Rational
型にて、除算(/
)を使いたい場合にもDouble
型と同様に、deriving
節にFractional
を追加します。
newtype Value a = Val Rational deriving (Show, Eq, Ord, Num, Fractional)
以下は、使用例です。
ghci> x = 100 :: Yen
ghci> x / 3
Val (100 % 3)
4.5 newtypeとphantomの組み合わせ、以外による方法
混ぜられない数のグループを作るための、その他の方法についての補足です。
3章では、newtype
宣言とphantom型の組み合わせにより、互いに区別できる数のグループを作る方法を紹介しました。しかし、互いに区別できる数のグループを作る方法は他にもあります。
例4.5.1 : 各型ごとのnewtype宣言による方法
他の方法の1つめの例として、phantom型を使わない方法があります。
以下のように、各々の型ごとに、newtype
宣言を個別に用意します。
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
newtype Yen = ValYen Int deriving (Show, Eq, Ord, Num)
newtype USD = ValUSD Int deriving (Show, Eq, Ord, Num)
但し、上記の方法では、3章のようにValue a
型を用いて共通の関数を作る方法は使えません。
しかし、以下のように型クラスの機能を用いることにより、両方の型における共通の関数を実現できます。
class Num a => Value a where
add :: a -> a -> a
add x1 x2 = x1 + x2
instance Value Yen
instance Value USD
例4.5.2 : data宣言による方法
他の方法の2つめの例として、newtype
宣言ではなく、data
宣言を使う方法があります。
以下のように、data
宣言の値コンストラクタにより、各々の値のグループを区別します。
data Value = Yen Int
| USD Int
deriving (Show, Eq, Ord)
この方法では、値コンストラクタであるYen
やUSD
を用いることにより、グループに応じた動作を実行時に変更できます。
しかし、両者とも型が共通のValue
型であるため、型の世界で数のグループを互いに区別することはできません。すなわち、数のグループの区別は、コンパイル時ではなく実行時に行うことになります。
また、newtype
宣言ではなくdata
宣言を使う場合には、基本的な型クラス以外をderiving機能により自動導出することはできません。つまり、Num
クラスをderivingできないため、加減乗除の演算などは、自分で実装する必要があります。
4.6 包まれた数値を安全に取り出す
包まれた数値の出し入れについての補足です。
3章では、独自に作った数値型により、各々の数値型を混ぜることなく安全に使える体系を実現できました。しかし、独自に作った数値型から、包まれた数値を取り出す場合に、型を取り間違えてしまう危険性は残ります。
例えば、次のようにValue a
型を入力とし、Int
型を出力とする関数を作る場合です。この場合、Value Yen
型とValue USD
型の引数を間違えて使用しても、コンパイルエラーにはなりません。せっかくの型の体系に危険な穴が開いてしまいます。
fromValue :: Value a -> Int
fromValue (Val x) = x
例えば次のように、各々の数値のグループに対してfromValueを使い間違えても気づきにくいのです。
ghci> y = 100 :: Yen
ghci> z = 1 :: USD
ghci> yen1 = fromValue y
ghci> yen2 = fromValue z -- mistake
これへの対策は幾つかあります。
例4.6.1 : 各型ごとのfromXXX関数
1つめは、抽象的なValue a
型でなく、型変数a
を具体化したValue ValYen
型などを使う方法です。これにより、型を誤って使うことを防ぎます。
ただし、この方法では、数のグループごとに関数を用意(fromYen
, fromUSD
, ...)する必要があります。
fromYen :: Value ValYen -> Int
fromYen (Val x) = x
以下は、使用例です。
ghci> y = 100 :: Yen
ghci> z = 1 :: USD
ghci> fromYen y
100
ghci> fromYen z
<interactive>:10:9: error:
例4.6.2 : 型を、追加の引数により指定するfromValue関数
2つめは、関数の引数を増やして、関数の使用時に型変数a
を具体的にする方法です。
次のfromValue
関数は、1つめの引数の型変数a
と、2つめの引数の型変数a
が共通です。
data ValYen = ValYen
data ValUSD = ValUSD
fromValue :: a -> Value a -> Int
fromValue _ (Val x) = x
次のように使います。fromValue
の1つめの引数において、型変数a
に応じた値を指定することで、型を誤って使うことを防ぎます。
以下の例では、変数y
が、Val ValYen
型でない場合は、コンパイルエラーとして検出できます。
ghci> y = 100 :: Yen
ghci> fromValue ValYen y
100
ghci> fromValue ValUSD y
<interactive>:17:18: error:
4.7 実行時に型を区別したい場合
長くなりましたが、ここで最後です。
実行時に型を区別する方法ついての簡単な補足です。
本記事の方法では、数のグループが混ざってしまう誤りを、コンパイル時に静的に検出できます。一方で、実行時に型を区別したい場合もあります。つまり例えば、実行時に、数のグループに応じて処理を変えたい場合です。
phantom型を使った3章の方法では、値コンストラクタが共通です(共通のVal
)。そのため、例4.5.2におけるdata
宣言を使う方法のように、値コンストラクタを用いて、型に応じて処理を切り替えることはできません。
しかし、GHC拡張機能(DeriveDataTypeable)とライブラリData.TypeableのtypeOf 関数を使うことにより、実行時に型の違いを識別して、型に応じて処理を切り替えることができます。
ここでは詳細は省略しますが、以下がコード例です。
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DeriveDataTypeable #-} -- extension
import Data.Typeable -- import
newtype Value a = Val Int deriving (Show, Eq, Ord, Num, Typeable) -- Typeable
data ValYen
data ValUSD
-- function
isYen :: Typeable a => Value a -> Bool -- Typeable constraint
isYen x = typeOf x == typeOf (1 :: Value ValYen) -- `typeOf`
Data.Typeable
モジュールからimportしたtypeOf
関数により、型の一致を判定して、TrueかFalseを返すことができます。
typeOf
関数を使用するためには、以下の対応が必要です。
-
DeriveDataTypeable
拡張を有効にする -
Data.Typeable
モジュールをimportする - 対象とする型の宣言の
deriving
節に、Typeable
を追加する - 使用する関数の型宣言にて、
Typeable x =>
の型クラス制約を記述する
以下は、使用例です。
ghci> y = 100 :: Yen
ghci> z = 1 :: USD
ghci> isYen y
True
ghci> isYen z
False
実行時に、型に応じて動作を変えられました。
以上です。
本記事ではHaskell/GHCにおける基本的な型の機能により、シンプルに便利なことを実現できることを紹介しました。
では改めて、Happy Haskelling!!
-
ここでは詳細を省略しますが、
newtype
宣言は、data
宣言の特別なバージョンのようなものです。data
宣言とは異なり、右辺には「1つの値コンストラクタと、1つの型」の組み合わせのみを記述できます。また、コンパイルによるコード生成では、「1つの型」側のみが残されるため、実行速度とメモリ使用の効率が良い特徴もあります。 ↩ -
deriving
節により、それぞれの型クラスの関数を導入できます。例えば、Eq
クラスをderivingすることにより、Eq
クラスにて定義されている==
演算子を使用できるようになります。 ↩ -
ghciコマンド内でGHCの言語拡張を有効にするには、
:set -X
に続けて言語拡張名を入力します。また、GHCの言語拡張をプログラムファイル中に記述するには、{-# LANGUAGE GeneralizedNewtypeDeriving #-}
のように記述します。 ↩ -
Haskellの言語仕様では、
deriving
節で自動導出できる型クラスは、Eq, Ord, Enum, Bounded, Read, Showのみです。GHCの言語拡張GeneralizedNewtypeDeriving
により、その他の型クラスの自動導出が、newtype
宣言において可能になります。新たに自動導出できる型クラスは、包む対象の型が属している型クラスです。 ↩ -
Num
クラスでは、+
,-
,*
などの演算子や、abs
,fromInteger
などの関数が定義されています。 ↩ -
x = 1 :: Value
における:: Value
の部分は、「型注釈」です。型注釈の機能により、式の特定の部分に、明示的に型を指定することができます。ここでは、式1
の型がValue
であることを指定しています。 ↩ -
Int型を包む独自の数値型に、Int型が属している型クラスを全て自動導出させたい場合には、さらに
Bounded
クラスをderivingします。注目対象の型がどの型クラスに属しているかを調べるには、ghciコマンドで:info Int
のように実行します。 ↩