Haskell Language
렌즈
수색…
소개
Lens 은 Haskell을위한 라이브러리로, 렌즈, 동형, 주름, 횡단, getter 및 setter를 제공하며 Java의 접근 자 및 변형 자 개념과 달리 임의의 구조를 쿼리하고 조작하기위한 균일 한 인터페이스를 제공합니다.
비고
렌즈 란 무엇입니까?
렌즈 (및 기타 광학) 우리가 우리가 수행 할 작업 일부 데이터에 액세스하는 방법을 설명 분리 할 수 있습니다. 추상적 인 렌즈 개념과 구체적인 구현을 구별하는 것이 중요합니다. 추상적으로 이해하면 lens
프로그래밍하는 것이 장기적으로 훨씬 쉬워집니다. 렌즈의 동형 상 표현이 많아서이 토론을 위해 구체적인 구현 토론을 피하고 대신 개념에 대한 높은 수준의 개요를 제공합니다.
초점
추상 이해의 중요한 개념은 집중 의 개념입니다. 중요한 광학은 큰 문맥을 잊지 않고 더 큰 데이터 구조의 특정 부분에 초점 을 맞 춥니 다 . 예를 들어, 렌즈 _1
은 튜플의 첫 번째 요소에 초점을 맞추지 만 두 번째 필드에 있던 것을 잊지 않습니다.
초점을 맞추면 렌즈로 수행 할 수있는 작업에 대해 이야기 할 수 있습니다. 주어진 Lens sa
유형의 데이터 타입 부여 s
특정에 초점을 맞추고 a
우리는 하나, 수
- 추가적인 문맥을 잊어서
a
를 추출하거나 - 새로운 값을 제공하여
a
를 대체하십시오.
이는 렌즈를 특성화하기 위해 일반적으로 사용되는 잘 알려진 get
및 set
작업에 해당합니다.
기타 광학 기기
우리는 비슷한 방식으로 다른 광학에 대해서 이야기 할 수 있습니다.
시력 | 초점을 ... |
---|---|
렌즈 | 제품의 한 부분 |
프리즘 | 합계의 한 부분 |
순회 | 데이터 구조의 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
렌즈 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
가 작동하려면 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)
는a
가s
수 있음을 "깨닫습니다" -
preview :: Prism' sa -> (s -> Maybe a)
설정하는 "시도"s
에 .a
그것에 대해 생각하는 또 다른 방법은 Lens' sa
유형의 값이 s
가 일부 알 수없는 r
대해 (r, a)
와 동일한 구조를 갖고 있음을 보여줍니다. 다른 한편, Prism' sa
은 s
가 Either 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 . g
는 f
먼저 따라 가면서 f . g
Lens' ac
그 다음 g
를 g
. 특히 :
- 렌즈는 함수로 구성됩니다 (실제로 는 함수입니다)
-
Lens
의view
기능을 생각하면 데이터 흐름이 "왼쪽에서 오른쪽으로"흐른 것처럼 보입니다. 이것은 기능 구성에 대한 일반적인 직감으로 거슬러 올라갈 수 있습니다. 반면에, 당신이 생각한다면 그것은 자연스럽게 느껴 져야합니다.
OO 언어에서 일어나는 것과 같은 주석.
단지 구성보다 Lens
와 Lens
, (.)
거의 모든 "구성하는 데 사용할 수있는 Lens
-like는"함께 입력합니다. 타입이 따라하기가 더 어려워지기 때문에 결과가 무엇인지를 항상 쉽게 알 수는 없지만 lens
차트 를 사용 하여 파악할 수 있습니다. 조성 x . y
는 해당 차트에서 x
와 y
유형의 최소 상한 유형을가집니다.
품위있는 렌즈
표준 외에 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
는 기존 클래스에 인스턴스를 더 추가하고 여러 모듈에 유형을 분산시킬 수 있습니다. 그러나 클래스를 가져 오지 않은 다른 모듈에서 다시 사용하면 이름이 같은 새 클래스가 만들어지고 호환되지 않는 두 개의 별도의 오버로드 된 용량의 렌즈가 생깁니다.