불변 구조 및 심층 구성 계층


9

GUI 응용 프로그램을 개발 중이며 그래픽을 많이 사용하고 있습니다. 예제를 위해이를 벡터 편집기로 생각할 수 있습니다. 모든 데이터 구조를 변경 불가능하게 만드는 것은 매우 유혹적입니다. 따라서 실행 취소 / 다시 실행, 복사 / 붙여 넣기 및 기타 많은 작업을 거의 노력없이 수행 할 수 있습니다.

간단하게하기 위해 다음 예제를 사용합니다. 응용 프로그램은 다각형 모양을 편집하는 데 사용되므로 불변 지점의 목록 인 "Polygon"개체가 있습니다.

Scene -> Polygon -> Point

그리고 내 프로그램에는 현재 Scene 객체를 보유하는 변수가 하나만 있습니다. 점 드래그를 구현하려고 할 때 시작되는 문제-가변 버전에서 Point객체 를 잡고 좌표를 수정하기 시작합니다. 불변 버전에서-나는 붙어 있습니다. 나는 Polygon현재 Scene의 색인을 끌어다 놓은 지점의 색인을 저장 Polygon하고 매번 교체 할 수있었습니다. 그러나이 접근법은 확장되지 않습니다-구성 수준이 5 이상으로 떨어지면 보일러 플레이트는 견딜 수 없게됩니다.

나는이 문제를 해결할 수 있다고 확신합니다. 결국 완전히 불변의 구조와 IO 모나드가있는 Haskell이 있습니다. 그러나 나는 단지 방법을 찾을 수 없습니다.

힌트 좀 주 시겠어요?


@Job-이것이 바로 작동하는 방식이며 많은 고통을 안겨줍니다. 그래서 나는 다른 접근법을 찾고 있습니다-그리고 불변성은 적어도 우리가 그것에 사용자 상호 작용을 추가하기 전에이 응용 프로그램 구조에 완벽하게 보입니다 :)
Rogach

@Rogach : 상용구 코드에 대해 더 자세히 설명해 주시겠습니까?
rwong

답변:


9

현재 장면에 다각형 인덱스를 저장하고 다각형에 드래그 한 지점의 인덱스를 저장하고 매번 교체 할 수있었습니다. 그러나이 접근법은 확장되지 않습니다-구성 수준이 5 이상으로 떨어지면 보일러 플레이트는 견딜 수 없게됩니다.

당신이 절대적으로 맞습니다.이 방법은 상용구를 돌 수 없다면 확장 할 수 없습니다 . 특히, 작은 하위 파트로 완전히 새로운 장면을 만들기위한 상용구가 변경되었습니다. 그러나 많은 기능적 언어는 이러한 종류의 중첩 구조 조작 (렌즈)을 처리하기위한 구성을 제공합니다.

렌즈는 기본적으로 불변 데이터에 대한 게터 및 세터입니다. 렌즈는 더 큰 구조물의 작은 부분에 중점둡니다 . 렌즈 감안할 때, 당신이 그것으로 할 수있는 두 가지가있다 - 당신은 할 수 있습니다 보고 더 큰 구조의 값의 작은 부분, 또는 당신은 할 수 설정 새로운 값으로 더 큰 구조의 값의 작은 부분. 예를 들어 목록의 세 번째 항목에 초점을 맞춘 렌즈가 있다고 가정합니다.

thirdItemLens :: Lens [a] a

이 유형은 더 큰 구조가 사물의 목록이고 작은 하위 부분이 그 중 하나임을 의미합니다. 이 렌즈를 사용하면 목록에서 세 번째 항목을보고 설정할 수 있습니다.

> view thirdItemLens [1, 2, 3, 4, 5]
3
> set thirdItemLens 100 [1, 2, 3, 4, 5]
[1, 2, 100, 4, 5]

렌즈가 유용한 이유는 렌즈가 게터와 세터를 나타내는 이므로 다른 값과 동일한 방식으로 렌즈 를 추상화 할 수 있기 때문 입니다. 렌즈를 반환하는 listItemLens기능 , 예를 들어 숫자를 가져와 목록의 항목을 n보는 렌즈를 반환하는 기능을 만들 수 있습니다 n. 또한 렌즈를 구성 할 수 있습니다 .

> firstLens = listItemLens 0
> thirdLens = listItemLens 2
> firstOfThirdLens = lensCompose firstLens thirdLens
> view firstOfThirdLens [[1, 2], [3, 4], [5, 6], [7, 8]]
5
> set firstOfThirdLens 100 [[1, 2], [3, 4], [5, 6], [7, 8]]
[[1, 2], [3, 4], [100, 6], [7, 8]]

각 렌즈는 한 레벨의 데이터 구조를 순회하기위한 동작을 캡슐화합니다. 이들을 결합하면 여러 수준의 복잡한 구조를 통과 할 수있는 상용구를 제거 할 수 있습니다. 예를 들어, 가정하면 당신은 가지고 scenePolygonLens i그 조회 수 iㄱ 장면에서 다각형, 그리고 번째 polygonPointLens n뷰 그 nth다각형의 포인트, 당신은 당신과 같이 전체 장면에서 관심 단지 특정 지점에 집중하는 렌즈 생성자를 만들 수 있습니다 :

scenePointLens i n = lensCompose (polygonPointLens n) (scenePolygonLens i)

이제 사용자가 다각형 14의 점 3을 클릭하고 오른쪽으로 10 픽셀 이동한다고 가정합니다. 다음과 같이 장면을 업데이트 할 수 있습니다.

lens = scenePointLens 14 3
point = view lens currentScene
newPoint = movePoint 10 0 point
newScene = set lens newPoint currentScene

여기에는 Scene 내부를 순회하고 업데이트하기위한 모든 상용구가 포함되어 lens있으므로 포인트를 변경하려는 것이 전부입니다. 당신은 이것을 추상적 촉진 할 수 lensTransform렌즈, 대상 및 수용 기능 기능 렌즈를 통해 대상의보기를 업데이트를 :

lensTransform lens transformFunc target =
  current = view lens target
  new = transformFunc current
  set lens new target

이 함수는 함수를 가져 와서 복잡한 데이터 구조에서 "업데이터"로 변환하여 뷰에만 함수를 적용하고 새 뷰를 구성하는 데 사용합니다. 따라서 14 번째 다각형의 3 번째 점을 오른쪽 10 픽셀로 이동하는 시나리오로 돌아가서 다음과 lensTransform같이 표현할 수 있습니다 .

lens = scenePointLens 14 3
moveRightTen point = movePoint 10 0 point
newScene = lensTransform lens moveRightTen currentScene

그리고 전체 장면을 업데이트하는 데 필요한 전부입니다. 이것은 매우 강력한 아이디어이며 관심있는 데이터를 보는 렌즈를 구성하는 데 유용한 기능이있을 때 매우 효과적입니다.

그러나 이것은 함수형 프로그래밍 커뮤니티에서도 현재 모든 것들이 있습니다. 렌즈 작업을위한 훌륭한 라이브러리 지원을 찾기가 어렵고 렌즈 작동 방식과 동료에게 어떤 이점이 있는지 설명하기가 훨씬 어렵습니다. 소금 한 조각으로이 방법을 사용하십시오.


훌륭한 설명! 이제 렌즈가 무엇인지 알 수 있습니다!
Vincent Lecrubier

13

나는 정확히 같은 문제에 대해 작업했습니다 (그러나 3 가지 구성 수준에서만). 기본 아이디어는 복제 한 다음 수정하는 것 입니다. 불변의 프로그래밍 스타일에서는 복제와 수정이 함께 이루어져 명령 객체가 됩니다.

변경 가능한 프로그래밍 스타일에서는 복제가 필요했을 것입니다.

  • 실행 취소 / 다시 실행을 허용하려면
  • 디스플레이 시스템은 사용자가 변경 사항을 볼 수 있도록 "편집 전"및 "편집 중"모델을 겹쳐서 (고스트 라인으로) 표시해야합니다.

변경 가능한 프로그래밍 스타일에서

  • 기존 구조는 심층 복제
  • 복제 된 사본에서 변경 사항이 작성됩니다.
  • 디스플레이 엔진은 기존 구조를 고스트 라인으로 렌더링하고 복제 / 수정 된 구조를 컬러로 렌더링하라는 지시를받습니다.

불변의 프로그래밍 스타일에서

  • 데이터 수정을 초래하는 각 사용자 조치는 일련의 "명령"에 맵핑됩니다.
  • 명령 개체는 적용 할 수정 내용과 원래 구조에 대한 참조를 정확하게 캡슐화합니다.
    • 필자의 경우 명령 객체는 변경 해야하는 포인트 인덱스와 새 좌표 만 기억합니다. (나는 불변의 스타일을 엄격하게 따르지 않기 때문에 매우 가볍습니다.)
  • 명령 객체가 실행될 때 수정 된 구조의 딥 카피가 생성되어 수정 사항이 새 사본에서 영구적으로 유지됩니다.
  • 사용자가 더 많은 편집을할수록 더 많은 명령 객체가 생성됩니다.

1
왜 불변 데이터 구조의 깊은 사본을 만드나요? 수정 된 객체에서 루트로 참조의 "스파인"을 복사하고 원래 구조의 나머지 부분에 대한 참조를 유지하면됩니다.
Reinstate Monica

3

깊이가 불변 인 객체는 무언가를 딥 클로닝하려면 단순히 참조를 복사하면된다는 이점이 있습니다. 깊이 중첩 된 객체를 약간만 변경해도 중첩 된 모든 객체의 새 인스턴스를 구성해야한다는 단점이 있습니다. 가변 객체는 객체를 변경하는 것이 쉽다는 장점이 있지만 단지 그렇게해야합니다. 그러나 객체를 딥 복제하려면 중첩 된 모든 객체의 딥 클론이 포함 된 새 객체를 구성해야합니다. 더 나쁜 것은, 객체를 복제하고 변경을 원한다면, 그 객체를 복제하고, 또 다른 변경을하는 등의 변경이 얼마나 크든 작든 상관없이, 저장된 모든 버전에 대해 전체 계층 구조의 사본 을 유지해야 합니다. 객체의 상태. 추잡한.

고려할 가치가있는 접근 방식은 변경 가능하고 깊이가 불변 인 파생 유형으로 추상 "maybeMutable"유형을 정의하는 것입니다. 이러한 모든 유형에는 AsImmutable방법이 있습니다. 매우 불변의 객체 인스턴스에서 해당 메소드를 호출하면 해당 인스턴스가 반환됩니다. 변경 가능한 인스턴스에서 호출하면 속성이 원래의 해당 항목에 대한 변경 불가능한 스냅 샷 인 변경 불가능한 인스턴스를 반환합니다. 변경 가능한 등가를 갖는 변경 불가능한 유형은 AsMutable메소드를 사용하여 해당 특성이 원래의 특성과 일치하는 변경 가능한 인스턴스를 구성합니다.

깊은 불변의 객체에서 중첩 된 객체를 변경하려면 먼저 외부 불변의 객체를 변경 가능한 객체로 교체 한 다음 변경 할 대상이 포함 된 속성을 변경 가능한 객체로 바꾸는 등의 작업을 수행해야하지만 전체 객체는 AsImmutable가변 객체 를 호출하려고 시도 할 때까지 추가 객체를 만들 필요가 없습니다 (이는 가변 객체를 변경 가능하게하지만 동일한 데이터를 보유하는 불변 객체를 반환합니다).

간단하지만 중요한 최적화로서, 각 가변 객체는 관련 불변 유형의 객체에 대한 캐시 된 참조를 보유 할 수 있으며, 각 불변 유형은 그 GetHashCode값을 캐시해야합니다 . AsImmutable변경 가능한 객체를 호출 할 때 새로운 불변 ​​객체를 반환하기 전에 캐시 된 참조와 일치하는지 확인하십시오. 그렇다면, 캐시 된 참조를 리턴하십시오 (새로운 불변 ​​오브젝트를 중단). 그렇지 않으면 캐시 된 참조를 업데이트하여 새 오브젝트를 보유하고이를 리턴하십시오. 이 작업이 완료되면AsImmutable개입하지 않으면 동일한 객체 참조가 생성됩니다. 새 인스턴스를 생성하는 비용을 절약하지 않아도 인스턴스를 유지하는 데 드는 메모리 비용을 피할 수 있습니다. 또한, 대부분의 경우 비교되는 항목이 참조-동일하거나 다른 해시 코드를 갖는 경우, 불변 객체 간의 동등 비교는 신속하게 진행될 수있다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.