developer tip

Haskell에서 복잡한 상태 유지

optionbox 2020. 12. 26. 09:58
반응형

Haskell에서 복잡한 상태 유지


Haskell에서 상당히 큰 시뮬레이션을 만들고 있다고 가정 해 보겠습니다. 시뮬레이션이 진행됨에 따라 속성이 업데이트되는 다양한 유형의 엔티티가 있습니다. 예를 들어 엔티티를 원숭이, 코끼리, 곰 등이라고 가정 해 보겠습니다.

이러한 엔티티의 상태를 유지하기 위해 선호하는 방법은 무엇입니까?

내가 생각한 첫 번째이자 가장 명백한 접근 방식은 다음과 같습니다.

mainLoop :: [Monkey] -> [Elephant] -> [Bear] -> String
mainLoop monkeys elephants bears =
  let monkeys'   = updateMonkeys   monkeys
      elephants' = updateElephants elephants
      bears'     = updateBears     bears
  in
    if shouldExit monkeys elephants bears then "Done" else
      mainLoop monkeys' elephants' bears'

mainLoop함수 시그니처 에 명시 적으로 언급 된 각 유형의 엔티티가 이미 추악 합니다. 예를 들어 20 가지 유형의 개체가 있다면 얼마나 끔찍해 질지 상상할 수 있습니다. (20은 복잡한 시뮬레이션에 적합하지 않습니다.) 그래서 이것은 용납 할 수없는 접근이라고 생각합니다. 그러나 그것의 구원의 은혜는 updateMonkeys그들이하는 일에서 다음 과 같은 기능 이 매우 명백 하다는 것입니다.

따라서 다음 생각은 모든 상태를 보유하는 하나의 빅 데이터 구조로 모든 것을 롤링하여 다음의 서명을 정리하는 것입니다 mainLoop.

mainLoop :: GameState -> String
mainLoop gs0 =
  let gs1 = updateMonkeys   gs0
      gs2 = updateElephants gs1
      gs3 = updateBears     gs2
  in
    if shouldExit gs0 then "Done" else
      mainLoop gs3

어떤 사람들은 우리 GameState가 State Monad에서 마무리 updateMonkeys하고 do. 괜찮아. 일부는 함수 구성으로 정리할 것을 제안합니다. 또한 괜찮다고 생각합니다. (BTW, 나는 Haskell의 초보자이므로 아마도 이것 중 일부에 대해 틀렸을 것입니다.)

그러나 문제는 같은 함수 updateMonkeys가 형식 서명에서 유용한 정보를 제공하지 않는다는 것입니다. 당신은 그들이 무엇을하는지 정말로 확신 할 수 없습니다. 물론 updateMonkeys설명적인 이름이지만 위안이 거의 없습니다. 신 객체를 전달하고 "내 글로벌 상태를 업데이트하십시오"라고 말하면 명령형 세계로 돌아온 것 같은 느낌이 듭니다. 다른 이름의 전역 변수처럼 느껴집니다 . 전역 상태에 무언가수행하는 함수가 있고 이를 호출하면 최선을 다할 수 있습니다. (명령형 프로그램에서 전역 변수와 함께 나타날 수있는 동시성 문제를 여전히 피할 수 있다고 가정합니다.하지만 전역 변수에서 동시성은 거의 유일한 문제가 아닙니다.)

또 다른 문제는 이것입니다. 객체가 상호 작용해야한다고 가정합니다. 예를 들어 다음과 같은 함수가 있습니다.

stomp :: Elephant -> Monkey -> (Elephant, Monkey)
stomp elephant monkey =
  (elongateEvilGrin elephant, decrementHealth monkey)

updateElephants여기에서 코끼리가 원숭이의 밟고있는 범위에 있는지 확인하기 때문에 이것이 호출 된다고 가정합니다 . 이 시나리오에서 어떻게 변경 사항을 원숭이와 코끼리 모두에게 우아하게 전파합니까? 두 번째 예제에서는 updateElephants신 객체를 취하고 반환하므로 두 변경 사항에 모두 영향을 미칠 수 있습니다. 그러나 이것은 단지 물을 더 엉망으로 만들고 내 요점을 강화합니다. 신 개체를 사용하면 효과적으로 전역 변수를 변경하는 것입니다. 그리고 신 개체를 사용하지 않는 경우 이러한 유형의 변경 사항을 전파하는 방법을 잘 모르겠습니다.

무엇을해야합니까? 확실히 많은 프로그램이 복잡한 상태를 관리해야하므로이 문제에 대해 잘 알려진 접근 방식이 있다고 생각합니다.

비교를 위해 OOP 세계의 문제를 해결하는 방법은 다음과 같습니다. 이 것 Monkey, Elephant등 오브젝트. 나는 모든 살아있는 동물 세트에서 조회를 수행하는 클래스 메소드를 가지고있을 것입니다. 위치, ID 등으로 조회 할 수 있습니다. 조회 기능의 기반이되는 데이터 구조 덕분에 힙에 할당 된 상태로 유지됩니다. (나는 GC 또는 참조 계산을 가정하고 있습니다.) 멤버 변수는 항상 변이됩니다. 어떤 클래스의 모든 메소드는 다른 클래스의 살아있는 동물을 돌연변이시킬 수 있습니다. 예를 들어 전달 된 객체 의 상태를 감소시키는 메서드를 Elephant가질 수 있으며 전달할 필요가 없습니다.stompMonkey

마찬가지로, Erlang 또는 기타 액터 지향 설계에서 이러한 문제를 상당히 우아하게 해결할 수 있습니다. 각 액터는 자체 루프를 유지하므로 자체 상태를 유지하므로 신 객체가 필요하지 않습니다. 그리고 메시지 전달을 사용하면 한 개체의 활동이 호출 스택을 백업하는 모든 방법을 전달하지 않고도 다른 개체의 변경을 트리거 할 수 있습니다. 하지만 하스켈의 배우들이 눈살을 찌푸린다는 말을 들었습니다.


대답은 FRP ( Functional Reactive Programming )입니다. 구성 요소 상태 관리와 시간에 따른 값이라는 두 가지 코딩 스타일의 하이브리드입니다. FRP는 실제로 디자인 패턴의 전체 제품군이기 때문에 좀 더 구체적으로 말하고 싶습니다 . Netwire를 추천 합니다 .

기본 아이디어는 매우 간단합니다. 각각 고유 한 로컬 상태를 사용하여 여러 개의 작고 독립적 인 구성 요소를 작성합니다. 이러한 구성 요소를 쿼리 할 때마다 다른 응답을 얻고 로컬 상태 업데이트를 유발할 수 있기 때문에 이는 시간 종속 값과 실질적으로 동일합니다. 그런 다음 이러한 구성 요소를 결합하여 실제 프로그램을 구성합니다.

이것은 복잡하고 비효율적으로 들리지만 실제로는 일반 함수를 둘러싼 매우 얇은 층일뿐입니다. Netwire에서 구현 한 디자인 패턴은 AFRP (Arrowized Functional Reactive Programming)에서 영감을 받았습니다. 자체 이름 (WFRP?)을 가질만큼 충분히 다를 수 있습니다. 튜토리얼 을 읽을 수 있습니다 .

어쨌든 작은 데모가 이어집니다. 빌딩 블록은 전선입니다.

myWire :: WireP A B

이것을 구성 요소로 생각하십시오. 예를 들어 시뮬레이터의 입자와 같이 A 유형 시변 값에 따라 달라지는 유형 B의 시변 값입니다 .

particle :: WireP [Particle] Particle

입자 목록 (예 : 현재 존재하는 모든 입자)에 따라 다르며 그 자체가 입자입니다. 미리 정의 된 와이어 (간단한 유형 사용)를 사용하겠습니다.

time :: WireP a Time

이것은 시간 (= Double ) 유형의 시간에 따라 변하는 값입니다 . 글쎄, 그것은 시간 자체입니다 (유선 네트워크가 시작될 때마다 계산되는 0에서 시작). 시간에 따라 다른 값에 의존하지 않기 때문에 원하는대로 공급할 수 있으므로 다형성 입력 유형이됩니다. 상수 와이어 (시간이 지나도 변하지 않는 시간에 따라 변하는 값)도 있습니다.

pure 15 :: Wire a Integer

-- or even:
15 :: Wire a Integer

두 개의 와이어를 연결하려면 범주 구성을 사용하면됩니다.

integral_ 3 . 15

This gives you a clock at 15x real time speed (the integral of 15 over time) starting at 3 (the integration constant). Thanks to various class instances wires are very handy to combine. You can use your regular operators as well as applicative style or arrow style. Want a clock that starts at 10 and is twice the real time speed?

10 + 2*time

Want a particle that starts and (0, 0) with (0, 0) velocity and accelerates with (2, 1) per second per second?

integral_ (0, 0) . integral_ (0, 0) . pure (2, 1)

Want to display statistics while the user presses the spacebar?

stats . keyDown Spacebar <|> "stats currently disabled"

This is just a small fraction of what Netwire can do for you.


I know this is old topic. But I am facing the same problem right now while trying to implement Rail Fence cipher exercise from exercism.io. It is quite disappointing to see such a common problem having such poor attention in Haskell. I don't take it that to do some as simple as maintaining state I need to learn FRP. So, I continued googling and found solution looking more straightforward - State monad: https://en.wikibooks.org/wiki/Haskell/Understanding_monads/State

ReferenceURL : https://stackoverflow.com/questions/15467925/maintaining-complex-state-in-haskell

반응형