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
가질 수 있으며 전달할 필요가 없습니다.stomp
Monkey
마찬가지로, 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
'developer tip' 카테고리의 다른 글
Android 애플리케이션 클래스 수명주기 (0) | 2020.12.26 |
---|---|
Android 앱을 MySQL 데이터베이스에 연결하는 방법은 무엇입니까? (0) | 2020.12.26 |
Android Java는 Java 8에서 람다 표현식을 지원합니까? (0) | 2020.12.26 |
변경 가능한 객체에 대해 GetHashCode를 재정의 하시겠습니까? (0) | 2020.12.26 |
실제 색상을 혼합하는 것과 같은 색상 혼합 알고리즘이 있습니까? (0) | 2020.12.26 |