extensible パッケージの楽しみ その1」です.

拡張可能レコードやら Extensible Effect やら,Haskell の Extensible なものを全て統一された仕組みで提供する化け物パッケージ extensible について,割とドキュメントには無い(?)ネタを書いておくシリーズ第一弾です. ぼく自身は作者ではないし,間違っているかもなのでこの記事を完全には当てにしないでください.

ちなみに,作者様の有り難い日本語ドキュメントは以下の通り(古い順).

また,現在の最新バージョンは 0.4.6 です(そのバージョンでハナシをしてる).

拡張可能レコード

Haskell のレコード型にはイロイロと問題がある. この辺りは lotz 氏の GitHub リポジトリが参考になります.

型システム的には「2. 部分的である」が一番致命的だと思うが,実用的には「1. 名前問題が解決できていない」が本当につらい. 例えば,なんかのユーザーアカウントを管理するために,ユーザーアカウントの型 User を次のように定義したとする.

data User =
  { id :: ID
  , name :: Text
  , age :: Int
  }

このとき,idname はタダの関数として定義されるので,id :: a -> a 関数などと 名前が衝突してしまう . なので,userId とか userName にしないといけない... OOP 言語でのクラスフィールドはクラスの中で名前空間が閉じているので,フィールド名を気にする必要が無いのだが,Haskell のレコード型は非常に残念な仕様だ...

そこで利用するのが拡張可能レコード. (lotz氏のまとめによると)いろんな実装があるみたいだが,今回取り上げるのは extensible というパッケージ. このパッケージでは 型レベル文字列と任意の型のタプルを型レベルリストで保持してレコードを表現している とイメージである.

さっきの User 型を extensible の拡張可能レコードで書くと次のようになる.

type User = Record
  '[ "id" >: ID
   , "name" >: Text
   , "age" >: Int
   ]

ちなみに,型レベルリストや型レベル文字列を使うのに DataKinds 拡張が,型演算子 (>:) を使うのに TypeOperators 拡張が要る. この型の値は次のように定義できる.

user1 :: User
user1
   = #id @= "U123456789"
  <: #name @= "Alice"
  <: #age @= 24
  <: emptyRecord

ちなみに,#id などを使うのには OverloadedLabels 拡張が必要. フィールドにアクセスするには lens パッケージの (^.) などを用いる.

ghci> user1 ^. #id
"U123456789"
ghci> user1 ^. #age
24

レコード型を拡張する

ココからが本題.

以前こんなツイートを見かけた.

そう思う(便乗).

これ,何を言ってるかと言うと,前に定義した User 型を更に拡張した User' 型を次のように定義したとする.

type User' = Record
  '[ "id" >: ID
   , "name" >: Text
   , "age" >: Int
   , "address" >: Text
   ]

で,前の User 型の値 user1address フィールドを加えただけで User' 型の値を定義できないか?というハナシ(たぶんね). もちろん,バニラな Haskell ではできないでしょう.

しかし,extensible パッケージならどうなの??という話になりまして. 最初は無理なんかなぁと思ったけど,ちゃんと調べてみたらできた. 流石 extensible

フィールドの順序

なんで最初は無理なのかと思ったかと言うと,型レベルリストは順序も大事 だからです. そりゃ集合じゃなくてリストなんだからそうだよね. 以下の二つの型は違う.

type A = Record '[ "foo" >: Text, "bar" >: Text ]
type B = Record '[ "bar" >: Text, "foo" >: Text ]

試しに Proxy を使って比較すると

ghci> (Proxy :: Proxy A) == (Proxy :: Proxy A)
True
ghci> (Proxy :: Proxy B) == (Proxy :: Proxy B)
True
ghci> (Proxy :: Proxy A) == (Proxy :: Proxy B)
<interactive>:11:24: error:
Couldn't match type"bar"’ with ‘"foo"
      Expected type: Proxy A
        Actual type: Proxy B
In the second argument of ‘(==)’, namely ‘(Proxy :: Proxy B)’
      In the expression: (Proxy :: Proxy A) == (Proxy :: Proxy B)
      In an equation for ‘it’:
          it = (Proxy :: Proxy A) == (Proxy :: Proxy B)

で,これを一緒にすることが出来るのが shrink という関数.

ghci> (Proxy :: Proxy A) == fmap shrink (Proxy :: Proxy B)
True

あら不思議,一致しました.

種明かし

型を見てみると

shrink :: xs ⊆ ys => (h :* ys) -> h :* xs

なんですよ. お察しの通り,キモは xs ⊆ ys という型クラス(h :* xs はレコード型そのもので,xs はフィールドの型レベルリストだと思って欲しい). 細かい定義は抜きにして,この意味は「ysxs が持つフィールドをすべて持ってる」って感じだと思う. あくまで持っているかなので,順番は関係なく,変換後の xs の順番にしてくれる. すごいね.

(ちなみにこのあたりの話は「ぼくのかんがえた最強の拡張可能レコード - モナドとわたしとコモナド」の最後にチロっと書いてあった)

順番があっていれば簡単

で,話を戻すと. もし,User' 型が次のような定義なら,ものすごーーーく話が簡単になる.

type User'' = Record
  '[ "address" >: Text
   , "id" >: ID
   , "name" >: Text
   , "age" >: Int
   ]

リストなんだから先頭にコンスしてやればよい.

ghci> #address @= "Japan" <: user1 :: User''
address @= "Japan" <: id @= "U123456789" <: name @= "Alice" <: age @= 24 <: nil

もちろん,User' 型なら怒られる

ghci> #address @= "Japan" <: user1 :: User'
<interactive>:37:1: error:
Couldn't match type'Missing "address"
                     with ‘'Expecting (n0 'Data.Extensible.:> v20)’
        arising from the overloaded label ‘#address’
In the first argument of ‘(@=)’, namely ‘#address’
      In the first argument of ‘(<:)’, namely ‘#address @= "Japan"
      In the expression: #address @= "Japan" <: user1 :: User'

魔法の shrink

まぁあとはお察しの通り,shrink を適用してやればよい

ghci> shrink $ #address @= ("Japan" :: Text) <: user1 :: User'
id @= "U123456789" <: name @= "Alice" <: age @= 24 <: address @= "Japan" <: nil

型注釈を加えてやらないといけないが,目的のことができた(Bool 型のように値が多相的じゃなければ型注釈は必要ないはず).

レコード型を縮小する

shrink の名前や xs ⊆ ys の見た目からわかるように,本来は縮小するのに用いる. 仮に次のような User''' 型を定義する.

type User''' = Record
  '[ "name" >: Text
   , "age" >: Int
   ]

User''' 型よりフィールドの種類が多い User 型の値 user1 から,User''' 型の値を生成するには,単純に shrink 関数を適用するだけで良い.

>> shrink user1 :: User'''
name @= "Alice" <: age @= 24 <: nil

すごいね.

おまけ: 拡張可能直和型

shrink の定義の下に,spread と言う関数があり,名前や型から察するに shrink の逆っぽい.

spread :: xs ⊆ ys => (h :| xs) -> h :| ys

(:|) が違う. shrink(:*) だった. 実は,(:*) は直積型,(:|) は直和型を表している. (:|) をラップした(ような)型が Variant 型である.

type Color = Variant
  '[ "rgb" >: (Int,Int,Int)
   , "cmyk" >: (Int,Int,Int,Int)
   ]

これは data Color = RGB Int Int Int | CMYK Int Int Int Int を拡張可能にした感じである.

ghci> color1 = embed $ #rgb @= (0 :: Int, 0 :: Int, 0 :: Int) :: Color
ghci> color1
EmbedAt $(mkMembership 0) (rgb @= (0,0,0))
ghci> color2 = embedAssoc $ #cmyk @= (0,0,0,0) :: Color
ghci> color2
EmbedAt $(mkMembership 1) (cmyk @= (0,0,0,0))

embed 関数を使って KeyValue #rgb @= (0,0,0)Color 型のひとつ目の要素に持ち上げている. ただし,Int がうまく推論できないようなので,型注釈を加えてやる必要がある. embedAssoc 関数なら,Color 型の KeyValue から推論してくれるようだ.

魔法の spread

もうわかる通り,spread 関数は直和型のサブセットしかない直和型を拡張する関数だ.

>> type RGB = Variant '[ "rgb" >: (Int,Int,Int) ]
>> type CMYK = Variant '[ "cmyk" >: (Int,Int,Int,Int) ]
>> color3 = embed $ #rgb @= (0 :: Int, 0 :: Int, 0 :: Int) :: RGB
>> color4 = embedAssoc $ #cmyk @= (0,0,0,0) :: CMYK
>> color1' = spread color3 :: Color
>> color2' = spread color4 :: Color

すごいね(笑)

おしまい

なんども言うけど,ぼくが作ったわけではないからね.