1. ホーム
  2. data-structures

[解決済み] lenses, fclabels, data-accessor - 構造体アクセスと突然変異のためのどのライブラリが良いか

2022-04-21 18:45:29

質問

レコードのフィールドにアクセスして操作するための一般的なライブラリは、少なくとも3つあります。私が知っているのは、data-accessor、fclabels、lensesです。

個人的にはdata-accessorから始めて、今はそれらを使っています。しかし、最近haskell-cafeでfclabelsの方が優れているという意見がありました。

したがって、私はこれら3つの(そしておそらくもっと多くの)ライブラリの比較に興味があります。

解決方法は?

私が知っている限りでは、少なくとも4つのライブラリがレンズを提供しています。

レンズの概念は、以下のものと同型のものを提供することです。

data Lens a b = Lens (a -> b) (b -> a -> a)

ゲッターとセッターの2つの関数を提供します。

get (Lens g _) = g
put (Lens _ s) = s

3つの法則に従う

第一に、何かを置いたら、それを元に戻すことができること

get l (put l b a) = b 

次に、取得してから設定しても答えが変わらないこと

put l (get l a) a = a

そして3つ目は、2回置くと1回置くのと同じというか、2回置いた方が勝ちということです。

put l b1 (put l b2 a) = put l b1 a

型システムはこれらの法則をチェックするのに十分ではないので、どのようなレンズの実装を使用する場合でも、自分でこれらの法則を確認する必要があることに注意してください。

これらのライブラリの多くは、追加のコンビネータや、単純なレコード型のフィールドのレンズを自動的に生成するテンプレート・ハスケルの機械も提供しています。

それを踏まえた上で、さまざまな実装に目を向けてみましょう。

実装例

fclabels

fclabels は、おそらくレンズライブラリの中で最も簡単に推論できるものです。 a :-> b は、上記の型に直接変換することができる。これは カテゴリ のインスタンスです。 (:->) を使用すると、レンズの合成ができるため便利です。また、無法地帯の Point 型は、ここで使われているレンズの概念を一般化したもので、同型を扱うためのいくつかのプラグインがあります。

を採用する上で障害となるのが fclabels は、メインパッケージに template-haskell 配管が含まれているため、パッケージが Haskell 98 ではないこと、また、(かなり議論の余地のない)Haskell 98 のための TypeOperators を拡張した。

データアクセサー

[編集 data-accessor は、この表現を使わず、以下のような形に移行しています。 data-lens . この解説は残しておきますが] 。

データアクセサー よりもややポピュラーです。 fclabels というのは Haskell 98です。しかし、その内部表現の選択は、私を少しばかり吐かせます。

型は T は、レンズを表現するために使用され、内部では次のように定義されています。

newtype T r a = Cons { decons :: a -> r -> (a, r) }

その結果 get を使用する場合、'a' 引数に未定義の値を指定する必要があります。これは、信じられないほど醜い、アドホックな実装だと思います。

とはいえ、Henning は、アクセッサを自動的に生成するための template-haskell プログラミングを、別の ' Zend Framework' に含めています。 データアクセサーテンプレート パッケージです。

これは、Haskell 98であること、すでに採用しているパッケージの数がかなり多いこと、そして、重要な Category そのため、ソーセージがどのように作られるかに注意を払わないのであれば、このパッケージは実に合理的な選択と言えるでしょう。

レンズ

次に レンズ このパッケージは、レンズを直接定義することによって、レンズが2つの状態モナド間の状態モナド同型性を提供できることを観察しています。 として このようなモナドの同型性

もし、レンズの型をわざわざ用意するのであれば、ランク2のような型になるでしょう。

newtype Lens s t = Lens (forall a. State t a -> State s a)

Haskell 98から不必要に引き離され(抽象的なレンズの型が必要な場合など)、また Category のインスタンスを作成し、それを使ってレンズを合成することができます。 . . また、この実装では、マルチパラメータの型クラスが必要です。

ここで言及した他のすべてのレンズライブラリは、何らかのコンビネータを提供するか、この同じ状態の焦点効果を提供するために使用することができますので、この方法で直接レンズをエンコードしても何も得るものはありません。

さらに、冒頭で述べたサイドコンディションは、このような形でうまく表現できるものではありません。fclabels' と同様に、これはメインパッケージで直接レコードタイプのレンズを自動生成するためのテンプレートハスケルメソッドを提供します。

がないため Category インスタンス、バロック様式のエンコーディング、そしてメインパッケージのtemplate-haskellの要件など、私の最も好きな実装です。

データレンズ

[編集: 1.8.0 で、これらは comonad-transformers パッケージから data-lens に移動しました] 。

私の data-lens パッケージが提供するレンズは 店舗 コモナドを使用します。

newtype Lens a b = Lens (a -> Store b a)

ここで

data Store b a = Store (b -> a) b

これを拡張すると、次のようになります。

newtype Lens a b = Lens (a -> (b, b -> a))

これは、ゲッターとセッターの共通引数を因数分解して、要素を取得した結果と、新しい値を戻すセッターの組を返すと見ることができます。これは、この「セッター」が値を取得するために使用した作業の一部を再利用できるという計算上の利点を提供し、より効率的な「変更」操作のために fclabels の定義は、特にアクセサが連鎖している場合に有効です。

また、この表現には素晴らしい理論的正当性があります。なぜなら、この回答の冒頭で述べた3つの法則を満たす「レンズ」の値のサブセットは、まさにラップされた関数がストアコモナドの「コモナド代数」であるレンズであるためです。これは、レンズのための3つの毛色を変換します。 l を、2つのポイントフリーな等価物に置き換えることができます。

extract . l = id
duplicate . l = fmap l . l

この手法は、ラッセル・オコナーの『日本経済新聞』に初めて言及され、記述された。 FunctorLens として ApplicativeBiplate : マルチプレートの紹介 であり プレプリントをもとにブログで紹介 ジェレミー・ギボンズ氏によるものです。

また、レンズを厳密に扱うための多くのコンビネータや、コンテナ用の純正レンズも含まれており、例えば、以下のようなものがあります。 Data.Map .

でのレンズは data-lens を形成する。 Category (とは異なり lenses パッケージとは異なります)、Haskell 98です。 fclabels / lenses のバックエンドとは異なり)、正気である。 data-accessor ) で、少し効率的な実装を提供します。 data-lens-fd は、MonadStateを扱うための機能をHaskell 98の外に出てもいいように提供し、テンプレート-ハスケル機構は data-lens-template .

2012年6月28日更新。その他のレンズ実装戦略

同型レンズ

他にも、検討すべきレンズエンコーディングが2つあります。1つ目は、レンズを、構造をフィールドの値と「それ以外」に分割する方法として見るための、素晴らしい理論的方法を提供します。

同型の型がある場合

data Iso a b = Iso { hither :: a -> b, yon :: b -> a }

を満たすような、有効なメンバが hither . yon = id であり、かつ yon . hither = id

でレンズを表現することができる。

data Lens a b = forall c. Lens (Iso a (b,c))

これらは主にレンズの意味について考える方法として有用であり、他のレンズを説明するための推論ツールとして使うことができるのです。

ファン・ラーホーフェンレンズ

で構成できるようにレンズをモデル化することができます。 (.)id がない場合でも Category のインスタンスを使用することで

type Lens a b = forall f. Functor f => (b -> f b) -> a -> f a

をレンズの型として使用します。

そうすると、レンズの定義は次のように簡単です。

_2 f (a,b) = (,) a <$> f b

で、関数合成がレンズ合成であることを自分で検証することができます。

最近、さらに van Laarhoven レンズを一般化する を一般化することで、フィールドの種類を変更できるレンズファミリーを得ることができます。

type LensFamily a b c d = forall f. Functor f => (c -> f d) -> a -> f b

このため、レンズについて語るにはランク2のポリモーフィズムを使うのが最適ですが、レンズを定義する際にそのシグネチャを直接使う必要はないという残念な結果になっています。

Lens 上で定義した _2 は、実際には LensFamily .

_2 :: Functor f => (a -> f b) -> (c,a) -> f (c, b)

私は、レンズ、レンズ・ファミリー、およびゲッター、セッター、フォールド、トラバーサルを含むその他の一般化を含むライブラリを書きました。これは、hackageで lens パッケージで提供されます。

このアプローチの大きな利点は、ライブラリメンテナが、レンズライブラリへの依存を一切発生させることなく、ライブラリ内にこのスタイルのレンズを実際に作成することができるということです。 Functor f => (b -> f b) -> a -> f a その特定の型'a'と'b'に対してです。これによって、導入コストを大幅に削減することができます。

新しいレンズを定義するために実際にパッケージを使用する必要がないので、Haskell 98のライブラリを維持することに対する私の以前の懸念から多くのプレッシャーを取り除くことができます。