함수형 프로그래밍 : 동시성 및 상태에 대한 올바른 아이디어?


21

FP 지지자들은 패러다임이 변경 가능한 상태를 피하기 때문에 동시성이 쉽다고 주장했다. 나는 그것을 얻지 못한다.

순수한 기능과 불변의 데이터 구조를 강조하는 FP를 사용하여 멀티 플레이어 던전 크롤링 (로그 로그)을 생성한다고 상상해보십시오. 방, 복도, 영웅, 몬스터 및 전리품으로 구성된 던전을 생성합니다. 우리의 세계는 효과적으로 구조와 그 관계의 대상 그래프입니다. 상황이 변화함에 따라 우리의 세계 표현은 그 변화를 반영하도록 수정됩니다. 우리의 영웅은 쥐를 죽이고, 단검을 집어 올리는 등

나에게 세계 (현재의 현실)는이 상태에 대한 아이디어를 가지고 있으며 FP가 이것을 극복하는 방법을 놓치고있다. 우리의 영웅이 행동을 취할 때, 기능은 세계의 상태를 수정합니다. 모든 결정 (AI 또는 인간)은 현재와 같이 세계의 상태를 기반으로해야하는 것으로 보입니다. 우리는 어디에서 동시성을 허용합니까? 하나의 프로세스가 만료 된 상태를 기반으로 결과를 얻지 않도록 여러 상태의 프로세스를 동시에 세계 상태로 개선 할 수는 없습니다. 모든 제어는 단일 제어 루프 내에서 발생해야하므로 항상 현재 세계 객체 그래프로 표시되는 현재 상태를 처리해야합니다.

분명히 동시성에 완벽하게 적합한 상황이 있습니다 (즉, 상태가 서로 독립적 인 격리 된 작업을 처리 할 때).

필자의 예에서 동시성이 얼마나 유용한 지 잘 모르겠으며 이것이 문제가 될 수 있습니다. 어떻게 든 주장을 잘못 표현했을 수 있습니다.

누군가이 주장을 더 잘 대표 할 수 있습니까?


1
공유 상태를 말하는 것입니다. 공유 상태는 항상 현재 상태이며 항상 어떤 형태의 동기화가 필요할 것입니다. 순수한 FP 사람들 사이에서 가장 선호되는 형태는 STM으로, 액세스 계층을 경쟁적으로 만드는 추상화 계층을 통해 공유 메모리를 로컬 메모리로 취급 할 수 있습니다. 조건은 자동으로 처리됩니다. 공유 메모리를위한 또 다른 기술은 메시지 전달인데, 공유 메모리 대신에 로컬 메모리와 다른 액터에 대한 로컬 메모리를 요구하는 지식이 있습니다.
Jimmy Hoffa

1
그렇다면 ... 공유 상태 동시성이 단일 스레드 응용 프로그램에서 상태를 관리하는 데 어떻게 도움이되는지 묻고 있습니까? 반면, 귀하의 예는 그러한 방식으로 구현되었는지 여부에 관계없이 개념적으로 동시성 (각 AI 제어 엔티티의 스레드)에 적합합니다. 나는 당신이 여기에서 묻는 것을 혼란스럽게 생각합니다.
CA McCann

1
한마디로 Zippers
jk.

2
모든 사물은 자신의 세계관을 가질 것입니다. 결국 일관성 이있을 것 입니다. 또한 아마도 웨이브 함수 축소 와 함께 우리의 "실제 세계" 에서 일들이 어떻게 작동하는지 입니다.
herzmeister

1
"순전히 기능적인 retrogames"가 흥미로울 것입니다 : prog21.dadgum.com/23.html
user802500

답변:


15

나는 대답을 암시하려고 노력할 것이다. 이것은 답 이 아니며 입문적인 예시 일뿐입니다. @ jk의 대답은 실제 지퍼를 가리 킵니다.

불변의 트리 구조가 있다고 상상해보십시오. 자식을 삽입하여 하나의 노드를 변경하려고합니다. 결과적으로 완전히 새로운 나무를 얻게됩니다.

그러나 새로운 나무의 대부분은 오래된 나무와 정확히 동일합니다. 영리한 구현 은 변경된 노드 주위에 포인터를 라우팅하여 대부분의 트리 조각을 재사용합니다.

위키 백과에서

오카 사키의 책 은 이와 같은 예들로 가득합니다.

따라서 매 이동마다 게임 세계의 작은 부분을 합리적으로 변경하고 (코인을 픽업) 실제로 세계 데이터 구조의 작은 부분 (코인이 픽업 된 셀) 만 변경할 수 있다고 가정합니다. 과거 상태에만 속하는 부품은 시간에 따라 가비지 수집됩니다.

이것은 데이터 게임 세계 구조를 적절한 방식으로 디자인 할 때 약간의 고려가 필요합니다. 불행히도, 나는 이러한 문제에 대해 전문가가 아닙니다. 분명히 이것은 NxM 행렬이 아닌 다른 것이어야하며 가변 데이터 구조로 사용합니다. 아마도 트리 노드처럼 서로를 가리키는 작은 조각 (복도-개별 셀?)으로 구성되어야합니다.


3
+1 : 오카 사키의 책을 가리 키기 위해. 나는 그것을 읽지 못했지만 할 일 목록에 있습니다. 나는 당신이 묘사 한 것이 올바른 해결책이라고 생각합니다. 대안으로 고유 유형 (Clean, en.wikipedia.org/wiki/Uniqueness_type )을 고려할 수 있습니다 . 이러한 유형을 사용하면 참조 투명성을 유지하면서 데이터 오브젝트를 파괴적으로 업데이트 할 수 있습니다.
Giorgio

키 또는 ID를 통한 간접 참조를 통해 관계를 정의하면 이점이 있습니까? 즉, 한 구조에서 다른 구조로의 실제 접촉이 적을수록 변화가 발생할 때 세계 구조에 대한 수정이 적어야한다고 생각했습니다. 아니면이 기술이 FP에서 실제로 사용되지 않습니까?
마리오 T. 란자

9

9000의 답 은 절반의 답이며 영구적 인 데이터 구조를 통해 변경되지 않은 부분을 재사용 할 수 있습니다.

그러나 이미 "나무의 뿌리를 바꾸고 싶다면 어떻게해야합니까?" 주어진 모든 예를 의미하므로 이제 모든 노드를 변경해야합니다. 이곳은 지퍼 가 구조를 위해 온 곳 입니다. 그것들은 O (1)에서 포커스의 요소가 변경 될 수있게하고, 포커스는 구조의 어느 곳으로나 이동할 수 있습니다.

지퍼의 또 다른 요점은 원하는 모든 데이터 유형에 지퍼가 존재한다는 것입니다


나는 단지 FP를 탐험하는 프린지에 있기 때문에 "지퍼"를 파는데 시간이 좀 걸릴 것 같습니다. 하스켈에 대한 경험이 없습니다.
마리오 T. 란자

나중에 오늘 예를 추가하려고합니다
JK합니다.

4

기능적 스타일 프로그램은 동시성을 사용할 수있는 많은 기회를 만듭니다. 컬렉션을 변환 또는 필터링하거나 집계 할 때마다 모든 것이 순수하거나 변경 불가능한 경우 동시성으로 작업이 가속화 될 수 있습니다.

예를 들어 AI 결정을 서로 독립적으로, 특정 순서없이 수행한다고 가정합니다. 그들은 차례를 바꾸지 않고 모두 동시에 결정을 내린 다음 세상이 발전합니다. 코드는 다음과 같습니다.

func MakeMonsterDecision curWorldState monster =
    ...
    ...
    return monsterDecision

func NextWorldState curWorldState =
    ...
    let monsterMakeDecisionForCurrentState = MakeMonsterDecision curWorldState
    let monsterDecisions = List.map monsterMakeDecisionForCurrentState activeMonsters
    ...
    return newWorldState

월드 상태에서 몬스터가 수행 할 작업을 계산하고 다음 월드 상태를 계산하는 과정에서 모든 몬스터에 적용하는 기능이 있습니다. 이것은 기능적 언어로하는 자연스러운 일이며, 컴파일러는 '모든 몬스터에게 적용'단계를 동시에 수행 할 수 있습니다.

명령형 언어에서는 모든 몬스터를 반복하여 그 효과를 세계에 적용 할 가능성이 높습니다. 복제 또는 복잡한 앨리어싱을 처리하고 싶지 않기 때문에 그렇게하는 것이 더 쉽습니다. 이 경우 초기 괴물 결정이 이후 괴물 결정에 영향을주기 때문에 컴파일러 괴물 계산을 병렬로 수행 할 수 없습니다 .


꽤 도움이됩니다. 게임에서 몬스터가 다음에 무엇을할지 결정할 때 어떻게 큰 이점이 있는지 알 수 있습니다.
Mario T. Lanza

4

- 몇 리치 키스 마크 회담 듣기 이 하나 특히 - 내 혼란을 완화. 하나에서 그는 동시 프로세스가 가장 최신 상태가 아닐 수도 있다고 지적했다. 나는 그것을들을 필요가 있었다. 내가 소화하는 데 어려움을 겪고 있었던 것은 프로그램이 실제로 새로운 스냅 샷으로 대체 된 세계의 스냅 샷을 기반으로 결정하면 괜찮다는 것입니다. 나는 동시 FP가 어떻게 이전 상태에 대한 결정을 내릴 수 있는지 궁금해했다.

뱅킹 응용 프로그램에서 우리는 이후에 새로운 것으로 대체 된 상태의 스냅 샷 (철회가 발생 함)을 기반으로 결정을 결코 원하지 않을 것입니다.

FP 패러다임은 변경 가능한 상태를 피하기 때문에 동시성이 쉽습니다. 이는 잠재적으로 오래된 상태에 기반을 둔 결정의 논리적 장점에 대해 아무 것도 말하지 않는 기술적 주장입니다. FP는 여전히 궁극적으로 상태 변화를 모델링합니다. 이 문제를 해결하지 못했습니다.


0

FP 지지자들은 패러다임이 변경 가능한 상태를 피하기 때문에 동시성이 쉽다고 주장했다. 나는 그것을 얻지 못한다.

나는이 일반적인 질문에 대해 기능적 신생 생물이지만 몇 년 동안 부작용으로 내 시선에 의존해 왔으며, 더 쉬운 (또는 구체적으로 "사피, 오류가 적은 ") 동시성. 기능적인 동료들과 그들이하고있는 일을 살펴보면 잔디는 조금 더 녹색으로 보이고 적어도이 점에서 더 좋은 냄새가납니다.

시리얼 알고리즘

즉, 특정 예에서 문제가 본질적으로 연속적이며 A가 완료 될 때까지 B를 실행할 수 없다면 개념적으로 A와 B를 병렬로 실행할 수 없습니다. 오래된 게임 상태를 사용하여 병렬 이동을 기반으로 답변에서와 같이 주문 종속성을 깨는 방법을 찾거나 다른 답변에서 제안 된 순서 종속성을 제거하기 위해 일부를 독립적으로 수정할 수있는 데이터 구조를 사용해야합니다 또는 이런 종류의 무언가. 그러나 이와 같은 개념 설계 문제는 분명히 불변이기 때문에 모든 것을 쉽게 멀티 스레드 할 수는 없습니다. 가능하다면 순서 의존성을 깨뜨리는 현명한 방법을 찾을 때까지 어떤 것이 자연스럽게 직렬화 될 것입니다.

보다 쉬운 동시성

즉, 스레드 안전성 이 떨어질 가능성 때문에 단순히 성능을 크게 향상시킬 수있는 장소에 부작용이있는 프로그램을 병렬화하지 못하는 경우가 많습니다 . 변경 가능한 상태 (또는 더 구체적으로 외부 부작용)를 제거하는 것이 도움이되는 경우 중 하나는 "스레드 안전 할 수도 있고 아닐 수도 있습니다""분명 스레드 안전" 으로 바뀌는 입니다.

이 문장을 좀 더 구체적으로 만들기 위해 C에서 정렬 기능을 구현하고 비교기를 허용하고 요소 배열을 정렬하는 데 사용하는 정렬 기능을 구현하는 작업을 제공한다고 생각하십시오. 상당히 일반화되어야하지만 항상 다중 스레드 구현을 사용하는 것이 의심 할 여지없이 그러한 규모 (수백만 요소 이상)의 입력에 대해 사용될 것이라고 쉽게 가정합니다. 정렬 기능을 멀티 스레드 할 수 있습니까?

문제는 당신이 비교기 때문에 함수 호출을 정렬 할 수 없다는 것입니다 수 있습니다기능을 퇴화시키지 않고 불가능한 모든 가능한 경우에 대해 어떻게 구현되는지 (또는 최소한 문서화되어 있는지) 알지 않는 한 부작용을 유발하십시오. 비교기는 전역이 아닌 변수로 내부 변수를 수정하는 것과 같은 역겨운 일을 할 수 있습니다. 비교기의 99.9999 %는이 작업을 수행하지 않을 수 있지만 부작용을 일으킬 수있는 경우의 0.00001 % 때문에이 일반화 된 함수를 멀티 스레딩 할 수 없습니다. 결과적으로 단일 스레드 및 다중 스레드 정렬 기능을 제공하고이를 사용하여 프로그래머에게 스레드 안전을 기반으로 사용할 기능을 결정하는 책임을 전달해야 할 수도 있습니다. 또한 사람들은 여전히 ​​단일 스레드 버전을 사용하고 비교기가 스레드 안전인지 확실하지 않기 때문에 멀티 스레드 기회를 놓칠 수 있습니다.

기능이 현재와 미래에 부작용을 일으키지 않을 것이라는 단단한 보장을 가지고 있다면 어디에서나 자물쇠를 던지지 않고 사물의 스레드 안전에 대해 합리화하는 데 관여 할 수있는 많은 두뇌 능력이 있습니다. 경쟁 조건을 너무 많이 디버깅해야하는 사람은 110 % 확신 할 수없는 모든 항목을 스레드로부터 안전하고 그대로 유지하기를 주저 할 수 있기 때문에 두려움이 있습니다. 가장 편집증적인 (아마도 적어도 경계선 일지라도) 순수한 기능조차도, 우리가 안전하게 그것을 동시에 부를 수 있다는 안심 감과 자신감을 제공합니다.

그리고 그것이 당신이 순수한 기능 언어로 얻을 수있는 스레드 안전하다는 확실한 보장을 얻을 수 있다면 그것이 매우 유익하다고 본 주요 사례 중 하나입니다. 다른 하나는 기능적 언어가 종종 부작용없이 기능을 만드는 것을 촉진한다는 것입니다. 예를 들어, 대규모 데이터 구조를 입력 한 다음 원본을 건드리지 않고 원본의 일부만 변경하여 새로운 것을 출력하는 것이 상당히 효율적인 영구 데이터 구조를 제공 할 수 있습니다. 이러한 데이터 구조없이 작업하는 사용자는 직접 수정하여 스레드 안전성을 잃을 수 있습니다.

부작용

즉, 나는 기능적인 친구들 (내가 멋지다고 생각하는 사람들)에 대해 모든 것에 대해 한 가지 의견에 동의하지 않는다.

[...] 패러다임이 변경 가능한 상태를 피하기 때문입니다.

동시성을 내가 보는 것처럼 실용적으로 만드는 것은 불변성이 아닙니다. 부작용을 피하는 기능입니다. 함수가 정렬 할 배열을 입력하고 복사 한 다음 사본을 변경하여 내용을 정렬하고 사본을 출력하는 경우 동일한 입력을 전달하더라도 불변 배열 유형을 사용하는 것과 마찬가지로 스레드 안전 여러 스레드에서 배열로 배열하십시오. 따라서 나는 매우 동시성 친화적 인 코드를 생성 할 때 여전히 가변 유형이있는 곳이 있다고 생각합니다. 그러나 불변 속성에 많이 사용하지 않는 영구 데이터 구조를 포함하여 불변 유형에 많은 추가 이점이 있지만 부작용없이 기능을 생성하기 위해 모든 것을 딥 카피해야하는 비용을 제거합니다.

그리고 추가 데이터를 섞고 복사하는 형태로 부작용을 없애고 기능을 추가하는 데 오버 헤드가있을 수 있습니다. 지속적인 데이터 구조의 일부에 대한 간접적 인 수준과 GC가있을 수 있습니다.하지만 32 코어 머신인데, 더 많은 것을 동시에 할 수 있다면 교환이 그만한 가치가 있다고 생각합니다.


1
"변경 가능 상태"는 항상 절차 수준이 아니라 응용 프로그램 수준의 상태를 의미합니다. 포인터를 제거하고 매개 변수를 복사 값으로 전달하는 것은 FP에 구운 기술 중 하나입니다. 그러나 유용한 함수는 어떤 수준 에서 상태 를 변경해야합니다. 함수형 프로그래밍의 요점은 호출자에 속하는 변경 가능한 상태가 프로 시저에 들어 가지 않고 리턴 값을 제외하고는 프로 시저를 종료하지 않도록하는 것입니다! 그러나 상태를 변경하지 않고 많은 작업을 수행 할 수있는 프로그램은 거의 없으며 오류는 항상 인터페이스에서 다시 발생합니다.
Steve

1
솔직히 말해서, 대부분의 현대 언어는 기능적 스타일의 프로그래밍이 (약간의 훈련과 함께) 사용되도록 허용하며, 물론 기능적 패턴 전용 언어가 있습니다. 그러나 이것은 계산 효율이 떨어지는 패턴이며 90 년대의 객체 방향과 같이 모든 질병에 대한 해결책으로 널리 퍼져 있습니다. 대부분의 프로그램은 병렬화의 이점이있는 CPU 집약적 계산에 구속되지 않으며, 병렬 실행에 적합한 방식으로 프로그램을 추론, 설계 및 구현하기가 어려운 경우가 많습니다.
Steve

1
변경 가능한 상태를 다루는 대부분의 프로그램은 어떤 이유로 든 그렇게해야하기 때문에 그렇게합니다. 그리고 대부분의 프로그램은 공유 상태를 사용하거나 비정상적으로 업데이트하기 때문에 부정확하지 않습니다. 일반적으로 예기치 않은 가비지를 입력으로 수신하거나 (가비지 출력을 결정 함) 입력에 대해 잘못 작동하기 때문입니다 (목적으로 잘못된 의미에서) 달성 될 것이다). 기능 패턴은이를 해결하는 데 거의 도움이되지 않습니다.
Steve

1
@Steve C 또는 C ++와 같은 언어에서 스레드로부터 안전한 방식으로 작업을 수행하는 방법을 모색하고 있기 때문에 적어도 반쯤 동의 할 수도 있습니다. 실제로 우리가 완전히 갈 필요는 없다고 생각합니다. -순수 기능을 날려 버렸습니다. 그러나 FP의 일부 개념은 적어도 유용하다는 것을 알았습니다. 필자는 여기서 PDS가 유용한 방법에 대한 답변을 작성했으며 PDS에 대해 가장 큰 이점은 실제로 스레드 안전성이 아니라 인스턴스화, 비파괴적인 편집, 예외 안전성, 간단한 실행 취소 등과 같은 것들입니다.

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