TypeFamilyDependencies の実用的な例を考える
この記事は以下のページに移転しました.
FunctionalDependencies
という GHC 言語拡張がある.Haskell Wiki によると,
Functional dependencies are used to constrain the parameters of type classes.
と書かれているが,これはどういうことか.
Haskell Language Report で定められた範囲では,型クラスに与えられるパラメータは1つに限られるが,MultiParamTypeClasses
を用いると,複数のパラメータを与えることができる.この際に,パラメータとして与えられた (複数の) 型の間の関係性に制限を加えることができるのが,FunctionalDependencies
なのであった.恐らく多くの人が初めて目にするのは,mtl
package の MonadReader
の定義 なのではないだろうか.| m -> r
というのがそれである.
class Monad m => MonadReader r m | m -> r where ...
さて,GHC 8 から TypeFamilyDependencies
という GHC 言語拡張が追加された.これについては既に lotz 先生が『型族が単射だと嬉しい理由』という記事を書いていらっしゃる のだが,(氏には失礼ながら) 少しばかりわざとらしい例だと感じたので,もう少し実務的な例を引き合いに出して,有用性を示したいと思う.
この記事では,以下の GHC 言語拡張を使う.また,GHC のバージョンは 8.2.2 である.
AllowAmbiguousTypes
OverloadedStrings
ScopedTypeVariables
TypeApplications
TypeFamilies
TypeFamilyDependencies
以下のような User
型と UserKey
型を関係データベースで扱いたいとしよう.UserKey
型は (典型的には auto increment な) primary key だと思ってもらえば良い.
data User = User { userName :: String , userAge :: Int } newtype UserKey = UserKey { unUserKey :: Int } deriving (Eq, Show)
さてここで,DBの中では primary key として INT
型を用いたいが,エンドユーザからはその整数の表現を隠蔽したいものとしよう.そのためには,整数をいい感じにエンコード・デコードしたものを,primary key の表現として用いればよさそうだ.このようなモチベーションはよくあるので,"Hashids" *1というライブラリが公開されている.これは様々な言語向けに実装が提供されていて,Haskell 版も Hackage から入手可能である.
このライブラリが (DB 上での内部表現である) Int
と (エンドユーザが目にする表現である) ByteString
の間の相互変換を提供してくれる.エンコードされた UserKey
を表現する,次のような型を定義しよう.
newtype EncodedUserKey = EncodedUserKey { unEncodedUserKey :: ByteString } deriving (Eq, Show)
次に,UserKey
と EncodedUserKey
を相互に変換する encodeUserKey
関数と decodeUserKey
関数を定義する.
encodeUserKey :: UserKey -> EncodedUserKey encodeUserKey (UserKey n) = EncodedUserKey $ encodeUsingSalt "this is my salt" n decodeUserKey :: EncodedUserKey -> Maybe UserKey decodeUserKey (EncodedUserKey x) = case decodeUsingSalt "this is my salt" x of [n] -> Just (UserKey n) _ -> Nothing
GHCi で挙動を確認してみよう.
> encodeUserKey $ UserKey 42 EncodedUserKey {unEncodedUserKey = "eP"} > decodeUserKey $ encodeUserKey $ UserKey 42 Just (UserKey {unUserKey = 42})
さてここで User
に加えて,新たに Team
という概念が増えたとしよう.Team
の primary key も User
と同様に,hashids を使って隠蔽したいとする.
data Team = Team { teamName :: String , teamUsers :: [User] } deriving (Show) newtype TeamKey = TeamKey { unTeamKey :: Int } deriving (Eq, Show) newtype EncodedTeamKey = EncodedTeamKey { unEncodedTeamKey :: ByteString } deriving (Eq, Show) encodeTeamKey :: TeamKey -> EncodedTeamKey encodeTeamKey (TeamKey n) = EncodedTeamKey $ encodeUsingSalt "another salt" n decodeTeamKey :: EncodedTeamKey -> Maybe TeamKey decodeTeamKey (EncodedTeamKey x) = case decodeUsingSalt "another salt" x of [n] -> Just (TeamKey n) _ -> Nothing
User
の場合とまったく同じ実装になってしまったので,これらを型クラスで抽象化しよう.先に User
型や Team
型を取って,その key を返す型レベル関数 Key
を type family を用いて定義する.つまり,
User
UserKey
Team
TeamKey
なる型レベル関数である.また,ついでなので,Key
の中身の Int
を取り出したり,また Int
を取って Key
を作る部分を抽象化しておく.
-- 何かしらの key を持つことを表す型クラス class HasKey a where type Key a -- a をとって key を返す型レベル関数 (e.g. Key User = UserKey) wrapKey :: Int -> Key a unwrapKey :: Key a -> Int instance HasKey User where type Key User = UserKey wrapKey = UserKey unwrapKey = unUserKey instance HasKey Team where type Key Team = TeamKey wrapKey = TeamKey unwrapKey = unTeamKey
今しがた定義した HasKey
を前提に,いよいよ HasCodableKey
型クラスを定義する.
-- Key を hashids でエンコード・デコードできることを表す型クラス class HasKey a => HasCodableKey a where -- エンコードされた key (e.g. EncodedUserKey) type EncodedKey a wrapEncodedKey :: ByteString -> EncodedKey a unwrapEncodedKey :: EncodedKey a -> ByteString -- salt は変えられるようにしておく salt :: ByteString encodeKey :: Key a -> EncodedKey a encodeKey key = let n = unwrapKey key bs = encodeUsingSalt (salt @a) n in wrapEncodedKey bs decodeKey :: EncodedKey a -> Maybe (Key a) decodeKey encodedKey = let bs = unwrapEncodedKey encodedKey ns = decodeUsingSalt (salt @a) bs in case ns of [n] -> Just (wrapKey n) _ -> Nothing
上のように定義すると,メッチャ怒られる.
/Users/ryota-ka/dev/tf-deps-example/src/Lib.hs:64:27: error: • Couldn't match expected type ‘Key a0’ with actual type ‘Key a’ NB: ‘Key’ is a type function, and may not be injective The type variable ‘a0’ is ambiguous • In the first argument of ‘unwrapKey’, namely ‘key’ In the expression: unwrapKey key In an equation for ‘n’: n = unwrapKey key • Relevant bindings include key :: Key a (bound at src/Lib.hs:63:15) encodeKey :: Key a -> EncodedKey a (bound at src/Lib.hs:63:5) | 64 | let n = unwrapKey key | ^^^ /Users/ryota-ka/dev/tf-deps-example/src/Lib.hs:66:12: error: • Couldn't match expected type ‘EncodedKey a’ with actual type ‘EncodedKey a1’ NB: ‘EncodedKey’ is a type function, and may not be injective The type variable ‘a1’ is ambiguous • In the expression: wrapEncodedKey bs In the expression: let n = unwrapKey key bs = encodeUsingSalt (salt @a) n in wrapEncodedKey bs In an equation for ‘encodeKey’: encodeKey key = let n = unwrapKey key bs = encodeUsingSalt (salt @a) n in wrapEncodedKey bs • Relevant bindings include key :: Key a (bound at src/Lib.hs:63:15) encodeKey :: Key a -> EncodedKey a (bound at src/Lib.hs:63:5) | 66 | in wrapEncodedKey bs | ^^^^^^^^^^^^^^^^^ /Users/ryota-ka/dev/tf-deps-example/src/Lib.hs:70:35: error: • Couldn't match expected type ‘EncodedKey a2’ with actual type ‘EncodedKey a’ NB: ‘EncodedKey’ is a type function, and may not be injective The type variable ‘a2’ is ambiguous • In the first argument of ‘unwrapEncodedKey’, namely ‘encodedKey’ In the expression: unwrapEncodedKey encodedKey In an equation for ‘bs’: bs = unwrapEncodedKey encodedKey • Relevant bindings include encodedKey :: EncodedKey a (bound at src/Lib.hs:69:15) decodeKey :: EncodedKey a -> Maybe (Key a) (bound at src/Lib.hs:69:5) | 70 | let bs = unwrapEncodedKey encodedKey | ^^^^^^^^^^ /Users/ryota-ka/dev/tf-deps-example/src/Lib.hs:74:20: error: • Couldn't match type ‘Key a’ with ‘Key a3’ Expected type: Maybe (Key a) Actual type: Maybe (Key a3) NB: ‘Key’ is a type function, and may not be injective The type variable ‘a3’ is ambiguous • In the expression: Nothing In a case alternative: _ -> Nothing In the expression: case ns of [n] -> Just (wrapKey n) _ -> Nothing • Relevant bindings include encodedKey :: EncodedKey a (bound at src/Lib.hs:69:15) decodeKey :: EncodedKey a -> Maybe (Key a) (bound at src/Lib.hs:69:5) | 74 | _ -> Nothing | ^^^^^^^
よくよく読んでみると,「型レベル関数である Key
とか EncodedKey
が injective ではないぞ」と言われている.
関数 が injective (単射) であるとは, について ということであるが,直感的には で写した先の集合 で要素が互いにぶつからないとイメージすることができる.今回の場合 EncodedKey
は,
User
EncodedUserKey
Company
EncodedCompanyKey
といった挙動をするが,User
以外の適当な型 a
を持ってきて,それを EncodedUserKey
に写されると困る のである.
関数で写した先でぶつからないということは,取りも直さず写した先の要素 から,写す前の要素 を一意に特定できるということを意味する.つまり,EncodedUserKey
から User
型を特定でき,EncodedTeamKey
からは Team
型を特定することができる.「この型レベル関数はこのように injective に振る舞いますよ,(a
から EncodedKey a
が定まることは当然として,逆に)EncodedKey a
から a
が定まることを前提に型推論してくださいね」という注記を与えるための機能こそが TypeFamilyDependencies
だったのだ.
では実際に TypeFamilyDependencies
を有効にして,先程のコードの型検査が通るように書き換えてみよう.
-- before class HasKey a where type Key a class HasKey a => HasCodableKey a where type EncodedKey a -- after class HasKey a where type Key a = r | r -> a class HasKey a => HasCodableKey a where type EncodedKey a = r | r -> a
ここで r
なり何なり適当な名前を与えてあげないと,単射性の制約が書けない*2.r -> a
の部分は,型 r
から型 a
が一意に定まることを表している.
カインドの制約が書きたければ,次のようにも書くことができる.
class HasKey a where type Key a = (r :: *) | r -> a
これで無事に HasCodableKey
型クラスが定義できたので,User
と Team
をこいつのインスタンスにしてやって,期待通りの動作をすることを確認しておこう.
instance HasCodableKey User where type EncodedKey User = EncodedUserKey wrapEncodedKey = EncodedUserKey unwrapEncodedKey = unEncodedUserKey salt = "this is my salt" instance HasCodableKey Team where type EncodedKey Team = EncodedTeamKey wrapEncodedKey = EncodedTeamKey unwrapEncodedKey = unEncodedTeamKey salt = "another salt"
> encodeKey $ UserKey 42 EncodedUserKey {unEncodedUserKey = "eP"} > decodeKey $ encodeKey $ UserKey 42 Just (UserKey {unUserKey = 42}) > encodeKey $ TeamKey 42 EncodedTeamKey {unEncodedTeamKey = "5Q"} > decodeKey $ encodeKey $ TeamKey 42 Just (TeamKey {unTeamKey = 42})
今回のコードの全文は以下のとおりである.
{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilyDependencies #-} module Lib where import Data.ByteString (ByteString) import Web.Hashids (encodeUsingSalt, decodeUsingSalt) data User = User { userName :: String , userAge :: Int } newtype UserKey = UserKey { unUserKey :: Int } deriving (Eq, Show) data Team = Team { teamName :: String , teamUsers :: [User] } newtype TeamKey = TeamKey { unTeamKey :: Int } deriving (Eq, Show) -- 何かしらの key を持つことを表す型クラス class HasKey a where type Key a = (r :: *) | r -> a -- a をとって key を返す型レベル関数 (e.g. Key User = UserKey) wrapKey :: Int -> Key a unwrapKey :: Key a -> Int instance HasKey User where type Key User = UserKey wrapKey = UserKey unwrapKey = unUserKey instance HasKey Team where type Key Team = TeamKey wrapKey = TeamKey unwrapKey = unTeamKey newtype EncodedUserKey = EncodedUserKey { unEncodedUserKey :: ByteString } deriving (Eq, Show) newtype EncodedTeamKey = EncodedTeamKey { unEncodedTeamKey :: ByteString } deriving (Eq, Show) -- Key を hashids でエンコード・デコードできることを表す型クラス class HasKey a => HasCodableKey a where -- エンコードした後のkey (e.g. EncodedUserKey) type EncodedKey a = (r :: *) | r -> a wrapEncodedKey :: ByteString -> EncodedKey a unwrapEncodedKey :: EncodedKey a -> ByteString -- salt は変えられるようにしておく salt :: ByteString encodeKey :: Key a -> EncodedKey a encodeKey key = let n = unwrapKey key bs = encodeUsingSalt (salt @a) n in wrapEncodedKey bs decodeKey :: EncodedKey a -> Maybe (Key a) decodeKey encodedKey = let bs = unwrapEncodedKey encodedKey ns = decodeUsingSalt (salt @a) bs in case ns of [n] -> Just (wrapKey n) _ -> Nothing instance HasCodableKey User where type EncodedKey User = EncodedUserKey wrapEncodedKey = EncodedUserKey unwrapEncodedKey = unEncodedUserKey salt = "this is my salt" instance HasCodableKey Team where type EncodedKey Team = EncodedTeamKey wrapEncodedKey = EncodedTeamKey unwrapEncodedKey = unEncodedTeamKey salt = "another salt"