Haskellでいかに多態を表すか
オブジェクト指向を行使する心 ではオブジェクト指向の必要性と仕組みについて議論した。
インスタンスは言語によって様々な実装方法があるが、大きく分けて「クラス(処理)のインデックス」か「処理そのもの」のどちらかがインスタンスの内部に隠れている。
と述べたが、Haskellの場合、クラスのインデックスに基づいた表現では、インターフェイスは型クラス、クラスはインスタンス、インスタンスは存在量化された型の値に対応する。…といってもややこしいことこの上ないので、実装例を考えてみよう。
まず、問題となっている愚直な実装は、Haskellではこんな感じだ。
data World = World { … } data SceneId = Menu | Map | Battle draw :: SceneId -> World -> IO World draw Menu = … draw Map = … draw Battle = …
Worldは描画に必要なすべての情報が入っている、グローバル変数のようなものと考えてよい。新しいシーンを追加するにはSceneId
とdraw
を両方書き換える必要があるため扱いにくく、シーンではなくキャラクターなどを管理するとなればなおさらだ。
存在量化を用いると以下のように書ける。
data Menu = Menu data Map = Map data Battle = Battle class Scene a where draw :: a -> World -> IO World instance Scene Menu where draw = … instance Scene Map where draw = … instance Scene Battle where draw = … instance Scene SomeScene where draw (SomeScene a) = draw a data SomeScene = forall a. Scene a => SomeScene a
data Menu = Menu
のくだりは若干ばかげているようにも見えるが、定義にシーン固有の値を追加してもよい。各シーンを表す値であるMenu
、Map
、Battle
をSomeScene
で包むと、内部にはインスタンスを識別するためのタグが入り、型を共通にしつつ実行時に処理を切り替えられる。標準ライブラリのControl.Exception
ではこの方法を採用しており、便利ではあるがSomeScene
のようなものを毎回作らなければならないのがいまいちだ。
インスタンスに直接処理を格納するアプローチとしてはHaskell's overlooked object system*1のHListがある。異なった型の要素を持てるリストを用い、操作の直接的な集まりとしてインスタンスを表す。
import Data.HList draw = Label :: Label "draw" type Scene = Record '[Tagged "draw" (World -> IO World)] sceneMenu :: Scene sceneMenu = draw .=. … .*. emptyRecord sceneMap :: Scene sceneMap = draw .=. … .*. emptyRecord sceneBattle :: Scene sceneBattle = draw .=. … .*. emptyRecord
こちらはシーンを値として定義できるのが魅力だ。演算子(#) :: HasField l r v => r -> Label l -> v
でdraw
の処理を呼び出せる。しかしその裏の仕組みがとても複雑なうえ、IORef
などを用いないと状態をまともに扱えないのが難点である。
objectiveはそのどちらでもなく、オブジェクトの中身は「操作を解釈する関数」になっている。
newtype Object f g = Object { runObject :: forall x. f x -> g (x, Object f g) }
あくまで操作とオブジェクトは別の概念として扱うのが特徴で、種が* -> *
ならば任意のデータ型を操作として利用できる。Scene
型のオブジェクトはSceneOp
を受け取りStateT World IO
の型のアクションを生み出す。
import Control.Object data SceneOp a where Draw :: SceneOp () type Scene = Object SceneOp (StateT World IO) sceneMenu :: Scene sceneMenu = Object $ \Draw -> … sceneMap :: Scene sceneMap = Object $ \Draw -> … sceneBattle :: Scene sceneBattle = Object $ \Draw -> …
runObject
にオブジェクトとDraw
を渡すと、オブジェクトはDraw
の結果と次のオブジェクトを返す。これをそのまま使ってもよいが、new :: Object f g -> IO (Instance f g)
を用いてインスタンスを生成することもできる。(.-) :: Instance f g -> f x -> g x
を使うと、インスタンスは次のオブジェクトで置き換わるため、広く使われているメソッド呼び出しも可能だ。
オブジェクトは関数と同様のコンポーザビリティを持ち、既存のオブジェクト指向の実装を超える拡張方法も提供する。具体的な利用については以下のちゅーんさんの記事で詳しく述べられているので、こちらも参照されたい。
存在量化、HList、objectiveは多態を実現するが、それぞれ表現方法は全く異なる。純粋、ファーストクラス、合成可能、この言葉にピンときたらobjectiveを是非使ってみてほしい。
*1:Oleg Kiselyov, Ralf Lämmel , http://arxiv.org/pdf/cs/0509027.pdf, 2008
オブジェクト指向を行使する心
今日、とあるツイートでプログラミングにおけるよくある問題が顕現していた。
プログラミングしてそうなサークル探したら、ゲーム公開してて、ソースコード公開されてたから見た。 pic.twitter.com/7W09sb9DFa
— タコス(祭り) (@tacosufestival) 2015, 4月 4
奇妙な行コメントには目を瞑るとして、このコードは要約すれば以下のような処理を実現していることが窺える。
- ゲームプログラミングでは、現在のシーンによって処理を切り替える必要がある。メニュー画面ならメニューの処理を、戦闘画面なら戦闘を、マップならマップの表示をそれぞれ行う。
- 現在のシーンの種類は変数によって与えられる。
- その変数の値によって、対応する処理を選ぶ。
こうしてみると単純だが、caseによる単純な分岐では扱いにくい。新しいシーンを作るたびに場合分けを書き換えなければならないし、何よりそれは「処理」と「処理を表す値」の一覧表を作るという面白みのない処理だからだ。
一つの(そして、よく用いられる)解決法はオブジェクト指向プログラミングである。各シーンをオブジェクトとして扱うことにより、問題となっている分岐を扱う必要がなくなる。
新たに、操作の集まりによって定義される「インターフェイス」という構造を導入する。以下の疑似コードでは、Scene
として扱う型は、draw
という処理が使えることを示している。
interface Scene: draw()
インターフェイスによって定められた操作を実装するのが「クラス」である。Menu
、Map
、Battle
という3つのクラスを定義し、それぞれ異なるdraw
の実装を持っている。
class Scene ⇒ Menu: draw(): … class Scene ⇒ Map: draw(): … class Scene ⇒ Battle: draw(): …
これらの処理を実際に使うには「インスタンス」を用いる。
s ← new Menu()
これはMenu
の実体を持つインスタンスを生成し、sに代入する操作を表す。ここで得られたインスタンスs
はScene
の型を持つ(サブタイピングがあれば、Menu
の型を持ってもよいが、Scene
であることがここでは重要である)。
s.draw()
はScene
が保証しているdraw
の処理を実行する。Menu
のインスタンスなのでMenu
のdraw
が実行されるが、仮にnew Map()
で生成した場合はMap
のdraw
になる。ここで注目すべきは、コードや型は一緒でも、実際に行われる処理は動的に決まるという点だ。今までは、シーンを表す値を見て処理を自分で選択しなければならなかったが、その必要がなくなっているのがわかる。したがってシーンの種類が増えても、draw
を呼ぶ部分を修正する必要はない。
なぜこのようなことを可能にしているのか?そのからくりはインスタンスに宿っている。インスタンスは言語によって様々な実装方法があるが、大きく分けて「クラス(処理)のインデックス」か「処理そのもの」のどちらかがインスタンスの内部に隠れている。
前者の場合、s.draw()
は、s
の中身のインデックスを元に、対応するクラスのdraw
を取り出して実行する。new Menu()
で作ったインスタンスには、Menu
を表すインデックスが入っており、new Battle()
で作ればBattle
を表す値が入っている(文字列でも数値でも、一意に対応させられれば何でもよい)。今までの方法と実は全く同じだが、自動化されていると言える。
後者はより簡単で、インスタンスにはインターフェイスが保証する処理の実装がすべて入っている。s.draw()
は、s
の中に入っているdraw
の処理を取り出しているに過ぎない。
C++などの静的型付きの言語は前者を、Pythonなどの動的型付きの言語は後者を取る傾向がある。なお、静的型付きの言語であるHaskellではどちらの方法でも実現できるが、あまり使われていない。
いずれにせよ、特にゲームプログラミングにおいて、動的に処理を選択したい場合は少なくない。オブジェクト指向がその便利な解決法であることは間違いなく、実際にゲームプログラミングで使われている言語のほとんどはオブジェクト指向をサポートしている。
モノイドと継続渡しの道具箱
関数型言語Haskellにおいて、普通は計算の結果は関数の戻り値として扱うが、「結果を受け取る関数」 に渡すという継続渡しというスタイルもある。これは単なる冗長なやり方ではなく、様々な興味深い性質を持つ。
基本形は、a
という値を渡すところを
∀r. (a -> r) -> r
のような表現にする。たとえば、与えられた数の42倍を渡したいとき、そのまま\x -> x * 42
ではなく、\x f -> f (x * 42)
と書く。もちろんこれだけではありがたみが分からない。
さて、与えられた文字列の中のうち、大文字のアルファベットを取り出し、それがアルファベットの何番目か計算するプログラムを作りたい。普通はリストを使ってこのように書くかもしれない。
import Data.Char uppers :: [Char] -> [Int] uppers [] = [] uppers (x:xs) | isUpper x = fromEnum x - fromEnum 'A' : uppers xs | otherwise = uppers xs
継続渡しにすると([Int] -> r) -> r
という形にもできるが、あえて(Int -> r) -> r
のようにInt
を渡したい場合はどうなるだろうか?すると、r
のために新たな演算が必要になる。
uppers :: (Int -> r) -> [Char] -> r uppers f [] = (空っぽ) uppers f (x:xs) | isUpper x = (がっちゃんこ) (f (fromEnum x - fromEnum 'A')) (uppers f xs) | otherwise = uppers f xs
ここで(空っぽ)
と(がっちゃんこ)
の型に着目しよう。
(空っぽ) :: r
(がっちゃんこ) :: r -> r -> r
つまり、r
は空の値と、結合する演算を持つような型であることがわかる。これはモノイドと呼ばれる代数的構造である。HaskellではMonoid
型クラスとして提供されている。
class Monoid a where mempty :: a (<>) :: a -> a -> a -- 実際は(<>) = mappendとして定義されている
Monoid r
の制約をつけ、空っぽとがっちゃんこはそれぞれmempty
と(<>)
で置き換えてやれば望みのプログラムは作れる。
uppers :: Monoid r => (Int -> r) -> [Char] -> r uppers f [] = mempty uppers f (x:xs) | isUpper x = f (fromEnum x - fromEnum 'A') <> uppers f xs | otherwise = uppers f xs
大文字をカウントするには、要素を数えるような振る舞いを持つモノイドを作ればよい。
data Count = Count { getCount :: Int } instance Monoid Count where mempty = Count 0 Count m <> Count n = Count (m + n) single :: a -> Count single _ = Count 1
実際にはSumというモノイドが定義されており、Countと同様、足し算がなすモノイドとして働く。
uppers
にsingle
関数を渡すとCountが返ってくる。その中身は数え立てほやほやの大文字の個数だ。
getCount . uppers single :: [Char] -> Int
uppersは、リスト以外の構造に対しても考えられる一方、要素の数え上げを実現するCount
は、uppersの扱う対象の構造には関与しない。その関心の分離に、このスタイルのパワーが表れている。
uppers :: Monoid r => (Char -> r) -> Map String Char -> r uppers :: Monoid r => (Char -> r) -> Maybe Char -> r uppers :: Monoid r => (Char -> r) -> Seq Char -> r
標準ライブラリでは、uppersのような操作を抽象化するFoldable
というクラスが定義されている。
foldMap
は、コンテナf a
の要素をすべて与えられた関数に渡すことが期待されている(期待されているというのは、一部だけ渡しても、あるいはまったく渡さなくても正当なインスタンスたりうる)。
class Foldable f where foldMap :: Monoid r => (a -> r) -> f a -> r
なお、大文字のみをフィルターする機能は独立して定義することが可能だ。
filtering :: Monoid r => (a -> Bool) -> (a -> r) -> a -> r filtering p f a | p a = f a | otherwise = mempty
また、要素のマッピングも独立した関数として定義できる。こちらは単なる関数合成だ。
maps :: (a -> b) -> (b -> r) -> a -> r maps f g = g . f
foldMap
、filtering
、maps
を組み合わせれば、uppers
は以下のように書ける。上から下に処理の流れが表現されているのがわかるだろうか。
uppers :: (Foldable f, Monoid r) => (Int -> r) -> f Char -> r uppers = foldMap . filtering isUpper . maps (subtract (fromEnum 'A') . fromEnum)
このような継続とモノイドを用いた畳み込みの仕組みは、高い柔軟性と美しい合成を提供する。
しかし、これでは不足する場合がある。というのも、foldMap
は元の構造を忘れてしまうので、構造を保ったまま要素を書き換えたりする目的には使えない。たとえば、シーザー暗号を実装したい場合、今までのuppers
では元のリストを失ってしまうため実現できない。
元のuppers
の定義に戻ってみよう。(空っぽ)
と(がっちゃんこ)
に元の構造を取り戻すヒントを教えてやれば、どうにかうまくやれそうだ。
uppers' :: (Int -> (Intを保つ何か)) -> [Char] -> ([Char]を保つ何か) uppers' f [] = (空っぽ) [] uppers' f (x:xs) | isUpper x = (がっちゃんこ) (:) x' (uppers' f xs) | otherwise = (がっちゃんこ) (:) ((空っぽ) x) (uppers' f xs) where x' = (ごにょ) (\n -> toEnum $ n + fromEnum 'A') (f (fromEnum x - fromEnum 'A'))
ここでの(空っぽ)、(ごにょ)、(がっちゃんこ)はそれぞれ以下のような型を持つはずだ。
(空っぽ) :: s -> (sを保つ何か) (ごにょ) :: (a -> b) -> (aを保つ何か) -> (bを保つ何か) (がっちゃんこ) :: (a -> s -> (sを保つ何か)) -> (aを保つ何か) -> (sを保つ何か) -> (sを保つ何か)
(xを保つ何か)というのは型パラメータx
を持つ型で表せる。ここではf
としよう。
(空っぽ) :: s -> f s (ごにょ) :: (a -> s) -> f a -> f s (がっちゃんこ) :: (a -> s -> f s) -> f a -> f s -> f s
これらの操作ができるようなf
にはApplicative
という名前がついている。
class Functor f where fmap :: (a -> b) -> f a -> f b instance Functor f => Applicative f where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b liftA2 :: Applicative f => (a -> b -> f c) -> f a -> f b -> f c liftA2 f a b = fmap f a <*> b
(ごにょ)
がfmap
、(空っぽ)
、(がっちゃんこ)
がそれぞれpure
とliftA2
である。すると、uppers'
はこう書ける。
import Control.Applicative uppers' :: Applicative f => (Int -> f Int) -> [Char] -> f [Char] uppers' f [] = pure [] uppers' f (x:xs) | isUpper x = liftA2 (:) x' (uppers' f xs) | otherwise = fmap (x:) (uppers' f xs) where x' = fmap (\n -> toEnum $ n + fromEnum 'A') (f (fromEnum x - fromEnum 'A'))
Applicativeは「元の構造を保てる」モノイドになっている。もしシーザー暗号を実装する場合、結果として[Char]そのものが欲しい。
そんな時は、元の構造をそのまま包むIdentity
が使える。
newtype Identity a = Identity { runIdentity :: a } instance Functor Identity where fmap f (Identity a) = Identity (f a) instance Applicative Identity where pure = Identity Identity f <*> Identity a = Identity (f a)
シーザー暗号は以下のように実装できる。Identity
は操作をそのまま中身に伝えるので、純粋な結果が得られる。
caesar :: Int -> [Char] -> [Char] caesar k = runIdentity . uppers' (Identity . (`mod`26) . (+k))
いちいちIdentity
とrunIdentity
を書くのは骨なので、以下のような関数で共通化すると便利だ。
purely :: ((a -> Identity a) -> s -> Identity s) -> (a -> a) -> s -> s purely t f = runIdentity . t (Identity . f)
各要素に対してアクションを走らせたい場合は、それをそのまま渡すだけだ。一番簡単なパターンかもしれない。
printUppers :: [Char] -> IO [Char] printUppers = uppers' (\x -> print x >> return x)
もし今までと同じように元の構造を捨て、モノイドにしたいときは、そのようなふるまいを持つApplicative
が使える。
newtype Const r a = Const { getConst :: r } instance Functor (Const r) where fmap _ (Const r) = Const r instance Monoid r => Applicative (Const r) where pure _ = Const mempty Const a <*> Const b = Const (a <> b)
uppers'
ではfmap
とpure
に元の構造のための操作が渡されていたが、Const
はそれらを捨てており、代わりにモノイドの演算をしていることがわかる。以下のsmash
は、Const
を用いて「格下げ」を行う関数で、uppers = smash uppers'
の関係が成り立つ。
smash :: ((a -> Const r a) -> s -> Const r s) -> (a -> r) -> s -> r smash t f = getConst . t (Const . f)
この強化されたuppers'
だが、こちらも共通化のためのクラスが提供されており、こちらはTraversable
と呼ばれている。traverse
はfoldMap
の上位版に位置する。
class (Functor t, Foldable t) => Traversable t where traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
フィルターは今までとほぼ同じような形で、独立して定義できる。
filtered :: Applicative f => (a -> Bool) -> (a -> f a) -> a -> f a filtered p f a | p a = f a | otherwise = pure a
要素へのマッピングをするには、元の構造に戻すための関数が追加で必要となる。
isomorphic :: Functor f => (s -> a) -> (a -> s) -> (a -> f a) -> s -> f s isomorphic f g c = fmap g . c . f
traverse
、filtered
、isomorphic
を使うとuppers'
はこう書ける。Int
をChar
に戻す関数が必要になったのを除けば、Foldable
版とほぼ変わらない。
uppers' :: (Applicative f, Traversable t) => (Int -> f Int) -> t Char -> f (t Char) uppers' = traverse . filtered isUpper . isomorphic (subtract (fromEnum 'A') . fromEnum) (\n -> toEnum $ n + fromEnum 'A')
このように、foldMap
やtraverse
に代表されるようなこの手の関数は、コンテナの処理に対して素晴らしい表現力を持つ。
ならばこれを最大限に活用しようと作られたのがlensパッケージだ。
uppers'
のような関数はtraversalと呼ばれ、lens
パッケージでは型シノニムが定義されている。
type Traversal' s a = forall f. Applicative f => (a -> f a) -> s -> f s
たとえば、コンテナの特定の要素を指すtraversalなどを提供している。
ix :: Ixed m => Index m -> Traversal' m (IxValue m) -- ix :: k -> Traversal' (Map k a) a -- ix :: Int -> Traversal' (Vector a) a
また、ここで紹介したpurely
、smash
、isomorphic
に相当するものだけでなく、traversalを構築および使用する手段が豊富に提供されている。
lensが扱っていない範囲でも、この考え方はプログラミングに役に立つ。計算結果をリストか何かで返そうと思ったとき、是非このスタイルも思い出してみてほしい。
Haskellにおけるたった一つのデザインパターン
デザインパターンを作らないこと。型とクラスがあんたの武器だ。
出、出~~wwwww銀行員待行列解説奴~wwwwwww
銀行員待行列(Banker's deque)、二つのリストで構成奴~~wwwww
入奴と出奴~wwwwwwwww
↓入奴
三(^o^)ノ [(^o^)ノ, (^o^)ノ, (^o^)ノ]
ヽ(^o^)三 [ヽ(^o^), ヽ(^o^), ヽ(^o^)]
↑出奴
追加は入奴にcons、取り出しは出奴にuncons奴~wwwリストなので基本定数時間奴~wwwwww
リスト枯渇防止の為、リストの長さに以下の条件課奴~~~wwwwww
length (入奴) <= length (出奴) * 3 + 1
length (出奴) <= length (入奴) * 3 + 1
条件充足不能場合、|length (入奴) - length (出奴)| <= 1
なるよう余剰分反転後短い側の末尾に結合して調整奴~wwwww時間計算量O(length (入奴) + length (出奴))必要~~~~wwwwww
動作奴~wwww個数nのリストを[n]
表記奴~wwwwwww
入、入~~wwwww調整奴~~wwwwww
[1][1]
入、入、入~wwwww調整不要~wwwwwwww
[4][1]
入~~wwwwwここで調整奴~~wwwwwww
[5][1] → [3][3]
入、入、入、入、入入入入~wwwwww再度調整奴~wwwwww
[11][3] → [7][7]
入入入入入入入入入入入入入入入入wwwwwwww調整必要奴~wwwwwww
[23][7] → [15][15]
入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入~~wwwwwww調整奴~wwwwwww
[47][15] → [31][31]
入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入~~wwwwwwwやっと調整奴~wwwwwww
[95][31] → [63][63]
調整間隔指数関数的増大奴~wwwwwwwww
相対的調整コスト、限りなく0に漸近奴~~~wwwww
収束故、償却定数時間奴~~~~wwwwwwwww
取り出しも同様奴~wwwwwww
出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出~~~~wwwwwww
[63][20] → [41][40]
出出出出出出出出出出出出出出出出出出出出出出出出出出出出出~wwwwww
[40][12] → [26][26]
長ければ長いほど調整コスト相殺奴~~wwwwwwwwww
実装簡単、効率的データ構造奴~~~wwww
参考文献奴~wwwww
- Chris Okasaki (1999). Purely Functional Data Structures
ぼくのかんがえた最強の拡張可能レコード
注意(2018/04) かなり古い記事なので、extensibleの最新のバージョンとはまったく互換性がない
__
動機
GHCに、OverloadedRecordFields(ORF)という拡張の導入が提案されている。
(Records/OverloadedRecordFields/Design – GHCより) Haskellのレコードの深刻な欠点は、フィールドをオーバーロードできないことだ。例えば、
data Person = Person { personId :: Int, name :: String } data Address = Address { personId :: Int, address :: String }
と定義したとき、personId
はどちらを参照すべきかわからない。よくある対策としてデータ型に接頭辞をつけるという方法があるが、コードやフィールド間の関係が不明瞭になる。モジュール単位で修飾するという方法もあるが、レコードごとにモジュールを作るのは非実用的だ。
そこで、personId
をその型によって解決するような、多相なフィールドの写像を扱いたい。r { x :: t }
で、r
はx
という名前の、型t
のフィールドを持つことを表す、新しい形の制約の記法が必要となる。そうすれば、以下のような記述が可能になる。
getPersonId :: r { personId :: Int } => r -> Int getPersonId e = personId e
問題点
最も大きな問題は、フィールドがファーストクラスでないことである。 lensなどのライブラリは、フィールドをデータとして表すことにより、合成などの演算を可能にしている。しかし、ORFのフィールドの概念は制約としてのみ現れる。残念ながら、Haskellは制約レベルのプログラミングは得意ではないため、非常に扱いにくいのだ。
解決
ならばORFに代わる最強の多相レコードを作ってやろう、と私は立ち上がった。仕様は以下の通りだ。
- フィールドは値であり、フィールド名と、それが指し示す対象を型によって決めることができる。
- レコードの型は、フィールド名の集まりによって決まる。
- フィールドはLensとして扱える(重要)。
- レコードの構築にフィールドを使える。必要なフィールドが欠けている場合、型エラーになる。
まず、フィールド名を型に対応させるために型族を用いる。
type family FieldValue (s :: Symbol) :: *
Symbol
は型レベル文字列の型(DataKinds拡張を使う)で、*
は値の型である。
type instance FieldValue "foo" = Bar
と定義すれば、名前"foo"に型Barを対応させることができる。
そして、フィールドの型を以下のように定義する。
data Field (s :: Symbol) = Field { getField :: FieldValue s }
そして、Field
の拡張可能な集まりとして、レコードを実現する。「拡張可能な集まり」のために、拙作のextensibleを利用した。
extensibleパッケージでは、積の表現として(:*)
という型を定義している。型h :* [a, b, c, …]
(型レベルリストを使用していることに注意されたい)は、(h a, h b, h c, …)
のタプルに相当する。直接(a, b, c, …)
と対応させない理由は、すぐに明らかになる。
積を扱うのに必要なAPIはこの3つだ。
Nil :: h :* [] (<:*) :: h x -> h :* xs -> h :* (x : xs) sector :: (x ∈ xs) => Lens' (h :* xs) (h x)
Nilと(<:*)はnilとcons、sector
は、積の特定の要素に対するLens
である。x ∈ xs
は、型レベルリストxs
の中にx
がただ一つ存在することを表す。
ちなみに、x
がない場合、Couldn't match type ‘Missing x’ with ‘Expecting one’
、x
が重複している場合は‘Couldn't match type ‘Ambiguous x’ with ‘Expecting one’
というエラーが出る。地味な売りの一つだ。
そんなわけで、Field :* '["foo", "bar", "baz"]
とすると、(Field "foo", Field "bar", Field "baz")
相当になり、その中身は、foo, bar, bazがFieldValue
が指し示す型になるのだ。レコードの型を以下のように定義しよう。
type Record = (:*) Field
FieldValue
のインスタンスと、Lens (x ∈ xs) => Lens' (Record xs) (FieldValue x)
はできれば一緒に生成したいが、こんなのはTemplate Haskellをちょいと練ればすぐにできるだろう。
mkField "personId" [t|Int|] -- personId :: ("personId" ∈ xs) => Lens' (Record xs) Int mkField "name" [t|String|] mkField "address" [t|String|]
getPersonId
はORF版と同じくらいシンプルに表現でき、言語自体を拡張する必要もない。
getPersonId :: ("personId" ∈ xs) => Record xs -> Int getPersonId = view personId
さて、問題は最後の要件だ。定義したLensをそのまま使い、こんな風に書ければ理想的ではある。
(@=) :: ((s ∈ xs) => Lens' (Record xs) (FieldValue s)) -> FieldValue s -> Field s (@=) _ = Field fubuki :: Record '["personId", "name"] fubuki = personId @= 1 <:* name @= "吹雪" <:* Nil
しかし、mkField
によって生成されるLensと、型族FieldValueは、どちらもフィールド名を具体化するのには使えず、(@=)
は作れないのだ!この世の終わりだー!
諦めるのはまだ早い。実は、既存のLensのインターフェイスを損なうことなく、フィールド名をLensに忍ばせることが可能なのである。
ちょっとした型クラスを用意する。
class Labelable s p where unlabel :: proxy s -> p a b -> a -> b instance Labelable s (->) where unlabel _ = id {-# INLINE unlabel #-}
フィールドの型を以下のように定義し直す。比較のため、普通のLens'
の場合をその下に書く。
type FieldLens s = forall f p xs. (Functor f, Labelable s p, s ∈ xs) => p (FieldValue s) (f (FieldValue s)) -> Record xs -> f (Record xs) -- Lens' (Record xs) (FieldValue s) = forall f. (Functor f) -- => (FieldValue s -> f (FieldValue s)) -> Record xs -> f (Record xs) -- field f = sector (fmap Field . unlabel f . getField)のような形になる(実際は型アノテーションが必要)
(->)
をLabelable p => p
に一般化しているのがポイントだ。これを使うことによって、以下のように単相な「代表」を構成し、フィールド名の曖昧性を排除できるのだ!
data LabelPhantom s a b instance (s ~ t) => Labelable s (LabelPhantom t) where unlabel _ = error "Impossible" type FieldName s = LabelPhantom s (FieldValue s) (Proxy (FieldValue s)) -> Record '[s] -> Proxy (Record '[s])
これのおかげで、無事に(@=)
コンビネータを定義できる。
(@=) :: FieldName s -> FieldValue s -> Field s (@=) _ = Field
めでたしめでたし…いや、志の高い諸君ならば、束縛の順番を入れ替えても大丈夫なようにしたいだろう。そんなときのために、Data.Extensible.Inclusion
のshrink
関数が使える。
shrink :: (xs ⊆ ys) => h :* ys -> h :* xs
前に定義したように、Record
は単なる型シノニムなので、shrink
を適用することによって順番を自由に入れ替えられる(し、一部を切り出すこともできる)。もちろん、型エラーの読みやすさは損なわれていないので、気になる人は試してみるとよいだろう。
fubuki :: Record '["personId", "name"] fubuki = shrink $ name @= "吹雪" <:* personId @= 1 <:* Nil
こうして、街に平和が訪れためうー!型レベルプログラミングのおかげだね!
このアイデアの実装はGitHubのextensibleのリポジトリにあり、次のextensibleのリリースに組み込む予定だ。
結論
OverloadedRecordFieldsなんていらんかったんや!
オブジェクト指向関連の不満な点
また遅れてしまった…オブジェクト指向 Advent Calendar11日目。
最近いろいろオブジェクト指向について調べていて、やっぱりこの概念はいかがなものか、と思ったことを軽く書く。
継承
まずこれ。オブジェクト指向において「AがBを継承している」というのは、外延的には「任意のBのメソッドはAのメソッドである」であり、実装としては「AはBが持つ内部状態やメソッドの実装を利用できる」ことが多い。しかし、Twitterにも似たようなことを書いたが、「車クラスが乗り物クラスを継承している」みたいなゆるふわな使い方が目立つ。クラス、インターフェイスの用語と、それらの継承の定義ははっきりさせたい。則のない関係に意味なし。
抽象クラス、抽象メソッド
実装のない「抽象メソッド」と実装のあるメソッドをごちゃ混ぜにできるような仕様は害があると考えている。インターフェイスがあるにもかかわらずこのような中途半端な仕組みを作るのは、不完全な抽象化と複雑化の原因になりうる。
多重継承
暗黙に何もかも引き継いでしまう仕様と、複数の概念から引き継ぐ仕様は本質的に両立しない。言語の簡単さを犠牲にして小難しいテクニックを導入するより、どこから引き継ぐのか明示したほうが楽に見える。
デザインパターン
デザインパターンは「道具」ではなく、「よくあるパターン」である。なぜそのパターンになったかを感覚的に理解せずに、デザインパターンを「使おう」としてもうまくいかないのに、なぜありがたがっている人が多いのかよくわからない。また、言語設計者はデザインパターンを不要にしていく方向への努力が必要だと考えている。
オブジェクト指向
すべてオブジェクトで扱うのが正義という風潮、これはよくない。オブジェクト指向は、内部状態を持った概念に対する操作の列(メインループでUpdateメソッドを繰り返し呼ぶなど)を抽象化するとき初めて必要になるものだと私は考えている。オブジェクト指向の表現力が高いのは確かだが、だからといって基本的な型や関数をおろそかにするのは本末転倒だ。
私が満足できるようなオブジェクト指向プログラミングができる言語はまだ存在しない。近いうちに理想的なオブジェクト指向プログラミング言語を作って、オブジェクト指向はどうあるべきか、また考えてみたい。