このシリーズでは 2018/5/22 にリリースされた PureScript 0.12.0 で何がどう変わったかを紹介していきます。
PureScript 本体だけでなく関連ライブラリの変更についても紹介します。
今回はレコードの話です。
なおこの記事は PureScript に触れたことがある読者を想定しています。
PureScript 自体について知りたい場合には他の Qiita の記事や 実例によるPureScript などが参考になると思います。
レコードとは?
レコードはラベルと型の組み合わせからなる PureScript の組み込み型です。
pochi ∷ { name ∷ String, age ∷ Int }
pochi = { name: "Pochi", age: 3 }
もう少し補足すると、Record
という型コンストラクタが用意されており、型の row1 を渡すとレコードの型が得られます。
{ name ∷ String, age ∷ Int }
は Record ( name ∷ String, age ∷ Int )
の糖衣構文です。
PureScript Language Reference の 7. Records や実例によるPureScriptの第3章 関数とレコードに詳しい説明があります。
何が変わったのか
PureScript 0.12.0 にあわせて更新された purescript-prelude の 4.0.0 で、レコードが下記の型クラスのインスタンスになりました。
- class Show
- class EQ
- class Semiring
- class Ring
- class CommutativeRing
- class HeytingAlgebra
- class BooleanAlgebra
- class Semigroup
- class Monoid
何が出来るようになったのか?
今回の変更によりレコードに対し何が出来るようになったのかを型クラス別に紹介します。
class Show
レコードを表示用の文字列に変換出来るようになりました。
main ∷ Effect Unit
main = do
log $ show { name: "Pochi", age: 3 }
> pulp run
{ age: 3, name: "Pochi" }
なおレコードの各フィールドの型がもれなく class Show
のインスタンスになっている必要があります。
このような制約はこれ以降に紹介する他の型クラスにも同様に存在します。
class EQ
レコードの値が等しいかどうかを判定出来るようになりました。
ret1 = { name: "Pochi", age: 3 } == { name: "Pochi", age: 3 } -- true
ret2 = { name: "Pochi", age: 3 } /= { name: "Mochi", age: 3 } -- true
ret3 = { name: "Pochi", age: 3 } == { name: "Pochi", age: 2 } -- false
すべてのフィールドの値が等しければ等しいとみなされます。
比較対象の型がきっちり一致していなければコンパイルエラーになります。
これは以降に紹介する他の型クラスの二項演算子でも同様です。
なお 。class Ord
のインスタンスではないため大小の比較は出来ません2
[2018/12/16追記]
2018/7/7 にリリースされた purescript-prelude 4.01 で class Ord
のインスタンスになったので、比較もできるようになりました。
class Semiring, class Ring, class CommutativeRing
レコードの足し算、掛け算、引き算が出来るようになりました。
ret1 = { x: 4, y: 3 } + { x: 2, y: 1 } -- { x: 6, y: 4 }
ret2 = { x: 4, y: 3 } * { x: 2, y: 1 } -- { x: 8, y: 3 }
ret3 = { x: 4, y: 3 } - { x: 2, y: 1 } -- { x: 2, y: 2 }
各フィールド毎に演算が行われます。
zero
や one
、negate
ももちろん使えます。
ret1 = zero ∷ { x ∷ Int, y ∷ Int } -- { x: 0, y: 0 }
ret2 = one ∷ { x ∷ Int, y ∷ Int } -- { x: 1, y: 1 }
ret3 = - { x: 1, y: 2} -- { x: -1, y: -2 }
class CommutativeRing
は乗算の交換法則が成り立つことを保証するのみで、関数や演算子は定義されていません。
また、ここでは 3つの型クラスまとめて紹介していますが、フィールドの型が class Semiring
のインスタンスではあっても class Ring
のインスタンスではない場合、足し算や掛け算は出来ても引き算は出来ないレコードになります。
これ以降も型クラス間に super - sub の関係がある場合はまとめて紹介しますが、どこまで出来るかはフィールドの型次第となります。
なお class EuclideanRing
のインスタンスになっていないため割り算は出来ません3。
class HeytingAlgebra, class BooleanAlgebra
レコードの論理演算が出来るようになりました4。
ret1 = { a: true, b: false } && { a: true, b: true } -- { a: true, b: false }
ret2 = { a: true, b: false } || { a: true, b: true } -- { a: true, b: true }
ret3 = not { a: true, b: false } -- { a: false, b: true }
各フィールド毎に演算が行われます。
class BooleanAlgebra
は排中律が成り立つことを保証するのみで、関数や演算子は定義されていません。
class Semigroup, class Monoid
レコードの連結が出来るようになりました5。
ret1 = { a: [ 1 ], b: "Foo" } <> { a: [ 2 ], b: "Bar" } -- { a: [1,2], b: "FooBar" }
ret2 = mempty ∷ { a ∷ Array Int, b ∷ String } -- { a: [], b: "" }
各フィールド毎に連結されます。
mempty
も見たままです。
どうやって実現しているのか?
すべて PureScript 0.11.6 で登場した RowToList
を使って実現しています。
ここでは class EQ
を例に説明します。
class EQ のインスタンス定義
レコードに対する class EQ
のインスタンス定義は下記のとおりです。
instance eqRec :: (RL.RowToList row list, EqRecord list row) => Eq (Record row) where
eq = eqRecord (RLProxy :: RLProxy list)
早速 RowToList
が出て来ました。
RowToList
については PureScript: RowToList に説明がある他、ここ Qiita でも Justin Woo さんがたくさん記事を書かれています。
ただしいずれも英語です。
この記事を書いている時点で日本語の記事は Justin Woo さんの記事を @oreshinya さんが翻訳した PureScriptで簡単にJSONをパースする - Qiita くらいしか見つかりませんでした。
しかしここで説明しきれる気がしないためとりあえず先に進みます。
レコード比較の本体は class EqRecord
とその関数 eqRecord
にあります。
class EqRecord と eqRecord
class EqRecord
と eqRecord
の定義はこのようになっています。
class EqRecord rowlist row where
eqRecord :: RLProxy rowlist -> Record row -> Record row -> Boolean
instance eqRowNil :: EqRecord RL.Nil row where
eqRecord _ _ _ = true
instance eqRowCons
:: ( EqRecord rowlistTail row
, Row.Cons key focus rowTail row
, IsSymbol key
, Eq focus
)
=> EqRecord (RL.Cons key focus rowlistTail) row where
eqRecord _ ra rb = (get ra == get rb) && tail
where
key = reflectSymbol (SProxy :: SProxy key)
get = unsafeGet key :: Record row -> focus
tail = eqRecord (RLProxy :: RLProxy rowlistTail) ra rb
eqRowNil
は中身が空のレコード、つまり {}
同士の比較にマッチします。
この場合関数 eqRecord
は常に true
を返します。
eqRowCons
の方は空ではないレコード同士の比較にマッチします。
tail
の定義にも eqRecord
が登場していることから判るようにレコードの内容を1つづつ再帰的に比較しています。
関数 eqRecord
をコールするたびに rowlistTail
の中のフィールドが1つ減り、空になると eqRowNil
の方にマッチして再帰呼び出しが終了する仕組みです6。
このように再帰的に処理出来るのは、実は RowToList
のおかげです。
row には「順序」がないので「先頭」と「残り」に分割できません。
RowToList
によって row を RowList
という「順序」7を持った型レベルのリストに変換することで、Cons
で「先頭」と「残り」のリストに分割して再帰的に処理できるようになります。
-
row の定訳が「行」なのか「列」なのかその他なのか判りませんでした。自分は普段そのまま row(ロウ) と呼んでいるので今回はとりあえずこのままにします。いずれ用語も整理したいところです。 ↩
-
一応ソース上にコードが書かれているのですが、ラベルのアルファベット順に比較する実装でよいのか疑問があるようで、コメントアウトされています。 ↩
-
Record instances of Show, Eq, Ord etc · Issue #154 によると整域の定義「任意の非零元の積は非零である」を満たさないからのようです。そしてユークリッド環(Euclidean ring)は整域の真部分集合なので整域でなければユークリッド環じゃない、ということらしいです。割り算出来ればいいじゃん、とはいかない厳しい世界のようです。 ↩
-
Prelude から再export されていないこともあり記載を省略しましたが
ff
やtt
、implies
も使えます。 ↩ -
Prelude から再export されていないこともあり記載を省略しましたが
power
やguard
も使えます。 ↩ -
フィールドが不一致ならそこですぐに比較を終了して欲しいところですが、実際にはすべてのフィールドが比較されるようです。
{a: 1, b: 2, c: 3} == {a: 2, b: 2, c: 3}
が1 == 2 && (2 == 2 && (3 == 3 && true)
に展開され、かつ右から評価されるイメージです。 ↩ -
RowList
ではラベルのアルファベット順になります。 ↩