변경 가능한 상태없이 유용한 것을 어떻게 할 수 있습니까?


265

최근에 함수형 프로그래밍에 대한 많은 내용을 읽었으며 대부분을 이해할 수 있지만 머리를 감쌀 수없는 것은 상태 비 저장 코딩입니다. 변경 가능한 상태를 제거하여 프로그래밍을 단순화하는 것은 대시 보드를 제거하여 자동차를 "단순화"하는 것과 같습니다. 완제품이 더 간단 할 수 있지만 최종 사용자와 상호 작용할 수 있기를 바랍니다.

내가 생각할 수있는 거의 모든 사용자 응용 프로그램은 상태를 핵심 개념으로 포함합니다. 문서 (또는 SO 게시물)를 작성하면 새로운 입력마다 상태가 변경됩니다. 또는 비디오 게임을하는 경우 모든 캐릭터의 위치부터 끊임없이 움직이는 경향이있는 수많은 상태 변수가 있습니다. 값 변경을 추적하지 않고 어떻게 유용한 것을 할 수 있습니까?

이 문제에 대해 논의 할 내용을 찾을 때마다 실제 기술적 인 기능으로 작성되어 있으며 FP 배경이 없다고 가정합니다. 누구든지 명령형 코딩에 대해 잘 이해하고 있지만 기능 측면에서 완전한 n00b를 가진 사람에게 이것을 설명하는 방법을 알고 있습니까?

편집 : 지금까지 많은 응답이 불변 값의 장점을 설득하려고하는 것 같습니다. 나는 그 부분을 얻는다. 완벽하게 이해됩니다. 내가 이해하지 못하는 것은 가변 변수없이 변경하고 지속적으로 변경 해야하는 값을 추적하는 방법입니다.



1
저의 개인적인 겸손한 의견은 그것이 힘과 돈과 같다는 것입니다. 수익 감소 법이 적용됩니다. 당신이 상당히 강하다면 약간 더 강해지는 인센티브가 거의 없을 수도 있지만, 그것에 대해 일하는 것은 상처를 입지 않습니다. 전역 변경 가능 상태에도 동일하게 적용됩니다. 코딩 기술이 진행됨에 따라 코드에서 전역 변경 가능 상태의 양을 제한하는 것이 좋다는 것을 개인적으로 선호합니다. 완벽하지는 않지만 전역 변경 가능 상태를 최소화하기 위해 노력하는 것이 좋습니다.
AturSams

돈과 마찬가지로 더 많은 시간을 투자하고 더 이상 유용하지 않으며 다른 우선 순위가 더 높아질 것입니다. 예를 들어, 당신이 (내 은유에 따라) 가능한 최대의 힘에 도달하면, 그것은 유용한 목적을 달성하지 못하고 심지어 짐이 될 수도 있습니다. 그러나 달성하기 어려운 목표를 향해 노력하고 적절한 자원을 투자하는 것이 여전히 좋습니다.
AturSams

7
간단히 말해서 FP에서 함수는 상태를 수정하지 않습니다. 결국 현재 상태 를 대체 하는 무언가를 반환 합니다 . 그러나 상태는 수정되지 않습니다.
jinglesthula

돌연변이없이 상태 저장을 얻는 방법이 있지만 (내가 이해 한 스택을 사용하여)이 질문은 요점 옆에 있습니다 (훌륭한 질문 임에도 불구하고). 간결하게 이야기하기는 어렵지만 다음은 질문에 대한 답변이 medium.com/@jbmilgrom/… 인 게시물 입니다. TLDR은 상태 저장 기능 프로그램의 의미도 변경할 수 없지만 프로그램 기능의 통신 실행은 처리됩니다.
jbmilgrom

답변:


166

또는 비디오 게임을하는 경우 모든 캐릭터의 위치부터 끊임없이 움직이는 경향이있는 수많은 상태 변수가 있습니다. 값 변경을 추적하지 않고 어떻게 유용한 것을 할 수 있습니까?

당신이 관심이 있다면, 여기 얼랑와 게임 프로그래밍을 설명하는 일련의 기사는.

이 답변이 마음에 들지 않지만 사용할 때까지 기능적 프로그램을 얻지 못할 것입니다. 나는 코드 샘플을 게시하고 말 "여기,하지 작업을 수행 할 수 있습니다 참조 "-하지만 당신은 구문과 기본 원리를 이해하지 않으면, 당신의 눈은 유약. 당신의 관점에서 볼 때, 나는 명령형 언어와 같은 일을하는 것처럼 보이지만 의도적으로 프로그래밍을 더 어렵게 만들기 위해 모든 종류의 경계를 설정합니다. 내 관점에서, 당신은 Blub 역설을 경험하고 있습니다.

처음에는 회의적이지만 몇 년 전에 기능 프로그래밍 기차를 타고 뛰어 들었습니다. 함수형 프로그래밍의 요령은 패턴, 특정 변수 할당을 인식하고 명령 상태를 스택으로 옮길 수 있습니다. 예를 들어 for-loop는 재귀가됩니다.

// Imperative
let printTo x =
    for a in 1 .. x do
        printfn "%i" a

// Recursive
let printTo x =
    let rec loop a = if a <= x then printfn "%i" a; loop (a + 1)
    loop 1

그다지 예쁘지는 않지만 돌연변이없이 동일한 효과를 얻었습니다. 물론, 가능하면 루프를 피하고 추상화하는 것이 좋습니다.

// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)

Seq.iter 메소드는 콜렉션을 열거하고 각 항목에 대해 익명 함수를 호출합니다. 매우 편리합니다 :)

숫자 인쇄가 인상적이지는 않습니다. 그러나 게임에서 동일한 접근 방식을 사용할 수 있습니다. 스택의 모든 상태를 유지하고 재귀 호출의 변경 사항으로 새 객체를 만듭니다. 이러한 방식으로 각 프레임은 게임의 상태 비 저장 스냅 샷이며, 각 프레임은 단순히 상태 비 저장 개체가 업데이트해야하는 항목을 원하는대로 변경하여 완전히 새로운 개체를 만듭니다. 이에 대한 의사 코드는 다음과 같습니다.

// imperative version
pacman = new pacman(0, 0)
while true
    if key = UP then pacman.y++
    elif key = DOWN then pacman.y--
    elif key = LEFT then pacman.x--
    elif key = UP then pacman.x++
    render(pacman)

// functional version
let rec loop pacman =
    render(pacman)
    let x, y = switch(key)
        case LEFT: pacman.x - 1, pacman.y
        case RIGHT: pacman.x + 1, pacman.y
        case UP: pacman.x, pacman.y - 1
        case DOWN: pacman.x, pacman.y + 1
    loop(new pacman(x, y))

명령형 버전과 기능 버전은 동일하지만 기능 버전은 변경 가능한 상태를 분명히 사용하지 않습니다. 함수 코드는 모든 상태를 스택에 유지합니다.이 방법의 좋은 점은 문제가 발생하면 디버깅이 쉬우 며 스택 트레이스 만 있으면된다는 것입니다.

모든 오브젝트 (또는 관련 오브젝트 모음)를 자체 스레드에서 렌더링 할 수 있기 때문에 게임에서 오브젝트의 개수에 관계없이 확장 할 수 있습니다.

내가 생각할 수있는 거의 모든 사용자 응용 프로그램은 상태를 핵심 개념으로 포함합니다.

기능적 언어에서는 객체의 상태를 변경하지 않고 원하는 변경 사항을 가진 새로운 객체를 반환합니다. 소리보다 효율적입니다. 예를 들어 데이터 구조는 변경 불가능한 데이터 구조로 표현하기가 매우 쉽습니다. 예를 들어 스택은 구현하기가 매우 쉽습니다.

using System;

namespace ConsoleApplication1
{
    static class Stack
    {
        public static Stack<T> Cons<T>(T hd, Stack<T> tl) { return new Stack<T>(hd, tl); }
        public static Stack<T> Append<T>(Stack<T> x, Stack<T> y)
        {
            return x == null ? y : Cons(x.Head, Append(x.Tail, y));
        }
        public static void Iter<T>(Stack<T> x, Action<T> f) { if (x != null) { f(x.Head); Iter(x.Tail, f); } }
    }

    class Stack<T>
    {
        public readonly T Head;
        public readonly Stack<T> Tail;
        public Stack(T hd, Stack<T> tl)
        {
            this.Head = hd;
            this.Tail = tl;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Stack<int> x = Stack.Cons(1, Stack.Cons(2, Stack.Cons(3, Stack.Cons(4, null))));
            Stack<int> y = Stack.Cons(5, Stack.Cons(6, Stack.Cons(7, Stack.Cons(8, null))));
            Stack<int> z = Stack.Append(x, y);
            Stack.Iter(z, a => Console.WriteLine(a));
            Console.ReadKey(true);
        }
    }
}

위의 코드는 두 개의 불변 목록을 구성하고 함께 추가하여 새 목록을 만들고 결과를 추가합니다. 응용 프로그램의 어느 곳에서도 변경 가능한 상태가 사용되지 않습니다. 약간 부피가 커 보이지만 C #이 자세한 언어이기 때문입니다. F #의 해당 프로그램은 다음과 같습니다.

type 'a stack =
    | Cons of 'a * 'a stack
    | Nil

let rec append x y =
    match x with
    | Cons(hd, tl) -> Cons(hd, append tl y)
    | Nil -> y

let rec iter f = function
    | Cons(hd, tl) -> f(hd); iter f tl
    | Nil -> ()

let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
let y = Cons(5, Cons(6, Cons(7, Cons(8, Nil))))
let z = append x y
iter (fun a -> printfn "%i" a) z

목록을 작성하고 조작하는 데 변경이 필요하지 않습니다. 거의 모든 데이터 구조를 기능적으로 동등한 것으로 쉽게 변환 할 수 있습니다. 나는 스택, 큐, 좌파 힙, 레드 블랙 트리, 게으른 목록의 변경 불가능한 구현을 제공 하는 페이지를 여기에 작성했습니다 . 단일 코드 스 니펫에는 변경 가능한 상태가 없습니다. 나무를 "돌연변이"시키기 위해 원하는 새 노드로 새로운 노드를 만듭니다. 이것은 트리에서 모든 노드의 복사본을 만들 필요가 없기 때문에 매우 효율적입니다. 새 노드에서 기존 노드를 재사용 할 수 있습니다. 나무.

더 중요한 예를 사용하여, 나는 완전히 상태가없는 이 SQL 파서 를 작성 했습니다 (또는 적어도 코드는 상태가 없습니다. 기본 lexing 라이브러리가 상태가 없는지 모르겠습니다).

상태 비 저장 프로그래밍은 상태 비 저장 프로그래밍과 마찬가지로 표현력이 뛰어나고 강력하므로 무 상태로 사고를 시작하도록 약간의 연습 만하면됩니다. 물론 "가능한 경우 상태 비 저장 프로그래밍, 필요한 경우 상태 저장 프로그래밍"은 가장 불완전한 기능 언어의 모토로 보입니다. 기능적 접근 방식이 깨끗하지 않거나 효율적이지 않을 때 변경 가능 변수로 넘어가는 데 아무런 해가 없습니다.


7
나는 팩맨 예제를 좋아한다. 그러나 그것은 하나의 문제를 해결하여 다른 문제를 제기 할 수 있습니다. 그러면 가비지 수집 및 교체되지 않습니다. 대신 두 개의 객체 사본으로 끝나고 그중 하나는 유효하지 않습니다. 이 문제를 어떻게 처리합니까?
메이슨 휠러

9
분명히 새로운 팩맨 객체로 새로운 "다른 것"을 만들어야합니다.) 물론 경로를 너무 멀리 가져 가면 무언가가 바뀔 때마다 전 세계에 대한 객체 그래프가 다시 생성됩니다. 더 나은 접근법은 여기에 설명되어 있습니다 ( prog21.dadgum.com/26.html ) : 객체가 자신과 모든 종속성을 업데이트하지 않고 상태에 대한 메시지를 이벤트 루프로 전달하는 것이 훨씬 쉽습니다. 업데이트. 이를 통해 그래프의 어떤 개체를 업데이트해야하는지, 그렇지 않은 개체를 결정하기가 훨씬 쉬워집니다.
Juliet

6
@ 줄리엣, 의심의 여지가 있습니다-완전히 명령적인 사고 방식에서 재귀는 어느 시점에서 끝나야합니다. 그렇지 않으면 결국 스택 오버플로가 발생합니다. 재귀 팩맨 예제에서 스택은 어떻게 베이에 유지됩니까? 함수의 시작 부분에 객체가 내재적으로 팝됩니까?
BlueStrat

9
@BlueStrat-좋은 질문 ... "꼬리 호출"인 경우 ... 예를 들어 재귀 호출이 함수의 마지막 항목입니다 ... 시스템은 새로운 스택 프레임을 생성 할 필요가 없습니다 ... 이전 것을 재사용하십시오. 이것은 기능적 프로그래밍 언어에 대한 일반적인 최적화입니다. en.wikipedia.org/wiki/Tail_call
파충류

4
@MichaelOsofsky는 데이터베이스 및 API와 상호 작용할 때 항상 통신 할 상태가있는 '외부 세계'가 있습니다. 이 경우 100 % 기능을 수행 할 수 없습니다. 이 '기능하지 않은'코드를 격리하고 추상화하여 외부 세계로가는 입구와 출구가 하나만 있도록하는 것이 중요합니다. 이 방법으로 나머지 코드를 기능적으로 유지할 수 있습니다.
Chielt

76

짧은 대답 : 당신은 할 수 없습니다.

그렇다면 불변성에 대한 소란은 무엇입니까?

명령형 언어에 정통한 사용자라면 "전역이 나쁘다"는 것을 알고 있습니다. 왜? 그것들은 코드에서 매우 다루기 어려운 의존성을 도입하거나 도입 할 가능성이 있기 때문에. 그리고 의존성은 좋지 않습니다. 코드가 모듈화 되기를 원합니다 . 프로그램의 일부는 다른 부분에 가능한 한 영향을 미치지 않습니다. 그리고 FP는 모듈 방식의 성배 당신을 제공합니다 : 없음 부작용이 전혀은 . f (x) = y가 있습니다. x를 넣고 y를 꺼내십시오. x 나 다른 변경 사항이 없습니다. FP를 사용하면 상태에 대한 생각을 멈추고 가치 측면에서 생각을 시작할 수 있습니다. 모든 함수는 단순히 값을 받고 새로운 값을 생성합니다.

이것은 몇 가지 장점이 있습니다.

첫째, 부작용이 없다는 것은 프로그램이 단순하고 추론하기 쉽다는 것을 의미합니다. 프로그램의 새로운 부분을 도입하는 것이 기존의 작동중인 부분을 방해하고 충돌 할 염려가 없습니다.

둘째, 이것은 프로그램을 단순하게 병렬화 할 수있게합니다 (효율적인 병렬화는 또 다른 문제입니다).

셋째, 몇 가지 가능한 성능 이점이 있습니다. 기능이 있다고 가정 해보십시오.

double x = 2 * x

이제 3을 입력하고 6을 얻습니다. 매번 그러나 당신도 그렇게 할 수 있습니다. 네. 그러나 문제는 필수적으로 더 많은 것을 할 수 있다는 것 입니다. 내가 할 수있는:

int y = 2;
int double(x){ return x * y; }

그러나 나는 또한 할 수 있었다

int y = 2;
int double(x){ return x * (y++); }

명령형 컴파일러는 부작용이 있는지 여부를 알지 못하므로 최적화하기가 더 어려워집니다 (즉, 매번 두 번 2가 4 일 필요는 없습니다). 기능적인 사람은 내가 알지 못하므로 "double 2"가 표시 될 때마다 최적화 할 수 있습니다.

이제는 컴퓨터 메모리 측면에서 복잡한 유형의 값에 대해 매번 새로운 값을 생성하는 것이 엄청나게 낭비적인 것처럼 보이지만 반드시 그렇게 할 필요는 없습니다. 만약 당신이 f (x) = y를 가지고 있고 값 x와 y가 "대부분 동일하다"(예를 들어 몇 잎에서만 다른 나무들)라면, x와 y는 메모리의 일부를 공유 할 수 있습니다. .

따라서이 변경 불가능한 것이 너무 크다면 왜 내가 변경 가능한 상태없이 유용한 것을 할 수 없다고 대답 했습니까? 음, 변경이 없으면 전체 프로그램은 거대한 f (x) = y 함수가됩니다. 그리고 프로그램의 모든 부분에서도 마찬가지입니다. 단지 "순수한"의미의 함수와 함수입니다. 내가 말했듯이, 이것은 매번 f (x) = y를 의미 합니다. 따라서 readFile ( "myFile.txt")은 매번 같은 문자열 값을 반환해야합니다. 너무 유용하지 않습니다.

따라서 모든 FP는 상태를 변경 하는 몇 가지 수단을 제공합니다 . "순수한"기능 언어 (예 : Haskell)는 모나드와 같은 다소 무서운 개념을 사용하여이 작업을 수행하지만 "불순한"언어 (예 : ML)는이를 직접 허용합니다.

물론 함수형 언어에는 일류 함수 등과 같이 프로그래밍을 더욱 효율적으로 만드는 다른 많은 장점이 있습니다.


2
<< readFile ( "myFile.txt")는 매번 같은 문자열 값을 반환해야합니다. 너무 유용하지는 않습니다. >> 전역 파일 시스템을 숨기면 유용하다고 생각합니다. 두 번째 매개 변수로 간주하고 다른 프로세스가 filesystem2 = write (filesystem1, fd, pos, "string")로 파일 시스템을 수정할 때마다 파일 시스템에 대한 새 참조를 리턴하게하고 모든 프로세스가 파일 시스템에 대한 참조를 교환하게 할 경우 운영 체제를 훨씬 더 깨끗하게 파악할 수 있습니다.
eel ghEEz

@eelghEEz, 이것은 Datomic이 데이터베이스에 취하는 것과 동일한 접근 방식입니다.
Jason

1
패러다임 사이의 명확하고 간결한 비교를 위해 +1. 한 가지 제안은 int double(x){ return x * (++y); }아직 광고되지 않은 부작용이 있지만 현재는 여전히 4 일 것이기 때문에 ++y6을 반환합니다.
BrainFRZ

@eelghEEz 대안이 확실하지 않습니다. 실제로 다른 사람이 있습니까? (순수한) FP 컨텍스트에 정보를 도입하려면 "타임 스탬프 X에서 온도는 Y"인 "측정"을 수행하십시오. 누군가가 온도를 묻는다면, 암시 적으로 X = 현재를 의미 할 수도 있지만, 온도를 보편적 인 시간 함수로 요구할 수는 없습니다. FP는 불변 상태를 처리하므로 내부 외부 소스 에서 불변 상태를 변경 가능 상태로 만들어야합니다 . 인덱스, 타임 스탬프 등은 유용하지만 VCS가 버전 제어 자체와 같이 변경 가능성에 직교합니다.
John P

29

함수형 프로그래밍에 '상태'가 없다고 말하는 것은 약간 오해의 소지가 있으며 혼란의 원인이 될 수 있습니다. 분명히 '변경 가능한 상태'는 없지만 조작되는 값을 여전히 가질 수 있습니다. 그것들은 제자리에서 변경 될 수 없습니다 (예를 들어, 이전 값에서 새로운 값을 생성해야합니다).

이것은 과도하게 단순화되었지만 클래스의 모든 속성이 생성자에서만 한 번 설정되고 모든 메소드가 정적 함수 인 OO 언어가 있다고 상상해보십시오. 메소드가 계산에 필요한 모든 값을 포함하는 오브젝트를 가져 와서 결과와 함께 새 오브젝트를 리턴하도록하여 거의 모든 계산을 수행 할 수 있습니다 (같은 오브젝트의 새 인스턴스 일 수도 있음).

기존 코드를이 패러다임으로 변환하는 것은 '어려운'일 수 있지만 코드에 대해 완전히 다른 사고 방식이 필요하기 때문입니다. 부작용으로 대부분의 경우 무료로 병렬 처리를 할 수 있습니다.

부록 : (변경 해야하는 값을 추적하는 방법에 대한 편집 내용)
불변의 데이터 구조에 저장됩니다 ...

이것은 제안 된 '솔루션'이 아니지만 이것이 항상 효과가 있음을 확인하는 가장 쉬운 방법은 이러한 불변 값을 '변수 이름'으로 키가 지정된 구조와 같은 맵 (사전 / 해시 테이블)에 저장할 수 있다는 것입니다.

분명히 실제 솔루션에서는보다 깔끔한 접근 방식을 사용하지만 다른 방법으로 아무것도 작동하지 않으면 호출 트리를 통해 이동하는 맵으로 변경 가능한 상태를 '시뮬레이션'할 수있는 최악의 경우를 보여줍니다.


2
좋아, 제목을 변경했습니다. 그래도 대답은 더 나쁜 문제로 이어질 것 같습니다. 상태가 변경 될 때마다 모든 객체를 재생성해야하는 경우, 모든 CPU 시간을 사용하여 객체를 구성하는 것 외에는 아무것도하지 않습니다. 여기서 게임 프로그래밍에 대해 생각하고 있습니다. 여기서 화면 (및 화면 밖)에서 많은 것들이 움직이며 서로 상호 작용할 수 있어야합니다. 전체 엔진은 정해진 프레임 속도를 가지고 있습니다 : 당신이 할 모든 것, 당신은 X 수 밀리 초 안에해야합니다. 지속적으로 전체 객체를 재활용하는 것보다 더 좋은 방법이 있습니까?
메이슨 휠러

4
그것의 아름다움은 불변성이 구현이 아니라 언어에 있다는 것입니다. 몇 가지 트릭을 사용하면 구현 상태가 실제로 변경되는 동안 언어로 상태를 변경할 수 없습니다. 예를 들어 Haskell의 ST 모나드를 참조하십시오.
CesarB 2016 년

4
@Mason : 컴파일러가 상태를 변경하는 것보다 (스레드) 안전한 위치를 훨씬 더 잘 결정할 수 있다는 것이 요점입니다.
jerryjvl

게임의 경우 속도가 중요하지 않은 부분에 대해서는 불변을 피해야한다고 생각합니다. 불변 언어는 최적화 할 수 있지만 CPU가 빠른 메모리를 수정하는 것보다 더 빠른 것은 없습니다. 따라서 명령이 필요한 10 또는 20 곳이있는 것으로 판명되면 게임 메뉴와 같이 매우 분리 된 영역에 대해 모듈화 할 수 없다면 불변을 피해야한다고 생각합니다. 특히 게임 로직은 비즈니스 규칙과 같은 순수한 시스템의 복잡한 모델링에 적합하다고 생각하기 때문에 불변의 사용하기 좋은 곳이 될 수 있습니다.
LegendLength

@LegendLength 당신은 자신을 모순합니다.
Ixx

18

약간의 오해가 있다고 생각합니다. 순수한 기능성 프로그램에는 상태가 있습니다. 차이점은 해당 상태가 모델링되는 방식입니다. 순수한 함수형 프로그래밍에서 상태는 어떤 상태를 취하고 다음 상태를 반환하는 함수에 의해 조작됩니다. 그런 다음 상태를 순차 화하여 순차 함수 시퀀스를 통해 상태를 전달합니다.

전역 변경 가능 상태조차도 이런 식으로 모델링 할 수 있습니다. 예를 들어, Haskell에서 프로그램은 월드에서 월드로의 함수입니다. 즉, 전체 유니버스 를 전달 하면 프로그램이 새 유니버스를 반환합니다. 하지만 실제로는 실제로 프로그램에 관심이있는 우주의 일부만 통과하면됩니다. 또한 프로그램은 실제로 프로그램이 실행되는 운영 환경에 대한 지침으로 사용 되는 일련의 작업 을 반환 합니다.

명령형 프로그래밍의 관점에서 이것을 설명하고 싶었습니다. 자, 기능적 언어로 된 아주 간단한 명령형 프로그래밍을 살펴 봅시다.

이 코드를 고려하십시오.

int x = 1;
int y = x + 1;
x = x + y;
return x;

꽤 멍청한 표준 명령 코드입니다. 흥미로운 것은 없지만 설명하기에는 괜찮습니다. 여기에 관련된 주가 있다는 데 동의 할 것입니다. x 변수의 값은 시간이 지남에 따라 변경됩니다. 이제 새로운 구문을 발명하여 표기법을 약간 변경해 보겠습니다.

let x = 1 in
let y = x + 1 in
let z = x + y in z 

이것이 의미하는 바를 명확하게하기 위해 괄호를 넣으십시오.

let x = 1 in (let y = x + 1 in (let z = x + y in (z)))

따라서 상태는 다음 표현식의 자유 변수를 바인딩하는 일련의 순수 표현식으로 모델링됩니다.

이 패턴은 IO를 포함한 모든 종류의 상태를 모델링 할 수 있습니다.


그런거 모나드 같아요?
CMCDragonkai

이것을 고려하겠습니까 : A는 레벨 1에서 선언적입니다. B는 레벨 2에서 선언적입니다. A는 명령적인 것으로 간주됩니다. C는 레벨 3에서 선언적이며 B는 명령적인 것으로 간주합니다. 추상화 계층을 늘리면 항상 추상화 계층의 하위 언어가 그 자체보다 더 중요하다고 생각합니다.
CMCDragonkai

14

변경 가능한 상태없이 코드를 작성하는 방법은 다음과 같습니다. 변경 가능한 상태를 변경 가능한 변수에 넣는 대신 함수의 매개 변수에 넣습니다. 루프를 작성하는 대신 재귀 함수를 작성합니다. 예를 들어이 명령 코드는 다음과 같습니다.

f_imperative(y) {
  local x;
  x := e;
  while p(x, y) do
    x := g(x, y)
  return h(x, y)
}

이 기능 코드가됩니다 (스키마와 유사한 구문).

(define (f-functional y) 
  (letrec (
     (f-helper (lambda (x y)
                  (if (p x y) 
                     (f-helper (g x y) y)
                     (h x y)))))
     (f-helper e y)))

또는이 Haskellish 코드

f_fun y = h x_final y
   where x_initial = e
         x_final   = loop x_initial
         loop x = if p x y then loop (g x y) else x

함수형 프로그래머가 (그렇지 않은)이 작업 을 선호 하는 이유에 관해서는 프로그램의 더 많은 부분이 무국적 일수록 더 많은 부분을 나누지 않고 조각을 모을 수있는 더 많은 방법이 있습니다 . 무국적 패러다임의 힘은 무국적 (또는 순도) 자체 가 아니라 강력하고 재사용 가능한 기능 을 작성 하고 결합 할 수있는 능력 입니다.

John Hughes의 논문 Why Functional Programming Matters 에서 많은 예제가 포함 된 유용한 자습서를 찾을 수 있습니다 .


13

똑같은 일을하는 다른 방법 일뿐입니다.

숫자 3, 5 및 10을 추가하는 것과 같은 간단한 예를 생각해보십시오. 먼저 5를 추가하여 3의 값을 변경 한 다음 10을 해당 "3"에 추가 한 다음 " 3 "(18). 이것은 말도 안되는 것처럼 보이지만 본질적으로 상태 기반 명령 프로그래밍이 종종 이루어지는 방식입니다. 실제로, 값이 3이지만 아직 다른 많은 "3"을 가질 수 있습니다. 이 모든 것이 이상하게 보입니다. 왜냐하면 우리는 숫자가 불변이라는 매우 엄청나게 합리적인 생각에 깊이 뿌리 박혀 있기 때문입니다.

이제 값을 변경할 수 없을 때 3, 5 및 10을 추가하는 것을 고려하십시오. 3과 5를 더하여 다른 값 8을 생성 한 다음 해당 값에 10을 더하여 또 다른 값 18을 생성합니다.

이것들은 같은 일을하는 동등한 방법입니다. 필요한 정보는 모두 두 가지 방법으로 존재하지만 다른 형식으로 존재합니다. 정보는 상태로 존재하고 상태를 변경하기위한 규칙에 있습니다. 다른 한편으로, 정보는 불변 데이터 및 기능적 정의에 존재한다.


10

토론에 늦었지만 기능 프로그래밍에 어려움을 겪고있는 사람들에게 몇 가지 요점을 추가하고 싶었습니다.

  1. 함수형 언어는 명령형 언어와 동일한 상태 업데이트를 유지하지만 업데이트 된 상태를 후속 함수 호출로 전달하여 업데이트합니다 . 다음은 숫자 줄을 따라 이동하는 매우 간단한 예입니다. 주는 현재 위치입니다.

첫 번째 필수 방식 (의사 코드)

moveTo(dest, cur):
    while (cur != dest):
         if (cur < dest):
             cur += 1
         else:
             cur -= 1
    return cur

이제 기능적 방식 (의사 코드). 필연적 인 배경을 가진 사람들이 실제로이 코드를 읽을 수 있기를 원하기 때문에 삼항 연산자에 크게 기울고 있습니다. 따라서 삼항 연산자를 많이 사용하지 않으면 (필자는 항상 필연적 인 일을 피했습니다) 작동 방식은 다음과 같습니다.

predicate ? if-true-expression : if-false-expression

허위 표현식 대신 새로운 삼항 표현식을 넣어 삼항 표현식을 연결할 수 있습니다.

predicate1 ? if-true1-expression :
predicate2 ? if-true2-expression :
else-expression

이를 염두에두고 기능 버전이 있습니다.

moveTo(dest, cur):
    return (
        cur == dest ? return cur :
        cur < dest ? moveTo(dest, cur + 1) : 
        moveTo(dest, cur - 1)
    )

이것은 간단한 예입니다. 이것이 게임 세계에서 사람들을 움직이고 있다면, 화면에 객체의 현재 위치를 그리는 것과 객체가 얼마나 빨리 움직이는 지에 따라 각 호출에서 약간의 지연을 일으키는 것과 같은 부작용을 가져와야합니다. 그러나 여전히 변경 가능한 상태가 필요하지 않습니다.

  1. 교훈은 기능 매개 변수가 다른 매개 변수로 함수를 호출하여 기능 언어가 "돌연변이"상태라는 것입니다. 분명히 이것은 실제로 변수를 변경하지는 않지만 비슷한 효과를 얻는 방법입니다. 이것은 함수형 프로그래밍을하고 싶다면 재귀 적으로 생각하는 것에 익숙해 져야한다는 것을 의미합니다.

  2. 재귀 적으로 생각하는 법을 배우는 것은 어렵지 않지만 연습과 툴킷이 모두 필요합니다. 재귀를 사용하여 계승을 계산 한 "학습 Java"책의 작은 부분은 그것을 자르지 않습니다. 재귀에서 반복적 인 프로세스를 만드는 것과 같은 기능의 툴킷이 필요합니다 (이것은 함수 재귀에 꼬리 재귀가 필수적인 이유입니다), 연속, 불변 등입니다. 액세스 수정 자, 인터페이스 등에 대한 학습없이 OO 프로그래밍을 수행하지 않을 것입니다. 기능적 프로그래밍.

Little Schemer를 추천하고 ( "읽지"말고 "읽지"말고) SICP의 모든 연습을하는 것이 좋습니다. 완료되면 시작했을 때와 다른 두뇌를 ​​갖게됩니다.


8

가변 상태가없는 언어에서도 가변 상태처럼 보이는 것을 갖는 것은 실제로 쉽습니다.

type 함수를 고려하십시오 s -> (a, s). Haskell 구문에서 번역하면 " s" 유형의 매개 변수 하나를 사용 하고 " a"및 " s" 유형의 값 쌍을 리턴하는 함수를 의미합니다 . 경우 s우리의 국가의 유형이,이 기능은 하나 개의 상태 및 반품 새로운 상태를 취하고, 가능한 값이 (당신은 항상 일명 "단위"를 반환 할 수 있습니다 ()종류 "에 해당의이다 void"는로 C / C ++로 "를 a" 유형). 이와 같은 유형으로 여러 함수 호출을 연결하면 (한 함수에서 반환 된 상태를 가져오고 다음 함수로 전달) "변경 가능"상태가됩니다 (사실 각 함수에서 새 상태를 작성하고 이전 상태를 포기 함) ).

변경 가능한 상태를 프로그램이 실행중인 "공간"으로 생각한 다음 시간 차원을 생각하면 이해하기가 더 쉬울 수 있습니다. 순간 t1에서, "공간"은 특정 조건에있다 (예를 들어, 일부 메모리 위치는 5의 값을 갖는다). 나중에 순간 t2에서, 그것은 다른 조건에있다 (예를 들어, 메모리 위치는 이제 10의 값을 갖는다). 각각의 "슬라이스"는 상태이며 변경할 수 없습니다 (시간을 되돌려 변경할 수는 없습니다). 따라서이 시점에서 시간 화살표 (변경 가능 상태)가있는 전체 시공간에서 시공간 조각 세트 (여러 개의 불변 상태)로 이동했으며 프로그램은 각 슬라이스를 값으로 처리하고 각각을 계산합니다. 그중 하나는 이전에 적용된 함수로 사용됩니다.

좋아, 아마 이해하기 쉽지 않을 것입니다 :-)

전체 프로그램 상태를 값으로 명시 적으로 표현하는 것은 비효율적 일 수 있습니다.이 값은 다음 순간을 버릴 때만 작성해야합니다 (새 상태가 작성된 직후). 일부 알고리즘의 경우 자연 스럽지만 그렇지 않은 경우 또 다른 트릭이 있습니다. 실제 상태 대신 마커에 불과한 가짜 상태를 사용할 수 있습니다 (이 가짜 상태의 유형이라고하자 State#). 이 가짜 상태는 언어의 관점에서 존재하며 다른 값과 같이 전달되지만 기계 코드를 생성 할 때 컴파일러는이를 완전히 생략합니다. 실행 순서를 표시하는 역할 만합니다.

예를 들어, 컴파일러가 다음과 같은 기능을 제공한다고 가정합니다.

readRef :: Ref a -> State# -> (a, State#)
writeRef :: Ref a -> a -> State# -> (a, State#)

이러한 Haskell과 같은 선언에서 번역하여 readRef" a" 유형의 값에 대한 포인터 또는 핸들과 유사한 무언가 및 위조 상태를 수신하고 a첫 번째 매개 변수와 새 가짜 상태가 가리키는 " " 유형의 값을 리턴합니다 . writeRef비슷하지만 대신 가리키는 값을 변경합니다.

호출 readRef한 다음 반환 한 가짜 상태를 전달하면 writeRef(아마도 중간에 관련없는 함수에 대한 다른 호출이있을 수 있습니다. 이러한 상태 값은 함수 호출의 "체인"을 생성합니다) 작성된 값을 반환합니다. writeRef동일한 포인터 / 핸들로 다시 호출 할 수 있으며 동일한 메모리 위치에 기록합니다. 그러나 개념적으로 새로운 (가짜) 상태를 반환하기 때문에 (가짜) 상태는 여전히 변경 불가능합니다 (새로운 상태는 "만들어졌습니다" "). 컴파일러는 계산해야하는 실제 상태 변수가 있지만 실제 하드웨어의 전체 (변경 가능) 상태 만있는 경우 호출해야하는 순서대로 함수를 호출합니다.

(하스켈을 알고있는 사람들은 좀 봐, 원하는 사람들이 자세한 내용을 볼 수 있도록. 나는 몇 가지 중요한 세부 사항을 일을 많이 단순화 ommited 알 Control.Monad.State로부터 mtl, 그리고에서 ST sIO(일명 ST RealWorld) 모나드.)

단순히 언어로 변경 가능한 상태를 유지하는 대신 왜 이런 방식으로 진행하는지 궁금 할 것입니다. 실제 장점은 프로그램 상태를 구체화 했다는 것 입니다. 이전에 암시 된 것 (프로그램 상태가 전역 적이므로 먼 거리에서 동작 하는 것을 허용 )이 이제 명확 해졌습니다. 상태를 수신 및 반환하지 않는 함수는 상태를 수정하거나 영향을받을 수 없습니다. 그들은 "순수하다". 더 나은 방법은 별도의 상태 스레드를 가질 수 있으며 약간의 마법 유형을 사용하면 순전 한 연산 내에 명령형 계산을 포함시키지 않고 불완전하게 만들 수 있습니다 ( ST하스켈 의 모나드는이 트릭에 일반적으로 사용되는 것입니다. State#제가 위에서 언급 한 사실 GHC의에 State# s의 구현에 의해 사용, STIO 모나드).


7

함수형 프로그래밍 상태를 피하고 강조기능. 상태가 실제로는 불변하거나 작업중 인 아키텍처에 구워진 것일 수도 있지만 상태가없는 것과 같은 것은 없습니다. 파일 시스템에서 파일을로드하는 정적 웹 서버와 Rubik의 큐브를 구현하는 프로그램의 차이점을 고려하십시오. 전자는 요청을 파일 경로 요청으로 변환하고 해당 파일의 내용에 대한 응답으로 변환하도록 설계된 기능 측면에서 구현됩니다. 약간의 구성을 넘어서는 사실상 상태가 필요하지 않습니다 (파일 시스템 '상태'는 실제로 프로그램 범위를 벗어납니다. 프로그램은 파일의 상태에 관계없이 동일한 방식으로 작동합니다). 후자의 경우 큐브를 모델링하고 해당 큐브의 조작이 상태를 변경하는 방법에 대한 프로그램 구현을 모델링해야합니다.


내가 더 기능이 떨어졌을 때 나는 하드 드라이브와 같은 것이 변할 때 그것이 어떻게 좋을지 궁금해했다. 내 C # 클래스는 모두 변경 가능한 상태였으며 하드 드라이브 나 다른 장치를 논리적으로 시뮬레이션 할 수있었습니다. 기능적으로는 모델과 모델링 한 실제 기계간에 불일치가있었습니다. 기능에 대해 더 깊이 파고 들고 나면 당신이 얻는 이점이 그 문제보다 훨씬 중요하다는 것을 깨닫게되었습니다. 그리고 실제로 사본을 만든 하드 드라이브를 발명 할 수 있다면 실제로 유용합니다 (저널링이 이미하는 것처럼).
LegendLength

5

다른 사람들이 제공하는 큰 대답 외에도 클래스 IntegerStringJava를 생각하십시오 . 이러한 클래스의 인스턴스는 변경할 수 없지만 인스턴스를 변경할 수 없어 클래스를 쓸모 없게 만듭니다. 불변성은 안전을 제공합니다. String 또는 Integer 인스턴스를의 키로 사용하면 키를 Map변경할 수 없습니다. 이것을 DateJava 클래스와 비교하십시오 .

Date date = new Date();
mymap.put(date, date.toString());
// Some time later:
date.setTime(new Date().getTime());

지도에서 키를 자동으로 변경했습니다! 함수형 프로그래밍에서와 같이 불변의 객체로 작업하는 것이 훨씬 깨끗합니다. 어떤 부작용이 발생했는지 쉽게 추론 할 수 있습니다. 이것은 프로그래머에게는 더 쉽고 옵티 마이저에게는 더 쉽다는 것을 의미합니다.


2
나는 그것을 이해하지만 내 질문에 대답하지는 않습니다. 컴퓨터 프로그램은 실제 이벤트 나 프로세스의 모델이라는 점을 명심하십시오. 값을 변경할 수없는 경우 변경되는 것을 어떻게 모델링합니까?
메이슨 휠러

글쎄, 당신은 확실히 Integer와 String 클래스로 유용한 일을 할 수 있습니다. 불변성이 아니기 때문에 변경 가능한 상태를 가질 수 없습니다.
Eddie

@Mason Wheeler-한 가지와 상태가 서로 다른 "사물"이라는 것을 이해함으로써. 팩맨은 A에서 B로 바뀌지 않습니다. 팩맨은 어디에서 변경됩니까? 시간 A에서 시간 B로 이동하면 동일한 팩맨, 다른 상태 인 pacman + state의 새로운 조합을 얻게됩니다. 상태가 변경되지 않았습니다 ... 다른 상태입니다.
RHSeeger

4

게임과 같은 대화 형 응용 프로그램의 경우 Functional Reactive Programming 은 친구입니다. 게임 세계의 속성을 시변 값 (및 / 또는 이벤트 스트림)으로 공식화 할 수 있다면 준비가 된 것입니다! 이러한 공식은 때때로 상태를 변경하는 것보다 훨씬 더 자연스럽고 의도적으로 드러날 것입니다. 예를 들어 움직이는 공의 경우 잘 알려진 법칙 x = v * t를 직접 사용할 수 있습니다 . 더 좋은 점은, 이런 방식으로 작성된 게임 규칙 은 객체 지향 추상화보다 더 잘 작성 됩니다. 예를 들어,이 경우, 공의 속도는 또한 시변 값일 수 있으며, 이는 공의 충돌로 구성된 이벤트 스트림에 따라 다릅니다. 보다 구체적인 디자인 고려 사항 은 Elm에서 게임 만들기를 참조하십시오 .


4

3

이것이 FORTRAN이 COMMON 블록없이 작동하는 방식입니다. 전달한 값을 가진 메소드와 로컬 변수를 작성합니다. 그게 다야.

객체 지향 프로그래밍은 우리에게 상태와 행동을 함께 가져 왔지만 1994 년 C ++에서 처음 접했을 때 새로운 아이디어였습니다.

Geez, 저는 기계 엔지니어 였을 때 기능 프로그래머였습니다.


2
나는 이것이 당신이 OO에 고정시킬 수있는 것에 동의하지 않습니다. OO 이전의 언어는 결합 상태와 알고리즘을 권장했습니다. OO는 더 나은 관리 방법을 제공했습니다.
Jason Baker

"장려"-아마도. OO는 언어의 명시적인 부분으로 만듭니다. C로 캡슐화 및 정보 숨기기를 수행 할 수는 있지만 OO 언어를 사용하면 훨씬 쉽게 만들 수 있습니다.
duffymo 2016 년

2

명심하십시오 : 기능적인 언어는 튜링 완료되었습니다. 따라서 명령형 언어로 수행하는 유용한 작업은 기능적 언어로 수행 할 수 있습니다. 그러나 마지막 날에는 하이브리드 방식에 대해 이야기 할 것이 있다고 생각합니다. F # 및 Clojure와 같은 언어 (및 다른 언어도 확실 함)는 상태 비 저장 디자인을 권장하지만 필요한 경우 가변성을 허용합니다.


두 언어가 Turing complete라고해서 동일한 작업을 수행 할 수있는 것은 아닙니다. 이는 동일한 계산을 수행 할 수 있다는 의미입니다. Brainfuck은 Turing이 완료되었지만 TCP 스택을 통해 통신 할 수 없다고 확신합니다.
RHSeeger

2
물론 가능합니다. C와 동일한 하드웨어 액세스가 가능하다면 가능합니다. 그것이 실용적이라는 것을 의미하는 것은 아니지만 가능성이 있습니다.
Jason Baker

2

유용한 기능적 언어를 사용할 수 없습니다. 항상 다루어야 할 가변성 수준이있을 것입니다. IO가 한 예입니다.

기능적 언어를 사용하는 다른 도구로 생각하십시오. 특정 것들에는 좋지만 다른 것들에는 좋지 않습니다. 귀하가 제공 한 게임 예제는 기능적 언어를 사용하는 가장 좋은 방법이 아닐 수 있습니다. 최소한 화면에는 FP로 아무 것도 할 수없는 변경 가능한 상태가됩니다. 문제를 생각하는 방식과 FP로 해결하는 문제의 유형은 명령 프로그래밍에 익숙한 것과 다릅니다.



-3

이것은 매우 간단합니다. 함수형 프로그래밍에서 원하는만큼의 변수를 사용할 수 있지만 로컬 변수 (함수 내에 포함 된) 인 경우에만 사용할 수 있습니다 . 따라서 코드를 함수로 감싸고, 전달 된 매개 변수 및 반환 값으로 해당 함수 사이에서 값을 앞뒤로 전달하면됩니다. 그게 전부입니다!

예를 들면 다음과 같습니다.

function ReadDataFromKeyboard() {
    $input_values = $_POST[];
    return $input_values;
}
function ProcessInformation($input_values) {
    if ($input_values['a'] > 10)
        return ($input_values['a'] + $input_values['b'] + 3);
    else if ($input_values['a'] > 5)
        return ($input_values['b'] * 3);
    else
        return ($input_values['b'] - $input_values['a'] - 7);
}
function DisplayToPage($data) {
    print "Based your input, the answer is: ";
    print $data;
    print "\n";
}

/* begin: */
DisplayToPage (
    ProcessInformation (
        GetDataFromKeyboard()
    )
);

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