Haskell Language
レンズ
サーチ…
前書き
Lensは、レンズ、同形、折りたたみ、トラバーサル、ゲッター、セッターを提供するHaskell用のライブラリであり、Javaのアクセサやミューテータのコンセプトとは異なり、任意の構造を照会および操作するための統一されたインターフェイスを提供します。
備考
レンズとは何ですか?
レンズ(およびその他の光学系は)私たちは私たちがそれをやりたいから一部のデータにアクセスする方法を説明し分離することができます。レンズの抽象概念と具体的な実装を区別することは重要です。抽象的に理解することは、長期的にはlens
プログラミングをはるかに容易にする。レンズの同型表現は多くありますので、この議論では具体的な実装の議論を避け、その概念の概要を説明します。
フォーカシング
抽象的に理解する上での重要な概念は、 フォーカシングの概念です。重要な光学は、より大きな文脈を忘れることなく、より大きなデータ構造の特定の部分に焦点を合わせる 。たとえば、レンズ_1
はタプルの最初の要素に着目しますが、2番目のフィールドにあったものを忘れません。
一度焦点を当てれば、レンズでどの操作を実行できるかについて話し合うことができます。与えられたLens sa
が与えられたとき、タイプs
データ型が特定のa
焦点を当てると、
- 追加のコンテキストを忘れて
a
抽出するか、または - 交換してください
a
新たな価値を提供することにより、
これらは、レンズを特徴づけるために通常使用set
れるよく知られたget
およびset
操作に対応しています。
その他の光学機器
同様の方法で他の光学系についても話すことができます。
視覚 | 注目に値する... |
---|---|
レンズ | 製品の一部 |
プリズム | 合計の1つの部分 |
トラバーサル | データ構造のゼロまたはそれ以上の部分 |
同型異性 | ... |
各光学系は、異なる光学系の種類に応じて異なる方法で焦点を合わせます。
組成
さらに、複雑なデータアクセスを指定するためにこれまで説明した2つのオプティクスを構成することができます。我々が議論した4つのタイプの光学系は格子を形成し、2つの光学系を一緒に構成した結果がその上限である。
たとえば、レンズとプリズムを一緒に構成すると、トラバーサルが得られます。その理由は、彼らの(垂直)構成によって、まず製品の1つの部分に焦点を当て、次にその部分の1つに焦点を当てるからです。結果は、トラバースの特別なケースである、データの正確にゼロまたは1つの部分に焦点を当てた光学である。 (これは、アフィントラバーサルと呼ばれることもあります)。
ハスケルで
ハスケルでの人気の理由は、光学の非常に簡潔な表現があるということです。すべての光学系は、機能構成を使用して一緒に構成できる特定の形式の単なる機能です。これにより、非常に軽量の埋め込みが行われ、オプティクスをプログラムに簡単に組み込むことができます。これに加えて、エンコードの詳細のために、関数合成は、自動的に作成する2つの光学系の上限を自動的に計算します。これは、明示的なキャストなしに、同じオプティクス用の同じコンビネータを再利用できることを意味します。
レンズでタプルを操作する
取得
("a", 1) ^. _1 -- returns "a"
("a", 1) ^. _2 -- returns 1
設定
("a", 1) & _1 .~ "b" -- returns ("b", 1)
変更
("a", 1) & _2 %~ (+1) -- returns ("a", 2)
both
トラバーサル
(1, 2) & both *~ 2 -- returns (2, 4)
記録用レンズ
シンプルレコード
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
data Point = Point {
_x :: Float,
_y :: Float
}
makeLenses ''Point
レンズx
とy
が作成されます。
let p = Point 5.0 6.0
p ^. x -- returns 5.0
set x 10 p -- returns Point { _x = 10.0, _y = 6.0 }
p & x +~ 1 -- returns Point { _x = 6.0, _y = 6.0 }
繰り返しのあるフィールド名でレコードを管理する
data Person = Person { _personName :: String }
makeFields ''Person
型クラスHasName
、 Person
レンズname
を作成し、 Person
をHasName
インスタンスにしHasName
。後続のレコードもクラスに追加されます。
data Entity = Entity { _entityName :: String }
makeFields ''Entity
makeFields
を動作さmakeFields
には、Template Haskell拡張が必要です。技術的には、他の手段、例えば手でこのように作られたレンズを作ることは完全に可能である。
ステートフルレンズ
レンズ演算子は、ステートフルなコンテキストで動作する便利なバリアントを持っています。それらは演算子名に~
を=
と置き換えることで得られます。
(+~) :: Num a => ASetter s t a a -> a -> s -> t
(+=) :: (MonadState s m, Num a) => ASetter' s a -> a -> m ()
注:ステートフルバリアントはタイプを変更するとは予想されないため、
Lens'
またはSimple Lens'
シグネチャを持っています。
退治&
チェーン
レンズの操作をチェーンする必要がある場合は、次のようになります。
change :: A -> A
change a = a & lensA %~ operationA
& lensB %~ operationB
& lensC %~ operationC
これは&
の結合性のおかげです。しかし、ステートフルバージョンはより明確です。
change a = flip execState a $ do
lensA %= operationA
lensB %= operationB
lensC %= operationC
lensX
が実際にid
である場合、 modify
をmodify
だけで全体の操作を直接実行することができます。
構造化状態の命令コード
この例の状態を仮定します。
data Point = Point { _x :: Float, _y :: Float }
data Entity = Entity { _position :: Point, _direction :: Float }
data World = World { _entities :: [Entity] }
makeLenses ''Point
makeLenses ''Entity
makeLenses ''World
Haskellの利点を利用できるようにしながら、古典的な命令言語に似たコードを書くことができます:
updateWorld :: MonadState World m => m ()
updateWorld = do
-- move the first entity
entities . ix 0 . position . x += 1
-- do some operation on all of them
entities . traversed . position %= \p -> p `pointAdd` ...
-- or only on a subset
entities . traversed . filtered (\e -> e ^. position.x > 100) %= ...
Template Haskellなしでレンズを書く
Template Haskellを解読するために、
data Example a = Example { _foo :: Int, _bar :: a }
次に
makeLenses 'Example
(多かれ少なかれ)
foo :: Lens' (Example a) Int bar :: Lens (Example a) (Example b) a b
しかし、特に魔法はありません。あなた自身で書くことができます:
foo :: Lens' (Example a) Int -- :: Functor f => (Int -> f Int) -> (Example a -> f (Example a)) ;; expand the alias foo wrap (Example foo bar) = fmap (\newFoo -> Example newFoo bar) (wrap foo) bar :: Lens (Example a) (Example b) a b -- :: Functor f => (a -> f b) -> (Example a -> f (Example b)) ;; expand the alias bar wrap (Example foo bar) = fmap (\newBar -> Example foo newBar) (wrap bar)
基本的には、 wrap
機能を使用してレンズの「フォーカス」を「訪問」し、「全体」タイプを再構築したいとします。
レンズとプリズム
Lens' sa
あなたが常に見つけることができることを意味しa
の任意内s
。 Prism' sa
あなたが、時にはそれが見つけることができることを意味しs
ただ実際には 、時にはそれが何か他のものだが。 a
より明確にするために、 _1 :: Lens' (a, b) a
があります。なぜなら、どのタプルも常に最初の要素を持つからです。我々は持っている_Just :: Prism' (Maybe a) a
、時にはので、 Maybe a
実際にあるに包まれた値a
Just
なく、 時にはそれはないですNothing
。
この直感で、いくつかの標準的なコンビネータは互いに平行に解釈することができます
-
view :: Lens' sa -> (s -> a)
は、s
うち "a
"を取得します。 -
set :: Lens' sa -> (a -> s -> s)
"セット"のスロットa
s
-
review :: Prism' sa -> (a -> s)
は、a
がs
-
preview :: Prism' sa -> (s -> Maybe a)
"試み"をs
をa
に変える。
それについて考えるための別の方法は、型の値ということであるLens' sa
ことを示しているs
同じ構造を有する(r, a)
いくつかの未知のためr
。一方、 Prism' sa
は、 s
がいくつかのr
Either ra
と同じ構造を持っていることを示しています。この知識を持って上記の4つの関数を書くことができます:
-- `Lens' s a` is no longer supplied, instead we just *know* that `s ~ (r, a)` view :: (r, a) -> a view (r, a) = a set :: a -> (r, a) -> (r, a) set a (r, _) = (r, a) -- `Prism' s a` is no longer supplied, instead we just *know* that `s ~ Either r a` review :: a -> Either r a review a = Right a preview :: Either r a -> Maybe a preview (Left _) = Nothing preview (Right a) = Just a
トラバーサル
Traversal' sa
あることを示しs
0対多数あり、その中に複数可。 a
toListOf :: Traversal' s a -> (s -> [a])
Traversable
あるすべてのタイプt
は、自動的にそのtraverse :: Traversal (ta) a
ます。
我々は使用することができますTraversal
設定したり、これらのすべての上にマッピングするために値をa
> set traverse 1 [1..10]
[1,1,1,1,1,1,1,1,1,1]
> over traverse (+1) [1..10]
[2,3,4,5,6,7,8,9,10,11]
f :: Lens' sa
正確に一つの存在だと言うの内側a
s
。 g :: Prism' ab
には、0または1 b
のいずれかが含まれa
ます。合成f . g
私たちに与えTraversal' sb
ために、以下のf
、その後、 g
0対1があるかそこに示しb
中のS s
。
レンズ構成
あなたはf :: Lens' ab
とa g :: Lens' bc
を持っていたら、 f . g
はf
最初に、次にg
たどって得たLens' ac
です。特に:
- レンズは(実際には、彼らはただ機能している )の関数として構成します
-
Lens
のview
機能について考えると、データが「左から右へ」流れているように見えます。これは、機能構成についてのあなたの通常の直感に逆らって感じるかもしれません。一方、あなたが考えるなら、それは自然であると感じるべきです.
OO言語でどのように起こるかのような注釈。
Lens
with Lens
使用するだけではなく、 (.)
を使って、ほぼすべての " Lens
ような"タイプを一緒に合成することができます。タイプが厳しくなるので結果が何であるかを見るのは必ずしも容易ではありませんが、 lens
チャートを使って理解することができます。組成x . y
は、そのチャートのx
とy
の両方の型の最小の上限の型があります。
高級レンズ
Control.Lens.TH
には、 Lens
を生成するための標準のmakeLenses
関数に加えて、 makeLenses
関数も用意されてmakeClassy
ます。 makeClassy
は同じタイプで、 makeClassy
と基本的に同じ方法でmakeLenses
、重要な違いが1つあります。標準レンズとトラバーサルを生成するだけでなく、引数に引数がない場合は、型をフィールドとして持つすべてのデータ型を記述するクラスも作成されます。例えば
data Foo = Foo { _fooX, _fooY :: Int }
makeClassy ''Foo
作成する
class HasFoo t where
foo :: Simple Lens t Foo
instance HasFoo Foo where foo = id
fooX, fooY :: HasFoo t => Simple Lens t Int
makeFieldsのフィールド
(この例はこのStackOverflowの回答からコピーされています )
同じ名前(この場合はcapacity
レンズをすべて持っている必要がある、さまざまな種類のデータがあるとします。 makeFields
スライスは、名前空間の競合なしにこれを達成するクラスを作成します。
{-# LANGUAGE FunctionalDependencies
, MultiParamTypeClasses
, TemplateHaskell
#-}
module Foo
where
import Control.Lens
data Foo
= Foo { fooCapacity :: Int }
deriving (Eq, Show)
$(makeFields ''Foo)
data Bar
= Bar { barCapacity :: Double }
deriving (Eq, Show)
$(makeFields ''Bar)
次にghciで:
*Foo
λ let f = Foo 3
| b = Bar 7
|
b :: Bar
f :: Foo
*Foo
λ fooCapacity f
3
it :: Int
*Foo
λ barCapacity b
7.0
it :: Double
*Foo
λ f ^. capacity
3
it :: Int
*Foo
λ b ^. capacity
7.0
it :: Double
λ :info HasCapacity
class HasCapacity s a | s -> a where
capacity :: Lens' s a
-- Defined at Foo.hs:14:3
instance HasCapacity Foo Int -- Defined at Foo.hs:14:3
instance HasCapacity Bar Double -- Defined at Foo.hs:19:3
それでは、それが実際に行われていますが、クラスの宣言されHasCapacity sa
容量があり、 Lens'
からs
への( a
a
一度固定されて知られているS)を。フィールドからデータ型の(小文字の)名前を取り除いて "容量"という名前を見つけました。フィールド名やレンズ名にアンダースコアを使用しないようにするのは楽しいことですが、時にはレコードの構文が実際にあなたが望むものであるためです。 makeFieldsWithとさまざまなlensRulesを使用して、レンズ名を計算するためのいくつかの異なるオプションを使用できます。
それが助けになる場合は、ghci -ddump-splicesを使う。Foo.hs:
[1 of 1] Compiling Foo ( Foo.hs, interpreted )
Foo.hs:14:3-18: Splicing declarations
makeFields ''Foo
======>
class HasCapacity s a | s -> a where
capacity :: Lens' s a
instance HasCapacity Foo Int where
{-# INLINE capacity #-}
capacity = iso (\ (Foo x_a7fG) -> x_a7fG) Foo
Foo.hs:19:3-18: Splicing declarations
makeFields ''Bar
======>
instance HasCapacity Bar Double where
{-# INLINE capacity #-}
capacity = iso (\ (Bar x_a7ne) -> x_a7ne) Bar
Ok, modules loaded: Foo.
最初のスプライスはHasCapcity
クラスをHasCapcity
、Fooのインスタンスを追加しました。 2番目のクラスは既存のクラスを使用し、Barのインスタンスを作成しました。
これは、別のモジュールからHasCapcity
クラスをインポートする場合にも機能します。 makeFields
は、既存のクラスにさらにインスタンスを追加して、複数のモジュールに型をmakeFields
ことができます。しかし、クラスをインポートしていない別のモジュールで再度使用すると、新しいクラス(同じ名前)が作成され、互換性のない2つの別々のオーバーロードされた容量のレンズが作成されます。