함수형 프로그래밍은 동일한 객체가 여러 장소에서 참조되는 상황을 어떻게 처리합니까?


10

나는 사람들이 (이 사이트에서도) 일상적으로 기능적 프로그래밍 패러다임을 칭찬하면서 모든 것이 불변의 것을 갖는 것이 얼마나 좋은지를 강조하고 있습니다. 특히 사람들은 C #, Java 또는 C ++와 같은 전통적인 명령형 OO 언어에서도 Haskell과 같은 순수한 기능 언어뿐만 아니라 프로그래머 에게이 기능을 강요하는 방식 으로이 접근법을 제안합니다.

나는 변이성과 부작용을 발견하기 때문에 이해하기 어렵다는 것을 알았습니다 ... 편리합니다. 그러나 사람들이 현재 부작용을 비난하고 가능한 한 어디에서나 부작용을 제거하는 것이 좋은 방법이라고 생각할 때 유능한 프로그래머가 되려면 패러다임에 대한 이해를 돕기 위해 장을 시작해야한다고 생각합니다 ... 따라서 내 Q.

기능적 패러다임에서 문제를 발견 할 때 한 장소는 여러 장소에서 객체를 자연스럽게 참조하는 것입니다. 두 가지 예를 들어 설명하겠습니다.

첫 번째 예는 여가 시간에 만들려고하는 C # 게임 입니다. 턴 기반 웹 게임으로 두 플레이어 모두 4 명의 몬스터로 구성된 팀을 보유하고 있으며 팀에서 몬스터를 전장으로 보낼 수 있습니다.이 몬스터는 상대 플레이어가 보낸 몬스터를 향하게됩니다. 플레이어는 또한 전장에서 몬스터를 리콜하고 팀의 다른 몬스터로 대체 할 수 있습니다 (포켓몬과 유사).

이 설정에서, 하나의 몬스터는 최소한 두 곳 (플레이어 팀과 전장)에서 자연스럽게 참조 할 수 있습니다.

이제 한 몬스터가 맞고 체력이 20 감소하는 상황을 생각해 봅시다. 명령형 패러다임의 괄호 안에서 나는이 괴물의 health필드를 수정 하여이 변화를 반영합니다. 이것은 내가 지금하고있는 일입니다. 그러나 이것은 Monster클래스를 변경 가능 하게 만들고 관련 함수 (방법)를 불순 하게 만듭니다 . 현재로서는 나쁜 습관으로 간주됩니다.

미래의 어떤 시점에서 실제로 게임을 끝내기를 희망하기 위해이 게임의 코드를 이상적이지 않은 상태로 유지할 수있는 권한을 부여했지만 어떻게해야하는지 알고 이해하고 싶습니다. 제대로 작성되었습니다. 따라서 : 이것이 디자인 결함이라면 어떻게 고쳐야합니까?

기능적 스타일에서, 나는 그것을 이해하면서, 대신이 Monster객체 의 사본을 만들어서이 하나의 필드를 제외하고는 이전의 것과 동일하게 유지합니다. 메소드 suffer_hit는 이전 오브젝트를 수정하는 대신이 새 오브젝트를 리턴합니다. 그런 다음 Battlefield이 몬스터를 제외한 모든 필드를 동일하게 유지하면서 객체를 복사합니다 .

이것은 적어도 두 가지 어려움이 있습니다.

  1. 계층은 just- Battlefield> 의 단순화 된 예보다 훨씬 더 깊을 수 있습니다 Monster. 나는 하나를 제외한 모든 필드를 복사 하고이 계층 구조까지 새로운 객체를 반환해야합니다. 이것은 기능 프로그래밍이 상용구를 줄이려고하기 때문에 특히 성가신 것으로 보이는 상용구 코드입니다.
  2. 그러나 훨씬 더 심각한 문제는 데이터가 동기화되지 않는다는 것 입니다. 이 필드의 활성 몬스터는 체력이 감소 된 것을 볼 수 있습니다. 그러나 제어 플레이어에서 언급 한 동일한 몬스터 Team는 그렇지 않습니다. 내가 대신 필수적 스타일을 수용하면, 모든 데이터 수정 코드의 나는 정말 편리하게 찾을 이와 같은 이러한 경우 다른 모든 장소에서 즉시 볼 것 - 하지만 난 것들을 받고 있어요 방법이있다 정확하게 사람들이 무슨 말을 명령형 스타일로 잘못되었습니다!
    • 이제 Team각 공격 후로 이동하여이 문제를 처리 할 수 ​​있습니다 . 이것은 추가 작업입니다. 그러나 몬스터가 나중에 더 많은 곳에서 갑자기 참조 될 수 있다면 어떨까요? 예를 들어, 몬스터가 반드시 필드에 있지 않은 다른 몬스터에 집중할 수 있는 능력이 있다면 어떨까요? 공격 할 때마다 집중된 몬스터로 즉시 여행해야한다는 것을 반드시 기억해야합니까? 이것은 코드가 더 복잡 해짐에 따라 폭발 할 시한 폭탄 인 것 같습니다. 그래서 나는 그것이 해결책이 아니라고 생각합니다.

더 나은 솔루션에 대한 아이디어는 동일한 문제에 부딪쳤을 때 두 번째 예에서 비롯됩니다. 학계에서는 Haskell에서 자체 디자인 언어의 통역사를 작성하라는 지시를 받았습니다. (이 또한 FP가 무엇인지 이해하기 시작한 방법이기도합니다). 클로저를 구현할 때 문제가 나타났습니다. 다시 한 번 같은 범위는 지금 여러 곳에서 참조 할 수있다 :이 범위를 보유하고있는 변수를 통해 중첩 된 범위의 상위 범위로! 분명히이 범위를 가리키는 참조를 통해이 범위를 변경 한 경우이 변경은 다른 모든 참조를 통해서도 볼 수 있어야합니다.

내가 제공 한 해결책은 각 범위에 ID를 할당하고 State모나드 에있는 모든 범위의 중앙 사전을 보유하는 것이 었습니다 . 이제 변수는 범위 자체가 아닌 바인딩 된 범위의 ID 만 보유하고 중첩 된 범위는 상위 범위의 ID도 보유합니다.

내 몬스터 싸움 게임에서 같은 접근법을 시도 할 수있을 것 같다. 필드와 팀은 몬스터를 참조하지 않는다. 대신 중앙 몬스터 사전에 저장된 몬스터의 ID를 보유합니다.

그러나이 문제에 대한 해결책으로 주저없이 그것을 받아들이지 못하게하는이 접근법의 문제를 다시 한 번 볼 수 있습니다.

다시 한 번 상용구 코드 소스입니다. 1 개의 라이너를 반드시 3 개의 라이너로 만듭니다 : 이전에 단일 필드를 한 줄로 수정 한 것은 이제 (a) 중앙 사전에서 객체 검색 (b) 변경 (c) 새 객체 저장 중앙 사전에. 또한 참조 대신 객체 ID와 중앙 사전을 보유하면 복잡성이 증가합니다. FP는 복잡성과 상용구 코드 를 줄이기 위해 광고되기 때문에 이것이 잘못하고 있음을 암시합니다.

또한 훨씬 더 심각한 것처럼 보이는 두 번째 문제에 대해 작성하려고했습니다.이 방법은 메모리 누수를 유발 합니다. 도달 할 수없는 객체는 일반적으로 가비지 수집됩니다. 그러나 도달 가능한 오브젝트가이 특정 ID를 참조하지 않더라도 중앙 사전에 보유 된 오브젝트는 가비지 콜렉션 될 수 없습니다. 이론적으로 신중한 프로그래밍은 메모리 누수를 피할 수 있지만 (더 이상 필요하지 않은 경우 각 사전을 중앙 사전에서 수동으로 제거하도록주의를 기울일 수 있음) 오류가 발생하기 쉬우 며 FP는 다시 한 번 프로그램의 정확성을 높이기 위해 광고됩니다. 올바른 방법이 아닙니다.

그러나 나는 시간이 지남에 오히려 그것이 해결 된 문제인 것처럼 보였다. Java는 WeakHashMap이 문제를 해결하는 데 사용할 수있는 기능 을 제공 합니다. C #은 비슷한 기능을 제공합니다.- ConditionalWeakTable문서에 따르면 컴파일러에서 사용하도록되어 있습니다. 그리고 Haskell에는 System.Mem.Weak이 있습니다.

이러한 사전을 저장하면이 문제에 대한 올바른 기능적 솔루션입니까, 아니면 내가 볼 수없는 간단한 것이 있습니까? 그러한 사전의 수가 쉽게 늘어나고 나빠질 수 있다고 생각합니다. 따라서 이러한 사전이 변경 불가능한 경우 사전이 모나드로 유지되므로 모나드 계산을 지원하는 언어에서 많은 매개 변수 전달 또는 모나드 계산을 의미 할 수 있습니다. 이 사전 솔루션은 거의 모든 코드를 State모나드 내부에 넣습니다 . 이것이 올바른 솔루션인지 다시 한 번 의심하게 만듭니다.)

몇 가지 고려를 한 후에 나는 하나 이상의 질문을 추가 할 것이라고 생각합니다. 그러한 사전을 구성하여 무엇을 얻고 있습니까? 많은 전문가들에 따르면 명령형 프로그래밍의 문제점은 일부 객체의 변경 사항이 다른 코드 조각으로 전파된다는 것입니다. 이 문제를 해결하기 위해 객체는 불변이어야합니다. 정확히 이해하면 객체의 변경 사항을 다른 곳에서 볼 수 없어야합니다. 그러나 이제는 오래된 데이터에서 작동하는 다른 코드 조각에 대해 걱정하고 있으므로 중앙 사전을 발명하여 일부 코드 조각의 변경 사항이 다시 한 번 다른 코드 조각으로 전파됩니다! 그러므로 우리는 모든 단점을 가지고 명령형 스타일로 돌아 가지 않습니까?


6
이러한 관점을 제공하기 위해, 기능 불변 프로그램은 대부분 동시성이 관련된 데이터 처리 상황을위한 것 입니다. 즉, 일련의 방정식 또는 출력 결과를 생성하는 프로세스를 통해 입력 데이터를 처리하는 프로그램입니다. 불변성은 여러 가지 이유로이 시나리오에서 도움이됩니다. 여러 스레드에서 읽은 값이 수명 동안 변경되지 않도록 보장되어 잠금없는 방식으로 데이터를 처리하는 기능과 알고리즘 작동 방식에 대한 이유가 크게 단순화됩니다.
Robert Harvey

8
기능 불변성과 게임 프로그래밍에 대한 작은 비밀은이 두 가지가 서로 호환되지 않는다는 것입니다. 본질적으로 움직일 수없는 정적 데이터 구조를 사용하여 끊임없이 변화하는 역동적 인 시스템을 모델링하려고합니다.
Robert Harvey

2
종교적 교리로서 변이성 대 불변성을 취하지 마십시오. 각각이 다른 것보다 낫고, 불변성이 항상 더 좋은 것은 아닙니다. 예를 들어 불변의 데이터 타입으로 GUI 툴킷을 작성하는 것은 절대 악몽입니다.
whatsisname

1
이 C # 관련 질문과 답변 은 상용구 문제를 다루며, 대부분 기존 불변 개체의 약간 수정 된 (업데이트 된) 복제본을 생성해야하기 때문에 발생합니다.
rwong

2
중요한 통찰력은이 게임의 몬스터가 실체로 간주된다는 것입니다. 또한 각 전투의 결과 (전투 순서 번호, 몬스터의 개체 ID, 전투 전후의 몬스터 상태)는 특정 시점 (또는 시간 단계)의 상태로 간주됩니다. 따라서 플레이어 ( Team)는 전투 결과와 몬스터 상태를 (전투 번호, 몬스터 엔티티 ID) 튜플로 검색 할 수 있습니다.
rwong

답변:


19

기능적 프로그래밍은 여러 장소에서 참조 된 객체를 어떻게 처리합니까? 모델을 다시 방문하도록 초대합니다!

설명하려면 ... 게임 상태의 중앙 "골든 소스"사본과 해당 상태를 업데이트 한 수신 클라이언트 이벤트 세트를 사용하여 네트워크 게임을 작성하는 방법을 살펴보고 다른 클라이언트에게 다시 브로드 캐스트하십시오. .

Factorio 팀이 일부 상황에서이 기능을 제대로 수행하는 데 따른 재미 에 대해 읽을 수 있습니다 . 다음은 모델에 대한 간략한 개요입니다.

멀티 플레이어가 작동하는 기본 방법은 모든 클라이언트가 게임 상태를 시뮬레이션하고 플레이어 입력 (입력 액션) 만 받고 전송하는 것입니다. 서버의 주요 책임은 입력 조치를 프록시하고 모든 클라이언트가 동일한 틱으로 동일한 조치를 실행하도록하는 것입니다.

액션이 실행될 때 서버는 중재를해야하기 때문에 플레이어 액션은 플레이어 액션-> 게임 클라이언트-> 네트워크-> 서버-> 네트워크-> 게임 클라이언트와 같이 움직입니다. 이것은 모든 플레이어 액션이 네트워크를 통해 왕복 여행을 한 후에 만 ​​실행됨을 의미합니다. 이로 인해 게임이 정말 게으르다 고 느끼게되는데, 이것이 바로 멀티 플레이어가 등장한 이후로 레이턴시 숨기기가 게임에 추가 된 메커니즘 인 이유입니다. 지연 시간 숨기기는 다른 플레이어의 동작을 고려하지 않고 서버의 차익 거래를 고려하지 않고 플레이어 입력을 시뮬레이션하여 작동합니다.

Factorio에서 우리는 게임 상태를 가지고 있습니다. 이것은 맵, 플레이어, entitites, 모든 상태입니다. 서버에서 수신 된 조치를 기반으로 모든 클라이언트에서 결정적으로 시뮬레이션됩니다. 이것은 성스럽고 서버 또는 다른 클라이언트와 다른 경우 비동기가 발생합니다.

게임 상태 위에 지연 시간 상태가 있습니다. 여기에는 주 상태의 작은 하위 집합이 포함됩니다. 레이턴시 상태는 신성하지 않으며 플레이어가 수행 한 입력 동작을 기반으로 향후 게임 상태가 어떻게 보일지 생각합니다.

중요한 것은 타임 라인의 특정 눈금에서 각 객체의 상태를 변경할 수 없다는 것입니다 . 글로벌 멀티 플레이어 상태의 모든 것은 궁극적으로 결정 론적 현실로 수렴해야합니다.

그리고-이것이 귀하의 질문의 열쇠 일 수 있습니다. 각 항목의 상태는 지정된 틱에 대해 변경할 수 없으며 시간이 지남에 따라 새 인스턴스를 생성하는 전환 이벤트를 추적합니다.

생각하면 서버에서 들어오는 이벤트 큐는 이벤트를 적용 할 수 있도록 엔티티의 중앙 디렉토리에 액세스해야합니다.

결국, 복잡한 원-라인 뮤 테이터 방법은 실제로 시간을 정확하게 모델링하지 않기 때문에 간단합니다. 결국 처리 루프 중간에서 상태 가 변경 될 수있는 경우이 틱의 이전 엔터티에는 이전 값이 표시되고 나중에는 변경된 값이 표시됩니다. 이것을 신중하게 관리한다는 것은 최소한 의 틱 타임 라인에서 두 개의 틱에 불과한 최소 차별화 전류 (불변) 상태와 다음 (구성 중) 상태를 의미합니다!

광범위한 가이드로서, 몬스터의 상태를 위치 / 속도 / 물리, 건강 / 손상, 자산과 관련된 다수의 작은 물체로 나누는 것을 고려하십시오. 발생할 수있는 각 돌연변이를 설명하는 이벤트를 구성하고 기본 루프를 다음과 같이 실행하십시오.

  1. 입력을 처리하고 해당 이벤트를 생성
  2. 내부 이벤트 생성 (예 : 객체 충돌 등)
  3. 현재 불변 몬스터에 이벤트를 적용하여 다음 틱을위한 새로운 몬스터를 생성합니다. 가능한 경우 변경되지 않은 오래된 상태를 대부분 복사하지만 필요한 경우 새로운 상태 객체를 생성합니다.
  4. 다음 진드기를 위해 렌더링하고 반복하십시오.

아니면 그런 것. "어떻게 배포 할까?"라는 생각이 들었습니다. 일반적으로 사물이 어디에 살고 어떻게 진화해야할지 혼란 스러울 때 이해력을 향상시키기위한 훌륭한 정신 운동입니다.

@ AaronM.Eshbach의 메모 덕분에 이것이 이벤트 소싱CQRS 패턴 과 유사한 문제 도메인 이며, 시간이 지남 에 따라 분산 시스템에서 상태 변경 사항을 일련의 불변 이벤트로 모델링 하고 있음을 강조합니다 . 이 경우 쿼리 / 뷰 시스템에서 뮤 테이터 명령 처리 (이름에서 알 수 있듯이!)를 분리하여 복잡한 데이터베이스 앱을 정리하려고합니다. 물론 더 복잡하지만 더 유연합니다.


2
추가 참조는 이벤트 소싱CQRS를 참조하십시오 . 이것은 비슷한 문제 영역입니다. 분산 시스템에서 시간에 따른 일련의 불변 이벤트로 상태 변경 모델링.
Aaron M. Eshbach

@ AaronM.Eshbach가 바로 그거야! 답변에 귀하의 의견 / 인용을 포함 시켜도 될까요? 보다 권위있는 소리를냅니다. 감사!
SusanW 19

물론하지 마십시오.
Aaron M. Eshbach

3

당신은 여전히 ​​명령형 캠프에 반입니다. 한 번에 하나의 대상에 대해 생각하는 대신 놀이 또는 사건의 역사 측면에서 게임을 생각하십시오.

p1 - send m1 to battlefield
p2 - send m2 to battlefield
m1 - attacks m2 (2 dam)
m2 - attacks m1 (10 dam)
p1 - retreats m1

기타

불변 상태 개체를 생성하기 위해 액션을 연결하여 특정 시점에서 게임 상태를 계산할 수 있습니다. 각 플레이는 상태 객체를 가져 와서 새로운 상태 객체를 반환하는 함수입니다

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