37

実際の例では、レンズが何に使われているのか説明がつかないようです。ハッキングページのこの短い段落は、私が見つけたものに最も近いものです。

このモジュールは、構造の要素にアクセスして更新するための便利な方法を提供します。これはData.Accessorsと非常に似ていますが、もう少し一般的で、依存関係が少なくなっています。私は特に、州のモナドでネストされた構造をきれいに処理する方法が好きです。

それで、彼らは何のために使われますか?他の方法に比べてどのようなメリットとデメリットがありますか?なぜそれらが必要なのですか?

4

3 に答える 3

52

これらは、データの更新を完全に抽象化し、実際に「必要」になることはありません。彼らはあなたに別の方法で問題について推論させます。

Cのようないくつかの命令型/「オブジェクト指向」プログラミング言語では、値のコレクション(「構造体」と呼びましょう)とコレクション内の各値にラベルを付ける方法(ラベルは通常「フィールド」と呼ばれます)のよく知られた概念があります。 )。これにより、次のような定義になります。

typedef struct { /* defining a new struct type */
  float x; /* field */
  float y; /* field */
} Vec2;

typedef struct {
  Vec2 col1; /* nested structs */
  Vec2 col2;
} Mat2;

次に、次のように、この新しく定義されたタイプの値を作成できます。

Vec2 vec = { 2.0f, 3.0f };
/* Reading the components of vec */
float foo = vec.x;
/* Writing to the components of vec */
vec.y = foo;

Mat2 mat = { vec, vec };
/* Changing a nested field in the matrix */
mat.col2.x = 4.0f;

同様にHaskellには、次のデータ型があります。

data Vec2 =
  Vec2
  { vecX :: Float
  , vecY :: Float
  }

data Mat2 =
  Mat2
  { matCol1 :: Vec2
  , matCol2 :: Vec2
  }

このデータ型は、次のように使用されます。

let vec  = Vec2 2 3
    -- Reading the components of vec
    foo  = vecX vec
    -- Creating a new vector with some component changed.
    vec2 = vec { vecY = foo }

    mat = Mat2 vec2 vec2

ただし、Haskellでは、データ構造内のネストされたフィールドを変更する簡単な方法はありません。これは、Haskellの値は不変であるため、変更する値の周りにすべてのラッピングオブジェクトを再作成する必要があるためです。Haskellに上記のような行列があり、行列の右上のセルを変更したい場合は、次のように記述する必要があります。

    mat2 = mat { matCol2 = (matCol2 mat) { vecX = 4 } }

動作しますが、不器用に見えます。つまり、誰かが思いついたのは、基本的には次のとおりです。2つのものをグループ化すると、値の「ゲッター」(上記など)vecXmatCol2、ゲッターが属するデータ構造を指定して、その値が変更された新しいデータ構造では、多くの優れた処理を実行できます。例えば:

data Data = Data { member :: Int }

-- The "getter" of the member variable
getMember :: Data -> Int
getMember d = member d

-- The "setter" or more accurately "updater" of the member variable
setMember :: Data -> Int -> Data
setMember d m = d { member = m }

memberLens :: (Data -> Int, Data -> Int -> Data)
memberLens = (getMember, setMember)

レンズを実装する方法はたくさんあります。このテキストでは、レンズが上記のようなものであるとしましょう。

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

aつまり、タイプのフィールドを持つあるタイプのゲッターとセッターの組み合わせであるbため、memberLens上記は。になりますLens Data Int。これは私たちに何をさせますか?

まず、レンズからゲッターとセッターを抽出する2つの簡単な関数を作成しましょう。

getL :: Lens a b -> a -> b
getL (getter, setter) = getter

setL :: Lens a b -> a -> b -> a
setL (getter, setter) = setter

これで、ものの抽象化を開始できます。上記の状況をもう一度見てみましょう。「2階建て」の値を変更したいとします。別のレンズでデータ構造を追加します。

data Foo = Foo { subData :: Data }

subDataLens :: Lens Foo Data
subDataLens = (subData, \ f s -> f { subData = s }) -- short lens definition

次に、2つのレンズを構成する関数を追加しましょう。

(#) :: Lens a b -> Lens b c -> Lens a c
(#) (getter1, setter1) (getter2, setter2) =
    (getter2 . getter1, combinedSetter)
    where
      combinedSetter a x =
        let oldInner = getter1 a
            newInner = setter2 oldInner x
        in setter1 a newInner

コードはちょっと速く書かれていますが、それが何をするのかは明らかだと思います。ゲッターは単純に構成されています。内部データ値を取得してから、そのフィールドを読み取ります。セッターはa、新しい内部フィールド値である値を変更することになっている場合x、最初に古い内部データ構造を取得し、その内部フィールドを設定してから、新しい内部データ構造で外部データ構造を更新します。

それでは、レンズの値を単純にインクリメントする関数を作成しましょう。

increment :: Lens a Int -> a -> a
increment l a = setL l a (getL l a + 1)

このコードがあれば、それが何をするのかが明らかになります。

d = Data 3
print $ increment memberLens d -- Prints "Data 4", the inner field is updated.

これで、レンズを作成できるため、次のことも実行できます。

f = Foo (Data 5)
print $ increment (subDataLens#memberLens) f
-- Prints "Foo (Data 6)", the innermost field is updated.

すべてのレンズパッケージが行うことは、基本的に、このレンズの概念、つまり「セッター」と「ゲッター」をグループ化して、使いやすいようにすっきりとしたパッケージにまとめることです。特定のレンズの実装では、次のように書くことができます。

with (Foo (Data 5)) $ do
  subDataLens . memberLens $= 7

したがって、Cバージョンのコードに非常に近くなります。データ構造のツリーでネストされた値を変更するのは非常に簡単になります。

レンズはこれにすぎません。一部のデータの一部を簡単に変更する方法です。特定の概念について推論するのが非常に簡単になるため、さまざまな方法で相互作用する必要のある膨大なデータ構造のセットがある状況で広く使用されます。

レンズの長所と短所については、SOに関する最近の質問を参照してください。

于 2012-05-28T17:58:09.977 に答える
13

レンズは、データ構造を均一で構成的な方法で編集するための便利な方法を提供します。

多くのプログラムは、次の操作を中心に構築されています。

  • (ネストされている可能性のある)データ構造のコンポーネントの表示
  • (ネストされている可能性のある)データ構造のフィールドを更新する

レンズは、編集の一貫性を確保する方法で構造を表示および編集するための言語サポートを提供します。編集は簡単に作成できます。また、構造の一部を更新する場合と同じコードを使用して、構造の一部を表示できます。

したがって、レンズを使用すると、ビューから構造にプログラムを簡単に作成できます。構造からそれらの構造のビュー(およびエディター)に戻ります。彼らはレコードアクセサーとセッターの多くの混乱を一掃します。

ピアス他 Quotient Lensesの論文などで普及しているレンズや、Haskellの実装は、現在広く使用されています(fclabelsやデータアクセサーなど)。

具体的なユースケースについては、次のことを考慮してください。

  • ユーザーが構造化された方法で情報を編集しているグラフィカルユーザーインターフェイス
  • パーサーときれいなプリンター
  • コンパイラ
  • 更新データ構造の同期
  • データベースとスキーマ

そして、世界のデータ構造モデルと、そのデータの編集可能なビューがある他の多くの状況。

于 2012-05-28T17:39:29.387 に答える
6

補足として、レンズが「フィールドアクセスと更新」の非常に一般的な概念を実装していることは見過ごされがちです。レンズは、関数のようなオブジェクトを含む、あらゆる種類のものに書き込むことができます。これを理解するには少し抽象的な思考が必要なので、レンズの力の例を示しましょう。

at :: (Eq a) => a -> Lens (a -> b) b

を使用atすると、以前の引数に応じて、複数の引数を持つ関数に実際にアクセスして操作できます。Lensそれがカテゴリであることを覚えておいてください。これは、関数などをローカルで調整するための非常に便利なイディオムです。

プロパティまたは代替表現によってデータにアクセスすることもできます。

polar :: (Floating a, RealFloat a) => Lens (Complex a) (a, a)
mag   :: (RealFloat a) => Lens (Complex a) a

さらにレンズを作成して、フーリエ変換された信号の個々の帯域などにアクセスできます。

于 2012-05-28T20:13:26.020 に答える