型クラスインスタンスの曖昧さへの対処
Haskell において、同じ型に対して複数のモノイド則が成立する場合があります。例えば整数型 Int は、加法に関するモノイド(単位元 0、演算 +)としても、乗法に関するモノイド(単位元 1、演算 *)としても振る舞うことができます。通常、型クラスはインスタンス化された型のみで解決されるため、このままではどちらの演算を使用するかコンパイラが判断できません。
この問題を解決する手法の一つが Phantom Type(虚無型)の活用です。値レベルでは使用されない型パラメータを導入することで、演算の種類を型レベルで区別し、型安全に関数を呼び分けることが可能になります。
実装例
以下のコードでは、演算モードを表す空のデータ型を定義し、それを型パラメータとして持つ型クラス Combiner を作成しています。これにより、同じ Int 型に対して異なる演算規則を適用するインスタンスを定義できます。
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module PhantomCalc where
import Prelude hiding (Monoid, mempty, (<>))
-- 演算の種類を区別するための空のデータ型
data AddMode
data MulMode
-- 複数の型パラメータを受け取るクラス定義
class Combiner mode value where
combine :: value -> value -> value
identity :: value
-- 加法モードにおける Int のインスタンス
instance Combiner AddMode Int where
combine = (+)
identity = 0
-- 乗法モードにおける Int のインスタンス
instance Combiner MulMode Int where
combine = (*)
identity = 1
-- 型アプリケーションを用いてモードを指定しながら畳み込みを行う
reduce :: forall mode value. Combiner mode value => [value] -> value
reduce [] = identity @mode
reduce (x:xs) = combine @mode x (reduce @mode xs)
-- 具体的な関数として公開
totalSum :: [Int] -> Int
totalSum = reduce @AddMode
totalProd :: [Int] -> Int
totalProd = reduce @MulMode
必要な言語拡張の説明
このパターンを実現するには、いくつかの GHC 言語拡張を有効にする必要があります。それぞれの役割は以下の通りです。
MultiParamTypeClasses
型クラスに複数の型パラメータを許可します。ここでは演算モードを表す mode と、実際の値の型を表す value の 2 つを定義するために使用します。
FlexibleInstances
インスタンス宣言における型変数の制約を緩和します。これにより、instance Combiner AddMode Int のように、具体的な型に対してインスタンスを定義できるようになります。
ScopedTypeVariables
関数の型署名で明示された型変数を、関数本体でも参照可能にします。forall mode value. と宣言することで、本体内の identity @mode などで mode を参照できるようになります。
AllowAmbiguousTypes
型パラメータが関数の引数や戻り値の型に現れない場合、通常は曖昧であると判断されエラーになります。Phantom Type は値レベルでは使用されないため、この拡張を有効にして型チェックの制限を緩和する必要があります。
TypeApplications
関数を呼び出す際に、明示的に型パラメータを指定できる構文です。@AddMode や @MulMode を使用することで、どの演算モードを使用するかを呼び出し側で決定できます。
実行結果
定義した関数は、型アプリケーションを通じて意図した演算が選択されていることを確認できます。
*PhantomCalc> totalSum [1, 2, 3, 4]
10
*PhantomCalc> totalProd [1, 2, 3, 4]
24
このように、Phantom Type を利用することで、実行時オーバーヘッドを生じさせることなく、型システムのみで演算の振る舞いを厳密に制御する仕組みを構築できます。