はじめに
空きが多かったので急遽書きました。アドベントカレンダー発参加です。そもそも Qiita への投稿が初めてです。とうかブログ書いたの数年ぶりです。ツッコミ、指摘歓迎です。
僕は元々 Haskell を齧っており、その後 PureScript を勉強しました。今はどちらも個人的に使っていますが、レコード回りでは断然 PureScript が使いやすいと思っています。PureScript のレコードで感動した所を少し書きたいと思います。
レコードがちゃんとした型!
Haskell にはレコード構文がありますが、レコード型はありません。実態はタプルで、フィールド名と同名のアクセス関数が定義されます。Haskell 触り初めた人が多く躓くのが、フィールド名の重複問題でしょう。OverloadedRecordFields
拡張、各レコードライブラリの登場、lens などでレコード回りの問題は解決されたとする意見もありますが、個人的には疑問です。
そのため PureScript のレコードを知ったときはコレ便利!ってなりました。PureScript ではちゃんとしたレコード型があります。data や newtype の型定義の中だけでなく、普通に関数の引数としてレコード型を取れます。
foo :: { x :: Int, y :: Int } -> Int
もちろん type でエイリアス付けたり、newtype/data の中で使うこともできます。ネストすることも可能です。
newtype Foo = Foo
{ foo :: { a :: Int, b :: String }
, bar :: String
}
レコード型毎にフィールドの名前空間を別に持つので、フィールド名の重複で悩む必要はありません。そもそもそれぐらい当然だろ、と思うかもしれませんが、Haskell -> PureScript の流れで学習した身としては感動しました。
ワイルドカード
レコードのアクセスはドット('.')記法を使います。レコードの生成にはhaskell に似たリテラル表記が用意されています。
foo :: { x :: Int, y :: Int }
foo = { x: 1, y: 4 } -- レコード生成のリテラル表記
foo.x :: Int -- ドット記法でのフィールドアクセス
ここで値を指定する箇所やレシーバに当る箇所に _ (アンダースコア)を使うとでレコード生成関数やレコードのアクセッサー関数が簡単に作れます。
{ x: _, y: 4 } :: forall a. a -> { x :: a, y :: Int }
_.foo :: forall r. { foo :: a | r } -> a
アクセッサーに関してはネストしたレコードに対するアクセッサー関数も簡単に作れます。
_.foo.bar :: forall a r1 r0. { foo :: { bar :: a | r1 } | r0 } -> a
便利!
ネストしたレコードの更新
PureScript のレコード更新も Haskell に習っています。
r { a = 1 } -- レコード r のフィールド a を 1 で更新
レコードがネストした場合でも更新がやりやすいよう便利な記法が追加されています。例えばあるレコードが a というフィールドを持っており、その a がフィールド型であり、その中の b フィールドを更新したい場合、以下のように書けます。
r { a { b = 4 } }
これも便利!
Row polymorphism、RowCons, Union, RowToList
ここら辺の話題を書き出すと日が変わってしまうため(というか良く分かっていないため)とりあえず雰囲気だけ。
レコード型 { x :: Int, y :: Int }
と書いていましたが、これは糖衣構文であり、実態は Recrod (x :: Int, y :: Int)
という型です。ここの (x :: Int, y :: Int) の箇所は row と呼ばれており、種 # Type を持っています。
row に対する制約を記述することができます。例えば x :: Int というフォールドを持つレコードなら何でも受けつけるような関数が書けます。
foo :: forall r. { x :: Int | r } -> Int
foo r = r.x + 1
foo { x: 1, y: 2 } -- OK
foo { x: 4, z: "hoge" } -- OK
foo { y: 1 } -- コンパイルエラー
他に RowCons
や Union
といった制約もあり、これを利用すると二つのレコードの型安全なマージなどが行なえるようになります。また最近登場したRowToList
制約により レコード型(というか # Type をパラメータに持つ型全般)に対して型クラスのインスタンスを定義できるようになりました。RowToList のおかげでレコードの Show
インスタンスを実装することが可能のなりました(多分 0.12 で入るはず)。また purescript-simple-json
のような RowToList を利用してレコード・JSONの相互変換を Generic を使わずにに実現するライブラリも出てきました。今後もますます便利なライブラリが出てくると思います。
タプル記法なくてもレコードがあれば何とかなる
PureScript では Haskell のタプル記法(e.g. (1,"foo")
)はありません。Tuple という型はありますが、生成するときは Tuple 1 2
のように書く必要があり、型の記述も面倒になっています。
(僕の記憶が正しければ) Haskell でタプルを使うところはレコードを使うべき、ということでタプル記法は導入されていません。適切な名前を付いているほうが良いという判断です。
例えば文字列を分割する splitAt
(Data.Stringモジュール)は以下のような型を持っています。
splitAt :: Int -> String -> Maybe { before :: String, after :: String }
Haskell なら結果の型は Maybe (String,String)
になっているはずです。この例ではあまり名前が付いているありがたみが分からないかもですが、例えば (Int,Int,String)
みたいな型に出会うと、よく言われる「型がドキュメント」は嘘だろーと思ってしまいます。
PureScript ではレコードが非常に扱いやすいものになっているので、Haskeller が PureScript 触るときはタプルは忘れてガンガンレコードを使うのがいいと思います。
ああ、けどやっぱりちょっとした所でタプルは欲しいですね。zip してうんたらするときとか。
まとめ
PureScript のレコードについて紹介でした。PureScript のレコードの便利さが少しでも伝わったら幸いです。あー誰か Haskell にも導入してくんないかなー。