「レンズ」と「部分レンズ」は、名前と概念がかなり似ているように見えます。それらはどのように異なりますか?どのような状況でどちらかを使用する必要がありますか?
Scala と Haskell にタグ付けしますが、レンズ ライブラリを持つ関数型言語に関連する説明を歓迎します。
部分レンズ (Haskell の命名法によると、今後はlensプリズムと呼びます (ただし、そうではないことを除いて! Ørjan のコメントを参照してください)) を説明するために、レンズ自体を別の角度から見ることから始めたいと思います。
lensLens s aは、与えられたtypesのサブコンポーネントに「焦点を合わせ」、それを表示し、置換し、(レンズ ファミリーのバリエーションを使用する場合は) その型を変更することさえできることを示します。saLens s t a b
これを調べる 1 つの方法は、未知のtypeのと のタプル type のLens s a間の同型性、同等性を証明することです。s(r, a)r
Lens s a ====== exists r . s ~ (r, a)
これにより、必要なものが得られます。これは、a取り出して置換し、同値を逆方向に実行して、s更新されていない新しいものを取得できるためaです。
それでは、代数データ型を使用して、高校の代数を更新してみましょう。ADT の 2 つの重要な演算は、乗算と加算です。anと a の両方a * bを持つ項目で構成される型がある場合は型と書き、 or のいずれかである項目で構成される型がある場合は型と書きます。aba + b ab
Haskellでは、タプル型をa * basと書きます。、どちらかの型(a, b)と書きます。a + bEither a b
積はデータのバンドルを表し、合計はオプションのバンドルを表します。製品は多くのものを持ち、そのうちの 1 つだけを (一度に) 選択したいという考えを表すことができますが、合計は失敗の考えを表します。もう1つ(右側に沿って)で解決します。
最後に、和と積はカテゴリ双対です。ほとんどのPLがそうであるように、それらは互いに適合し、一方が他方なしであると、あなたは厄介な場所に置かれます.
では、上記のレンズ定式化 (の一部)を二元化するとどうなるかを見てみましょう。
exists r . s ~ (r + a)
これは、型またはその他の宣言sです。オプション (および失敗) の概念をその核心に深く具現化するようなものがあります。a rlens
これはまさにプリズム(または部分的なレンズ)です
Prism s a ====== exists r . s ~ (r + a)
exists r . s ~ Either r a
では、いくつかの単純な例に関して、これはどのように機能するのでしょうか?
さて、リストを「unconses」するプリズムを考えてみましょう:
uncons :: Prism [a] (a, [a])
これと同等です
head :: exists r . [a] ~ (r + (a, [a]))
ここで何が必要になるかは比較的明白rです: 空のリストがあるため、完全な失敗です!
型を実証するには、anを aに変換し、 aをan に変換して互いに反転させるa ~ b方法を記述する必要があります。神話関数を介して私たちのプリズムを説明するためにそれを書きましょうabba
prism :: (s ~ exists r . Either r a) -> Prism s a
uncons = prism (iso fwd bck) where
fwd [] = Left () -- failure!
fwd (a:as) = Right (a, as)
bck (Left ()) = []
bck (Right (a, as)) = a:as
これは、この等価性を (少なくとも原則として) 使用してプリズムを作成する方法を示しており、リストなどの合計のような型を扱うときはいつでもプリズムが本当に自然に感じられるべきであることを示唆しています。
レンズは、より大きな値で一般化された「フィールド」を抽出および/または更新できるようにする「機能参照」です。通常の非部分的なレンズの場合、そのフィールドは、含まれる型の任意の値に対して常に存在する必要があります。これは、常にそこにあるとは限らない「フィールド」のようなものを見たい場合に問題を引き起こします。たとえば、「リストの n 番目の要素」(@ChrisMartin が貼り付けた Scalaz ドキュメントにリストされている) の場合、リストが短すぎる可能性があります。
このように、「部分レンズ」はレンズを一般化して、フィールドが常により大きな値で存在する場合と存在しない場合があります。
lensHaskellライブラリには、「部分レンズ」と見なすことができるものが少なくとも 3 つありますが、どれも Scala バージョンと正確に対応していません。
それらにはすべて用途がありますが、最初の 2 つは制限が多すぎてすべてのケースを含めることができませんが、Traversals は「一般的すぎます」。3 つのうち、Traversal「リストの n 番目の要素」の例をサポートするのは のみです。
「ラップされた値Lensを与えるMaybe」バージョンの場合、壊れるのはレンズの法則です。適切なレンズをNothing使用するには、オプションのフィールドを削除するように設定してから、元の値に戻してから取得する必要があります。同じ値を返します。これは、Map発言には問題なく機能します (そして、のようなコンテナーControl.Lens.At.atにそのようなレンズを提供します) が、たとえばth 要素を削除すると後の要素を妨害することを避けられないMapリストでは機能しません。0
APrismは、ある意味では、フィールドではなく、コンストラクター(Scala ではほぼケース クラス) の一般化です。そのため、存在する場合に提供される「フィールド」には、構造全体を再生成するためのすべての情報が含まれている必要があります (これはreview関数で実行できます)。
ATraversalは「リストの n 番目の要素」を問題なく実行できます。実際、少なくとも 2 つの異なる関数がixありelement、どちらもこれに対して機能します (ただし、他のコンテナーとの一般化は少し異なります)。
の型クラス マジックのおかげで、lensanyPrismまたははLens自動的に として機能します。TraversalLensMaybeTraversaltraverse
ただし、aは単一のフィールドに限定されないため、Traversalある意味では一般的すぎTraversalます。Aは、任意の数の「ターゲット」フィールドを持つことができます。例えば
elements odd
はTraversal、リストのすべての奇数インデックス要素を喜んで通過し、それらすべてから情報を更新および/または抽出します。
理論的には、Scala のバージョンにより近いと思われる 4 番目のバリアント (「アフィン トラバーサル」@J.Abrahamson が言及) を定義できますが、lensライブラリ自体の外部にある技術的な理由により、残りのバリアントにはうまく適合しません。ライブラリの一部のTraversal操作を使用するには、そのような「部分レンズ」を明示的に変換する必要があります。
また、最初にトラバースした要素だけを抽出Traversalする単純な演算子などがあるため、通常の s よりも多くの費用がかかることはありません。(^?)
(私が見る限り、技術的な理由はPointed、「アフィントラバーサル」を定義するために必要な型クラスがApplicative、通常Traversalの s が使用する のスーパークラスではないということです。)
以下は、Scalaz のLensFamilyとの scaladocPLensFamilyで、差分に重点が置かれています。
レンズ:
タイプからタイプへ同時に遷移するレコード内のへ遷移するフィールドにアクセスして取得するための純粋に機能的な手段を提供するレンズ ファミリ。when、および。
B1B2A1A2scalaz.LensA1 =:= A2B1 =:= B2「フィールド」という用語は、クラスのメンバーを意味するように限定的に解釈されるべきではありません。たとえば、レンズ ファミリは のメンバーシップに対応できます
Set。
部分レンズ:
部分的なレンズ ファミリは、タイプからタイプへ同時に遷移するレコード内のへ遷移するオプション フィールドにアクセスして取得するための純粋に機能的な手段を提供します。when、および。
B1B2A1A2scalaz.PLensA1 =:= A2B1 =:= B2「フィールド」という用語は、クラスのメンバーを意味するように限定的に解釈されるべきではありません。たとえば、部分的なレンズ ファミリは、 a の n 番目の要素をアドレス指定できます
List。
scalaz に慣れていない人のために、シンボリック型エイリアスを指摘する必要があります。
type @>[A, B] = Lens[A, B]
type @?>[A, B] = PLens[A, B]
B中置記法では、 typeのレコードからtype のフィールドを取得するレンズの型を 、部分的なレンズを としてA表すことを意味します。A @> BA @?> B
Argonaut (JSON ライブラリ) は、JSON のスキーマレスな性質により、任意の JSON 値から何かを取得しようとすると、常に失敗する可能性があることを意味するため、部分レンズの多くの例を提供します。Argonaut のレンズ構築関数の例をいくつか示します。
def jArrayPL: Json @?> JsonArray— JSON 値が配列の場合にのみ値を取得しますdef jStringPL: Json @?> JsonString— JSON 値が文字列の場合にのみ値を取得しますdef jsonObjectPL(f: JsonField): JsonObject @?> Json— JSON オブジェクトにフィールドがある場合にのみ値を取得します。fdef jsonArrayPL(n: Int): JsonArray @?> Json— JSON 配列のインデックスに要素がある場合にのみ値を取得しますn