新しいGHC拡張、NoFieldSelectorsについて

今まで不満の多かったHaskellのレコードの扱いを改善するための一歩として、NoFieldSelectorsというGHC拡張の実装を進めている。

動機

Haskellにはレコードを定義するための構文がある。

data User = User
    { userId :: Int
    , userName :: Text
    }

こう定義すると、各フィールドごとにuserId :: User -> IntuserName :: User -> Textというゲッターに相当する関数が生成される。これらの関数は特別な意味合いを持っており、以下のレコード操作の構文にも利用できる。

  • 構築 User { userId = 0, userName = "Zero" }
  • パターンマッチ case foo of User { userId = x, userName = name } -> ...
  • 更新 foo { userId = 1 }

しかし、フィールドと同じ名前の関数こそが使いづらさの原因となっている。 多くのプログラミング言語では、構造体のフィールドはそれぞれ固有の名前空間に属するが、Haskellはそうではない。以下のような定義は、idUserなのかArticleなのか、それともPreludeid :: a -> aなのか判別できないため、実際には使えない。

data User = User { id :: Int, name :: Text }
data Article = Article { id :: Int, title :: Text }

そのため、userIdarticleIdのように型名を接頭辞にするのが通例となっている。 するとタイプ数が増えるばかりか、JSONなどのフォーマットに変換する際に接頭辞を切り取るための仕組みなども必要になり、使い勝手がよくない。lensや拡張可能レコードなどの技法で改善できる面もあるものの、RecordWildCardsNamedFieldPunsなどの構文的な支援や、網羅性のチェックが受けられなくなるのは痛い。

DuplicateRecordFields拡張を用いれば、複数のデータ型で同じフィールド名を採用することも一応許される。しかし、ゲッター関数がどのデータ型に属するか判定するためのマジカルな実装があり、進んで使いたいものではないのが実情である(そのハックを無くす提案が最近受理された *1 )。

忘れがちだが、複数のコンストラクタを持つデータ型においてもレコードの構文は使える。その場合、対応するコンストラクタ以外にはエラーを出す部分関数が生成されるため大変使い勝手が悪く、実質的にないものとして扱われている。

提案

そんな問題を解決するアプローチとして、NoFieldSelectorsという拡張が提案された。

github.com

この拡張を有効にすると、レコードを定義しても、ゲッター関数としては使えなくなるが、レコードの構文としては使える――つまり、短い名前のデメリット(コンフリクト)をなくし、メリット(簡潔なコード)だけを得られるというわけだ。DuplicateRecordFieldsと併用すれば、複数のデータ型で同じフィールド名を気兼ねなく定義できる。

目の上のたんこぶだったゲッター関数の問題を回避できれば、他の言語と遜色ないレコード操作が可能になり、レコード自体の採用率も高まることが予想される。 プレーンなデータ型で起きがちな、変更に伴う破壊や、値の順番を間違えるバグを回避しやすくなり、Haskellの強みの一つであるジェネリクスを使った導出機構を活用できる場面も増える。

さらに、いずれ実装されるであろうRecordDotSyntax(プロポーザル, 日本語の紹介スライド)の実用性を飛躍的に高めるだろう。

要約すると、NoFieldSelectorsは以下のメリットをもたらす。

  • フィールド名に接頭辞を付けなくてよくなる
  • instance FromJSON Fooのように、そのままインスタンスを導出できる場面が増える
  • 今までは単なるバッドプラクティスだった、複数コンストラクタのレコードが実用的になる
  • 害を及ぼす心配なくレコードを導入できる

実装

提案者のSimon Hafner氏により、フラグの追加や試験的な実装が作られ、私が実際に機能する段階まであらかた完成させた。現在はレビュー段階にある。

Implement NoFieldSelectors (!4017) · Merge Requests · Glasgow Haskell Compiler / GHC · GitLab

単に関数の生成をやめればいいかと思いきや、そこまで単純な話ではなかったようだ。各種レコード操作をコンパイルするときの振る舞いは、ゲッター関数の存在を前提としている。それを省いてしまうと、単なるレコードでないデータ型と同じになってしまうのだ。フィールドに相当するシンボルは今まで通り作られるが、項としてコンパイルするときはそれを隠す、というアプローチをとった。この辺りは、DuplicateRecordFieldsなどの機能との兼ね合いで泥臭いものとなったが、Adam Gundry氏の助言のおかげで実装まで持っていくことができた。

このブランチはGHC 9.2までにはマージできるようにしたい。NoFieldSelectorsは、今まで避けようのなかった慣習を覆す機能であり、これが広まればGHC/Haskellという言語の全体像が変わるに違いない。まだ捕らぬ狸の皮算用でしかないが、GHCのレコードの進化を楽しみにしていただきたい。

2021/02/17 追記 Adam Gundry氏がGHCのレコード周りの内部仕様をブラッシュアップしたため、実装をリベースして再投稿していただいた。そしてついに16日、masterにマージされた。

gitlab.haskell.org

将来

PolyKindsやStrictDataと同様、NoFieldSelectorsはモジュール単位でしか振る舞いを制御できない。将来的には、データ型単位で挙動をコントロールしたり、その旨をドキュメントにも反映させるための仕組みが必要であると考えている。