これらは、データの更新を完全に抽象化し、実際に「必要」になることはありません。彼らはあなたに別の方法で問題について推論させます。
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つのものをグループ化すると、値の「ゲッター」(上記など)vecX
とmatCol2
、ゲッターが属するデータ構造を指定して、その値が変更された新しいデータ構造では、多くの優れた処理を実行できます。例えば:
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に関する最近の質問を参照してください。