함수형 프로그래밍 — 불변성


12

FP의 불변 데이터 (특히 F #에서는 다른 FP도 괜찮음)를 이해하고 상태 풀 사고 (OOP 스타일)의 오래된 습관을 깰려고합니다. 질문에 선택한 답변의 일부는 여기에 (: 생산자와 소비자가있는 큐 예를 들어) 어느 FP에 불변 사람과 OOP의 상태 표현에 의해 해결되는 문제의 주위에 쓰기 업에 대한 내 검색을 반복했다. 어떤 생각이나 링크를 환영합니다? 미리 감사드립니다.

편집 : 질문을 조금 더 명확히하기 위해 FP의 불변 구조 (예 : 대기열)를 여러 스레드 (예 : 생산자 및 소비자)에서 동시에 공유하는 방법


동시성 문제를 처리하는 한 가지 방법은 매번 큐 사본을 만드는 것입니다 (약간 비싸지 만 작동합니다).
Job

답변:


19

함수형 프로그래밍 ¹은 때때로 그런 식으로 표현 되기는하지만 상태 저장 계산을 방해하지는 않습니다. 프로그래머가 상태를 명시 적으로 만들도록 강제하는 것입니다.

예를 들어, 의사 대기열 (일부 의사 언어)을 사용하여 일부 프로그램의 기본 구조를 살펴 보겠습니다.

q := Queue.new();
while (true) {
    if (Queue.is_empty(q)) {
        Queue.add(q, producer());
    } else {
        consumer(Queue.take(q));
    }
}

기능적 큐 데이터 구조를 가진 해당 구조 (여전히 한 가지 차이점을 해결하기 위해 명령형 언어로)는 다음과 같습니다.

q := Queue.empty;
while (true) {
    if (q = Queue.empty) {
        q := Queue.add(q, producer());
    } else {
        (tail, element) := Queue.take(q);
        consumer(element);
        q := tail;
    }
}

대기열은 이제 변경할 수 없으므로 객체 자체는 변경되지 않습니다. 이 의사 코드에서 q자체는 변수입니다. 할당 q := Queue.add(…)하고 q := tail다른 객체를 가리 킵니다. 큐 함수의 인터페이스가 변경되었습니다. 각각은 조작의 결과 인 새 큐 오브젝트를 리턴해야합니다.

순전히 기능적인 언어, 즉 부작용이없는 언어에서는 모든 상태를 명시 적으로 만들어야합니다. 생산자와 소비자가 아마도 무언가를하고 있기 때문에, 그들의 상태는 여기서도 발신자의 인터페이스에 있어야합니다.

main_loop(q, other_state) {
    if (q = Queue.empty) {
        let (new_state, element) = producer(other_state);
        main_loop(Queue.add(q, element), new_state);
    } else {
        let (tail, element) = Queue.take(q);
        let new_state = consumer(other_state, element);
        main_loop(tail, new_state);
    }
}
main_loop(Queue.empty, initial_state)

이제 모든 상태 조각이 어떻게 명시 적으로 관리되는지 확인하십시오. 큐 조작 기능은 큐를 입력으로 사용하고 새 큐를 출력으로 생성합니다. 생산자와 소비자도 그들의 상태를 통과합니다.

동시 프로그래밍은 잘 맞지 않는 내부 함수형 프로그래밍,하지만 아주 잘 맞는 주위 함수형 프로그래밍을. 아이디어는 여러 개의 개별 계산 노드를 실행하고 메시지를 교환하도록하는 것입니다. 각 노드는 기능적 프로그램을 실행하며 메시지를 송수신 할 때 상태가 변경됩니다.

예제를 계속하면 하나의 큐가 있기 때문에 하나의 특정 노드에서 관리합니다. 소비자는 해당 노드에 요소를 얻기 위해 메시지를 보냅니다. 생산자는 해당 노드에 요소를 추가하라는 메시지를 보냅니다.

main_loop(q) =
    consumer->consume(q->take()) || q->add(producer->produce());
    main_loop(q)

동시성을 올바르게 얻는“산업화 된”언어는 Erlang 입니다. Erlang을 배우는 것은 동시 프로그래밍에 대한 깨달음의 길입니다.

모두 부작용없는 언어로 전환하십시오!

¹ 이 용어에는 몇 가지 의미가 있습니다. 여기서 나는 부작용없이 프로그래밍을 의미하기 위해 그것을 사용하고 있다고 생각하며, 이것이 또한 내가 사용하고있는 의미입니다.
² 암시 적 상태의 프로그래밍명령형 프로그래밍입니다 . 객체 방향은 완전히 직교하는 문제입니다.
³ 염증성, 나는 알고 있지만, 나는 그것을 의미합니다. 공유 메모리가있는 스레드는 동시 프로그래밍의 어셈블리 언어입니다. 메시지 전달은 이해하기 훨씬 쉬우 며 동시성 기능을 도입하자마자 부작용의 부재가 실제로 빛납니다.
그리고 이것은, 얼랑의 팬이 아니라 누군가하지만 다른 이유로오고있다.


2
+1 Erlang이 순수한 FP 언어가 아니라고 생각할 수도 있지만 훨씬 더 완전한 대답입니다.
Rein Henrichs

1
@Rein Henrichs : 그렇습니다. 실제로 현재 존재하는 모든 주류 언어 중에서 Erlang은 가장 충실하게 객체 지향을 구현하는 언어입니다.
Jörg W Mittag

2
@ Jörg Agreed. 다시 말하지만, 순수한 FP와 OO가 직교한다는 것을 당황하게 할 수 있습니다.
Rein Henrichs

따라서 동시 소프트웨어에서 변경 불가능한 큐를 구현하려면 노드간에 메시지를 보내고 받아야합니다. 보류중인 메시지는 어디에 저장됩니까?
mouviciel

@mouviciel 큐 요소는 노드의 수신 메시지 큐에 저장됩니다. 이 메시지 큐 기능은 분산 인프라의 기본 기능입니다. 로컬 동시성에는 적합하지만 분산 시스템에는 적합하지 않은 대체 인프라 설계는 수신자가 준비 될 때까지 발신자를 차단하는 것입니다. 나는 이것이 모든 것을 설명하지는 않는다는 것을 알고 있으며, 이것을 완전히 설명하기 위해서는 동시 프로그래밍에 관한 한 장 또는 두 권의 책이 필요합니다.
Gilles 'SO- 악마 그만해'

4

FP 언어의 상태 저장 동작은 이전 상태에서 새 상태로의 변환으로 구현됩니다. 예를 들어, 대기열에 넣기는 대기열과 값을 대기열에 넣은 새 대기열로 변환하는 것입니다. 대기열에서 값으로의 변환 및 값이 제거 된 새 대기열이 대기열에서 제외됩니다. 이 상태 변환 (및 기타 계산 결과)을 유용한 방법으로 추상화하기 위해 모나드와 같은 구성이 고안되었습니다.


3
모든 추가 / 제거 작업에 대해 새 대기열 인 경우 두 개 이상의 비동기 작업 (스레드)이 어떻게 대기열을 공유합니까? 새로운 큐잉을 추상화하는 패턴입니까?
venkram

동시성은 완전히 다른 질문입니다. 의견에 충분한 답변을 드릴 수 없습니다.
Rein Henrichs

2
@Rein Henrichs : "주석에 충분한 답변을 제공 할 수 없습니다". 일반적으로 주석 관련 문제를 해결하기 위해 답변을 업데이트해야합니다.
S.Lott

동시성도 모나 딕 할 수 있습니다. haskells Control.Concurrency.STM을 참조하십시오.
대안

1
이 경우 @ S.Lott는 OP가 새로운 질문을해야 함을 의미합니다. 불변 데이터 구조에 관한 동시성 (concurrency)은이 질문에 대한 OT입니다.
Rein Henrichs

2

... FP에서 불변 인 것으로 OOP의 상태 저장 표현으로 해결되는 문제 (예 : 생산자와 소비자가있는 대기열)

귀하의 질문은 "XY 문제"입니다. 구체적으로, 당신이 인용하는 개념 (생산자와 소비자와의 큐)은 실제로 해결책 이며 설명하는 "문제"가 아닙니다. 본질적으로 불완전한 것을 순전히 기능적으로 구현하도록 요구하기 때문에 어려움이 있습니다. 내 대답은 질문으로 시작합니다. 해결하려는 문제는 무엇입니까?

여러 생산자가 결과를 단일 공유 소비자에게 보내는 방법에는 여러 가지가 있습니다. F #에서 가장 확실한 해결책은 소비자를 에이전트 (일명 MailboxProcessor)로 만들고 생산자 Post에게 소비자 에이전트에 대한 결과를 제공하는 것입니다. 이것은 내부적으로 큐를 사용하며 순수하지 않습니다 (F #에서 메시지를 보내는 것은 통제되지 않은 부작용, 불순물입니다).

그러나 근본적인 문제는 병렬 프로그래밍의 스 캐터 수집 패턴과 비슷한 것일 가능성이 큽니다. 이 문제를 해결하기 위해 입력 값의 배열을 만든 다음 그 값을 Array.Parallel.map초과하여 serial을 사용하여 결과를 수집 할 수 Array.reduce있습니다. 또는 PSeq모듈의 함수를 사용 하여 시퀀스 요소를 병렬로 처리 할 수 있습니다 .

또한 스테이트 풀 한 사고에는 아무 문제가 없다고 강조해야합니다. 순도에는 장점이 있지만 그것은 만병 통치약이 아니므로 그 단점을 스스로 인식해야합니다. 실제로 이것이 바로 F #이 순수한 기능 언어가 아닌 이유입니다. 따라서 불순물을 선호 할 때 사용할 수 있습니다.


1

Clojure는 동시성과 밀접한 관련이있는 상태와 정체성이라는 개념을 잘 알고 있습니다. 불변성은 중요한 역할을하며 Clojure의 모든 값은 불변이며 참조를 통해 액세스 할 수 있습니다. 참조는 단순한 포인터 이상의 것입니다. 그들은 가치에 대한 액세스를 관리하며 다른 의미를 가진 여러 유형이 있습니다. 새로운 (불변의) 값을 가리 키도록 참조를 수정할 수 있으며 이러한 변경은 원자적인 것으로 보장됩니다. 그러나 수정 후에도 다른 모든 스레드는 최소한 참조에 다시 액세스 할 때까지 원래 값으로 계속 작동합니다.

Clojure의 상태와 정체성에 대한 훌륭한 기사 를 읽는 것이 좋습니다 . 세부 사항을 자세히 설명합니다.

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