수색…


소개

Lens 은 Haskell을위한 라이브러리로, 렌즈, 동형, 주름, 횡단, getter 및 setter를 제공하며 Java의 접근 자 및 변형 자 개념과 달리 임의의 구조를 쿼리하고 조작하기위한 균일 한 인터페이스를 제공합니다.

비고

렌즈 란 무엇입니까?

렌즈 (및 기타 광학) 우리가 우리가 수행 할 작업 일부 데이터에 액세스하는 방법을 설명 분리 할 수 있습니다. 추상적 인 렌즈 개념과 구체적인 구현을 구별하는 것이 중요합니다. 추상적으로 이해하면 lens 프로그래밍하는 것이 장기적으로 훨씬 쉬워집니다. 렌즈의 동형 상 표현이 많아서이 토론을 위해 구체적인 구현 토론을 피하고 대신 개념에 대한 높은 수준의 개요를 제공합니다.

초점

추상 이해의 중요한 개념은 집중 의 개념입니다. 중요한 광학은 큰 문맥을 잊지 않고 더 큰 데이터 구조의 특정 부분에 초점맞 춥니 다 . 예를 들어, 렌즈 _1 은 튜플의 첫 번째 요소에 초점을 맞추지 만 두 번째 필드에 있던 것을 잊지 않습니다.

초점을 맞추면 렌즈로 수행 할 수있는 작업에 대해 이야기 할 수 있습니다. 주어진 Lens sa 유형의 데이터 타입 부여 s 특정에 초점을 맞추고 a 우리는 하나, 수

  1. 추가적인 문맥을 잊어서 a 를 추출하거나
  2. 새로운 값을 제공하여 a 를 대체하십시오.

이는 렌즈를 특성화하기 위해 일반적으로 사용되는 잘 알려진 getset 작업에 해당합니다.

기타 광학 기기

우리는 비슷한 방식으로 다른 광학에 대해서 이야기 할 수 있습니다.

시력 초점을 ...
렌즈 제품의 한 부분
프리즘 합계의 한 부분
순회 데이터 구조의 0 개 이상의 부분
동형 성 ...

각각의 광학은 다른 방식으로 초점을 맞 춥니 다. 광학식의 종류에 따라 우리는 다른 작업을 수행 할 수 있습니다.

구성

게다가 복잡한 데이터 액세스를 지정하기 위해 지금까지 논의한 두 가지 요소 중 하나를 구성 할 수 있습니다. 우리가 논의한 네 가지 유형의 광학은 격자를 형성하고, 두 개의 광학을 함께 구성한 결과가 상한선입니다.

여기에 이미지 설명을 입력하십시오.

예를 들어 렌즈와 프리즘을 함께 구성하면 순회가 발생합니다. 그 이유는 그들의 (수직) 구성에 의해, 우리는 먼저 제품의 한 부분에 집중하고 그 다음에는 한 부분에 초점을 맞추기 때문입니다. 그 결과는 탐색의 특별한 경우 인 우리 데이터의 정확히 0 또는 1 부분에 초점을 맞추는 광경입니다. (이것은 종종 affine traversal이라고도 함).

하스켈에서

하스켈에서 인기를 얻은 이유는 광학에 대한 매우 간결한 표현이 있기 때문입니다. 모든 광학은 기능 구성을 사용하여 함께 구성 될 수있는 특정 형태의 기능 일뿐입니다. 이것은 매우 가벼운 임베디드로 연결되어 옵틱을 프로그램에 쉽게 통합 할 수 있습니다. 또한 인코딩의 세부 사항으로 인해 함수 합성은 자동으로 작성하는 두 개의 광학 요소의 상한을 자동으로 계산합니다. 즉, 명시 적 변환없이 동일한 광학 조합에 대해 동일한 결합자를 재사용 할 수 있습니다.

렌즈로 튜플 조작하기

방법

("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

HasName 이라는 유형 클래스, Person 렌즈 name 을 만들고 PersonHasName 의 인스턴스로 HasName . 이후의 레코드도 클래스에 추가됩니다.

data Entity = Entity { _entityName :: String }
makeFields ''Entity

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 인 경우 lensX 작업으로 전체 작업을 직접 수행 할 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

하스켈의 이점을 여전히 사용하면서도 고전적인 명령형 언어와 유사한 코드를 작성할 수 있습니다.

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을 demystify하기 위해,

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

어떤 투플 (tuple)은 항상 첫 번째 요소를 가지고 있기 때문에 더 명확히하기 위해 우리는 _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)s a 슬롯을 "설정"합니다 a
  • review :: Prism' sa -> (a -> s)as 수 있음을 "깨닫습니다"
  • preview :: Prism' sa -> (s -> Maybe a) 설정하는 "시도" s 에 . a

그것에 대해 생각하는 또 다른 방법은 Lens' sa 유형의 값이 s 가 일부 알 수없는 r 대해 (r, a) 와 동일한 구조를 갖고 있음을 보여줍니다. 다른 한편, Prism' sasEither ra 구조에 대해 r 과 동일한 구조를 가지고 있음을 보여줍니다. 우리는이 지식으로 위의 네 가지 함수를 작성할 수 있습니다.

-- `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 . gf 먼저 따라 가면서 f . g Lens' ac 그 다음 gg . 특히 :

  • 렌즈는 함수로 구성됩니다 (실제로 함수입니다)
  • Lensview 기능을 생각하면 데이터 흐름이 "왼쪽에서 오른쪽으로"흐른 것처럼 보입니다. 이것은 기능 구성에 대한 일반적인 직감으로 거슬러 올라갈 수 있습니다. 반면에, 당신이 생각한다면 그것은 자연스럽게 느껴 져야합니다 . OO 언어에서 일어나는 것과 같은 주석.

단지 구성보다 LensLens , (.) 거의 모든 "구성하는 데 사용할 수있는 Lens -like는"함께 입력합니다. 타입이 따라하기가 더 어려워지기 때문에 결과가 무엇인지를 항상 쉽게 알 수는 없지만 lens 차트 를 사용 하여 파악할 수 있습니다. 조성 x . y 는 해당 차트에서 xy 유형의 최소 상한 유형을가집니다.

품위있는 렌즈

표준 외에 makeLenses 생성 함수 Lens ES를 Control.Lens.TH 또한 제공 makeClassy 기능. makeClassy 는 동일한 유형을 가지고 있으며 하나의 주요 차이점을 makeLenses 하고는 makeLenses 와 본질적으로 동일한 방식으로 작동합니다. 표준 렌즈와 탐색을 생성하는 것 외에도 인수에 유형이없는 경우 유형을 필드로 갖는 모든 데이터 유형을 설명하는 클래스를 작성합니다. 예를 들어

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 한 번 고정 알려져들). 필드에서 데이터 유형의 이름 (소문자)을 제거하여 "용량"이라는 이름을 알아 냈습니다. 필드 이름이나 렌즈 이름에 밑줄을 사용하지 않아도되는 것은 즐겁습니다. 때로는 레코드 구문이 실제로 원하는 것일 수 있기 때문입니다. 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에 대한 인스턴스를 추가했습니다. 두 번째 클래스는 기존 클래스를 사용하여 Bar에 대한 인스턴스를 만들었습니다.

이것은 다른 모듈에서 HasCapcity 클래스를 가져 오는 경우에도 작동합니다. makeFields 는 기존 클래스에 인스턴스를 더 추가하고 여러 모듈에 유형을 분산시킬 수 있습니다. 그러나 클래스를 가져 오지 않은 다른 모듈에서 다시 사용하면 이름이 같은 새 클래스가 만들어지고 호환되지 않는 두 개의 별도의 오버로드 된 용량의 렌즈가 생깁니다.



Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow