extensible/Field
Field はレコードとバリアントのための包装型で、構成要素に名前を付けることができます。特にラッパーの概念は extensible を理解する上で非常に大切なので、きちんと抑えておきましょう。 code: (hs)
Field :: (val -> *) -> Assoc key val -> *
つまり、引数はそれぞれこんな感じの意味です。
第一引数: 任意のラッパー
第二引数: 名前keyと中身の型valの組
値と型のレベルで考えてみよう
いきなり型と種の関係を把握するのが難しい人は、とりあえず値と型の関係で理解してみましょう。
Haskell の値と型のレベルで考えるならば、だいたいこんな感じです。
code: (hs)
field :: (val -> c) -> (key, val) -> c
field f (k, v) = f v
それぞれ以下のように対応します。
field と Field
(key, val) と Assoc
具体例を考えてみよう!
Field はとても抽象的なので、何か具体的な型で考えることにしましょう。
code: (hs)
Field :: (val -> *) -> Assoc key val -> *
key や val は型変数 (種変数?) なのでどんな種でも問題ありませんが、ここではわかりやすく * に限定してみます。
code: (hs)
Field :: (* -> *) -> Assoc k * -> *
では Field の第一引数に何か型を適用してみます。 (* -> *) で一番身近な存在なのは、たぶん Maybe でしょう。
Maybe の種を一応確認しておきます。
code:_
*Main> :k Maybe
Maybe :: * -> *
Field Maybe の種は当然、以下のようになります。
code: (hs)
Field Maybe :: Assoc k * -> *
この演算子は以下のような種を持ちます。
code:_
*Main> :k (>:)
(>:) :: k -> v -> Assoc k v
*Main> :k (>:) "foo"
(>:) "foo" :: v -> Assoc Symbol v
*Main> :k (>:) "foo" Int
(>:) "foo" Int :: Assoc Symbol *
なんとなく値と型と種の関係がわかってきましたか?
この型を最後に適用するとやっと種が * になりました。これは Field Maybe (String >: Int) が Haskell の何らかの型であることを意味します。
code:_
*Main> :k Field Maybe ("foo" >: Int)
Field Maybe ("foo" >: Int) :: *
最初に確認した種と比較してみましょう。
code: (hs)
Field :: (val -> *) -> Assoc key val -> *
Field Maybe (Symbol >: Int) :: *
今回の例では Maybe がラッパーになります。extensible の関数で f x, g x, h x のような型が頻出しますが、f、g、hはラッパーを表しています。
extensible ではラッパー型と中身の型(この場合MaybeとInt)がそれぞれ別に存在している点が重要なポイントです。
レコードやバリアントを利用する際は、このラッパーが Identity となっているため Identity や runIdentity を使って構成子をつけたり、剥がしたりする必要があるのです。
通常のレコードにおけるフィールドとの関係
例えば以下のような Person 型が定義されていたとしましょう。
code: (hs)
data Person = Person
{ name :: Text
, age :: Int
}
name や age はレコードのフィールドと呼ばれています。
Field も同じように考えることができます。
つまり、name を key, Text を value として持つフィールドと解釈できます。
extensible では、上記の例を以下のような Field 型として扱います。
code: (hs)
Field Identity ("name" >: Text)
Field Identity ("age" >: Int)
ラッパーが Identity となっていることで、 エフェクトが何もない (= Haskell の純粋な) フィールドということを明示的に表しています。
Key / Value
それぞれの種を確認すれば、なんとなく動作がわかります。
code: (hs)
AssocKey :: Assoc k v -> k
AssocValue :: Assoc k v -> v
例えば Assoc KnownSymbol String に対して、AssocKey, AssocValue を適用すると、それぞれ以下の型になります。
code: (hs)
AssocKey (Assoc KnownSymbol String) = KnownSymbol
AssocValue (Assoc KnownSymbol String) = Symbol
Filed の値を作ろう!
今までは種に焦点を当てて解説してきましたが、ここでは実際に Field の値を作ってみましょう。
まずは、値を作るためには Field 構成子の型を確認します。
code:_
*Main Data.Functor.Identity> :t Field
Field :: h (AssocValue kv) -> Field h kv
とりあえず値を作りたいのでここでは h と AssocValue kv を以下のようにしてみます。
code: (hs)
h = Identity
AssocValue kv = AssocValue (Assoc Symbol String)
= String
h (AssocValue kv) = Identity String
となるため、 Identity String 型になるような値を作れば良さそうです。
とりあえず Field (Identity "test") であれば問題ないような気がしますが、以下のようなエラーになりました。
code:_
*Main> Field (Identity "test")
<interactive>:94:1: error:
• Couldn't match expected type ‘Char’ with actual type ‘AssocValue kv0’
The type variable ‘kv0’ is ambiguous
• In the first argument of ‘print’, namely ‘it’
In a stmt of an interactive GHCi command: print it
Fieldはトップレベルの型を省略すると基本的に曖昧だと言われてエラーになります。そのため、以下のような型注釈をつける必要があります。
code:_
*Main> Field (Identity "test") :: Field Identity ("name" :> String)
name @= "test"
やっと上手くいきました。
別の例も試してみましょう。ラッパーは Identity 以外にも色んなものを指定できます。次は Maybe にしてみます。
code:_
*Main> Field (Just "test") :: Field Maybe ("name" :> String)
name @= Just "test"
*Main> Field Nothing :: Field Maybe ("name" :> String)
name @= Nothing
このようにして値を作ることができました。
しかしながら、毎回 Field を付けるのは面倒ですし、型を明示的に指定するのもつらいです。そのため、通常は @= 等の演算子を利用します。 code:_
*Main> #name @= (Just "test") :: Field Maybe ("name" :> String) name @= Just "test"
*Main> #name @= "test" :: Field Identity ("name" :> String) name @= "test"
この場合は、やっぱり型注釈が必要になりますが、普通にプログラムを作っている時はかなり型推論を助けてくれます。
Constraint を作る
ForAll などに渡す制約を作りたい場合に、これらの関数を使うと便利です。
code: (hs)
KeyValue :: (k -> Constraint) -> (k1 -> Constraint) -> Assoc k k1 -> Constraint
KeyIs :: (k -> Constraint) -> Assoc k v -> Constraint
ValueIs :: (v -> Constraint) -> Assoc k v -> Constraint
上記の関数を使えば Assoc の key と value を制限することができます。
code: (hs)
Show :: * -> Constraint
KeyValue KnownSymbol Show :: Assoc Symbol * -> Constraint
KeyIs KnownSymbol :: Assoc Symbol v -> Constraint
ValueIs Show :: Assoc k * -> Constraint
KeyValue KnownSymbol Show はキーが型レベル文字列で、実体の型がShow クラスのインスタンスであるという制約を意味します。