サーチ…


前書き

Lensは、レンズ、同形、折りたたみ、トラバーサル、ゲッター、セッターを提供するHaskell用のライブラリであり、Javaのアクセサやミューテータのコンセプトとは異なり、任意の構造を照会および操作するための統一されたインターフェイスを提供します。

備考

レンズとは何ですか?

レンズ(およびその他の光学系は)私たちは私たちがそれをやりたいから一部のデータにアクセスする方法を説明し分離することができます。レンズの抽象概念と具体的な実装を区別することは重要です。抽象的に理解することは、長期的にはlensプログラミングをはるかに容易にする。レンズの同型表現は多くありますので、この議論では具体的な実装の議論を避け、その概念の概要を説明します。

フォーカシング

抽象的に理解する上での重要な概念は、 フォーカシングの概念です。重要な光学は、より大きな文脈を忘れることなく、より大きなデータ構造の特定の部分に焦点を合わせる 。たとえば、レンズ_1はタプルの最初の要素に着目しますが、2番目のフィールドにあったものを忘れません。

一度焦点を当てれば、レンズでどの操作を実行できるかについて話し合うことができます。与えられたLens saが与えられたとき、タイプsデータ型が特定のa焦点を当てると、

  1. 追加のコンテキストを忘れてa抽出するか、または
  2. 交換してください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

レンズxyが作成されます。

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

型クラスHasNamePersonレンズnameを作成し、 PersonHasNameインスタンスにし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である場合、 modifymodifyだけで全体の操作を直接実行することができます。

構造化状態の命令コード

この例の状態を仮定します。

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の任意内sPrism' 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)は、 as
  • preview :: Prism' sa -> (s -> Maybe a) "試み"をsaに変える。

それについて考えるための別の方法は、型の値ということである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 sg :: 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 . gf最初に、次にgたどって得たLens' acです。特に:

  • レンズは(実際には、彼らはただ機能している )の関数として構成します
  • Lensview機能について考えると、データが「左から右へ」流れているように見えます。これは、機能構成についてのあなたの通常の直感に逆らって感じるかもしれません。一方、あなたが考えるなら、それは自然であると感じるべきです. OO言語でどのように起こるかのような注釈。

Lens with Lens使用するだけではなく、 (.)を使って、ほぼすべての " Lensような"タイプを一緒に合成することができます。タイプが厳しくなるので結果が何であるかを見るのは必ずしも容易ではありませんがlensチャートを使って理解することができます。組成x . yは、そのチャートのxyの両方の型の最小の上限の型があります。

高級レンズ

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つの別々のオーバーロードされた容量のレンズが作成されます。



Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow