할당 문없이 순수한 함수형 프로그래밍 언어를 관리하는 방법


26

유명한 SICP를 읽을 때 저자들은 3 장에서 Scheme에 과제 진술을 소개하는 것을 꺼려하는 것으로 나타났습니다. 나는 그들이 왜 그렇게 느끼는지 텍스트와 종류를 읽었습니다.

Scheme은 내가 아는 첫 번째 기능 프로그래밍 언어이기 때문에 과제없이 Scheme이 아닌 일부 기능 프로그래밍 언어가 있다는 사실에 놀랐습니다.

책이 제공하는 예, 예를 사용하십시오 bank account. 할당 문이 없으면 어떻게 할 수 있습니까? balance변수 를 변경하는 방법은 무엇입니까? 나는 거기에 소위 순수한 기능적 언어가 있다는 것을 알고 있기 때문에 묻습니다. 그리고 튜링 완전 이론에 따르면, 이것도 역시 가능합니다.

나는 C, Java, Python을 배웠고 내가 작성한 모든 프로그램에서 과제를 많이 사용했습니다. 정말 눈에 띄는 경험입니다. 나는 함수형 프로그래밍 언어에서 할당을 피하는 방법과 이러한 언어에 미치는 영향에 대해 간단히 설명 할 수 있기를 바랍니다.

위에서 언급 한 예는 다음과 같습니다.

(define (make-withdraw balance)
    (lambda (amount)
        (if (>= balance amount)
            (begin (set! balance (- balance amount))
                balance)
            "Insufficient funds")))

이로 변경 balance되었습니다 set!. 나에게 그것은 클래스 멤버를 변경하는 클래스 메소드와 매우 비슷해 보인다 balance.

내가 말했듯이, 나는 함수형 프로그래밍 언어에 익숙하지 않기 때문에 그것에 대해 잘못 말하면 자유롭게 지적하십시오.


1
순전히 기능적인 언어를 배우는 것과 관련하여 : 나는 그것을 즉시하는 것을 권장하지는 않습니다. Haskell을 배우면 가변 변수없이 프로그램을 작성하는 방법을 배우는 것 외에도 게으름과 Haskell의 IO 수행 방법에 대해서도 배워야합니다. 그것은 한 번에 조금 많을 수도 있습니다. 변경 가능한 상태없이 프로그램을 작성하는 방법을 배우려면 가장 쉬운 방법은로 set!끝나거나 다른 함수를 사용하지 않고 여러 가지 계획 프로그램을 작성하는 것입니다 !. 일단 익숙해지면 순수한 FP로 쉽게 전환 할 수 있습니다.
sepp2k

답변:


21

할당 문이 없으면 어떻게 할 수 있습니까? 잔액 변수를 변경하는 방법은 무엇입니까?

할당 연산자가 없으면 변수를 변경할 수 없습니다.

나는 거기에 소위 순수한 기능적 언어가 있다는 것을 알고 있기 때문에 묻습니다. 그리고 튜링 완전 이론에 따르면, 이것도 역시 가능합니다.

좀 빠지는. 언어가 튜링 완료 인 경우 다른 튜링 완료 언어가 계산할 수있는 모든 것을 계산할 수 있음을 의미합니다. 다른 언어의 모든 기능을 갖추어야한다는 의미는 아닙니다.

가변 변수가있는 모든 프로그램에 대해 가변 변수가없는 동등한 프로그램을 작성할 수있는 한, Turing 완전한 프로그래밍 언어가 변수의 값을 변경할 수있는 방법이 없다는 것은 모순이 아닙니다. 같은 것을 계산합니다). 사실 모든 프로그램은 그런 식으로 작성 될 수 있습니다.

예제와 관련하여 : 순전히 기능적인 언어에서는 호출 될 때마다 다른 계정 잔액을 반환하는 함수를 작성할 수 없습니다. 그러나 여전히 그러한 기능을 사용하는 모든 프로그램을 다른 방식으로 다시 작성할 수 있습니다.


예제를 요청했기 때문에 make-withdraw 함수 (의사 코드)를 사용하는 명령형 프로그램을 고려해 봅시다. 이 프로그램을 통해 사용자는 계좌에서 인출하거나 계좌에 입금하거나 계좌의 금액을 조회 할 수 있습니다.

account = make-withdraw(0)
ask for input until the user enters "quit"
    if the user entered "withdraw $x"
        account(x)
    if the user entered "deposit $x"
        account(-x)
    if the user entered "query"
        print("The balance of the account is " + account(0))

다음은 가변 변수를 사용하지 않고 동일한 프로그램을 작성하는 방법입니다 (질문에 관한 것이 아니기 때문에 참조 가능한 투명한 IO를 신경 쓰지 않을 것입니다).

function IO_loop(balance):
    ask for input
    if the user entered "withdraw $x"
        IO_loop(balance - x)
    if the user entered "deposit $x"
        IO_loop(balance + x)
    if the user entered "query"
        print("The balance of the account is " + balance)
        IO_loop(balance)
    if the user entered "quit"
        do nothing

 IO_loop(0)

사용자 입력에 폴드를 사용하여 재귀를 사용하지 않고 동일한 함수를 작성할 수도 있지만 (명시 적 재귀보다 관용적이지만) 폴드에 아직 익숙하지 않은지 모르겠습니다. 아직 모르는 것을 사용하지 않는 방법.


요점을 볼 수는 있지만 은행 계좌를 시뮬레이트하고 이러한 일을 할 수있는 프로그램 (출금 및 입금)을 원합니다.이를 수행하는 쉬운 방법이 있습니까?
Gnijuohz

@Gnijuohz 항상 정확하게 해결하려는 문제에 달려 있습니다. 예를 들어, 시작 잔고와 인출 및 예금 목록이 있고 그 인출 및 예금 후 잔액을 알고 싶다면 예금의 합계에서 인출의 합계를 뺀 값을 시작 잔액에 추가하면됩니다. . 따라서 코드에서는입니다 newBalance = startingBalance + sum(deposits) - sum(withdrawals).
sepp2k

1
@Gnijuohz 내 대답에 예제 프로그램을 추가했습니다.
sepp2k

답변을 작성하고 다시 작성하는 시간과 노력에 감사드립니다! :)
Gnijuohz

나는 연속을 사용하는 것이 계획에서 그것을 달성하는 수단이 될 수 있다고 덧붙일 것입니다 (연속에 인수를 전달할 수 있습니까?)
dader51

11

객체의 메소드와 매우 비슷하다는 것이 맞습니다. 그것은 본질적으로 그것이 기 때문입니다. 이 lambda함수는 외부 변수 balance를 해당 범위로 가져 오는 클로저입니다 . 동일한 외부 변수를 닫는 여러 개의 클로저가 있고 동일한 객체에 여러 개의 메서드가있는 것은 정확히 같은 일을 수행하기위한 두 가지 다른 추상화이며 두 패러다임을 이해하면 하나는 다른 측면에서 구현 될 수 있습니다.

순수한 기능적 언어가 상태를 처리하는 방법은 부정 행위입니다. 예를 들어 Haskell에서 외부 소스의 입력을 읽으려면 (물론 결정적이지 않으며 반복해도 동일한 결과를 두 번 제공하지는 않습니다) 모나드 트릭을 사용하여 "우리는 나머지 세계 전체의 상태 를 나타내는 다른 척 변수를 얻었고 직접 검사 할 수는 없지만 입력을 읽는 것은 외부 세계의 상태를 취하여 정확한 상태의 결정적 입력을 반환하는 순수한 함수입니다. 항상 외부 세계의 새로운 상태와 함께 렌더링됩니다. " (물론 그것은 간단한 설명입니다. 실제로 작동하는 방식을 읽으면 뇌가 심각하게 손상 될 것입니다.)

또는 은행 계좌 문제의 경우 변수에 새 값을 할당하는 대신 새 값을 함수 결과로 반환 할 수 있으며 호출자는 일반적으로 데이터를 다시 작성하여 기능적 스타일로 처리해야합니다. 업데이트 된 값이 포함 된 새 버전으로 해당 값을 참조합니다. (이것은 데이터가 올바른 종류의 트리 구조로 설정된 경우 들리는 것처럼 큰 작업은 아닙니다.)


나는 우리의 대답과 Haskell의 예에 정말로 관심이 있지만 그것에 대한 지식이 없기 때문에 대답의 마지막 부분을 완전히 이해할 수 없습니다 (음, 두 번째 부분 :()
Gnijuohz

3
@Gnijuohz 마지막 단락 은 단순히 대신로 정의 된 위치를 b = makeWithdraw(42); b(1); b(2); b(3); print(b(4))수행 할 수 있다고 말합니다 . b = 42; b1 = withdraw(b1, 1); b2 = withdraw(b1, 2); b3 = withdraw(b2, 3); print(withdraw(b3, 4));withdrawwithdraw(balance, amount) = balance - amount
sepp2k

3

"다중 할당 연산자"는 일반적으로 말해서 부작용이 있으며 기능적 언어의 일부 유용한 속성 (예 : 지연 평가)과 호환되지 않는 언어 기능의 한 예입니다.

그렇다고해서 일반적으로 할당이 순수한 함수형 프로그래밍 스타일과 호환되지 않는다는 의미는 아니며 ( 예를 들어이 논의 참조 ), 일반적으로 할당처럼 보이는 동작을 허용하는 구문을 구성 할 수는 없지만 부작용없이 구현됩니다. 그래도 이런 종류의 구문을 작성하고 효율적인 프로그램을 작성하는 것은 시간이 많이 걸리고 어렵습니다.

구체적인 예에서 당신은 맞습니다-세트! 연산자 할당입니다. 부작용이없는 운영자 가 아니며 , 프로그래밍에 대한 순수하게 기능적인 접근 방식으로 Scheme이 중단되는 곳입니다.

궁극적으로, 어떤 순수하게 기능적인 언어는 순수하게 기능적인 접근 언젠가으로 중단해야 할 것입니다 - 유용한 프로그램의 대부분은 부작용이있다. 어디에서해야할지 결정하는 것은 보통 편의의 문제이며, 언어 디자이너는 프로그래머가 프로그램과 문제 영역에 적합한 순전히 기능적인 접근 방식으로 중단 할 위치를 결정할 때 프로그래머에게 최대한의 유연성을 제공하려고합니다.


"결국 순수한 기능 언어는 순수한 기능적 접근 방식으로 언젠가는 깨어 져야 할 것입니다. 유용한 프로그램의 대부분은 부작용이 있습니다."그러나 IO와 그와 관련하여 이야기하고 있습니다. 가변 변수없이 많은 유용한 프로그램을 작성할 수 있습니다.
sepp2k

1
... 그리고 유용한 프로그램의 "대부분"은 "모두"를 의미합니다. I / O를 수행하지 않는 "유용한"프로그램이라고 할 수있는 프로그램의 존재 가능성을 상상하기가 어렵습니다. 양방향으로 부작용이 필요한 작업입니다.
메이슨 휠러

@MasonWheeler SQL 프로그램은 IO를 수행하지 않습니다. REPL이있는 언어에서 IO를 수행하지 않고 단순히 REPL에서 해당 함수를 호출하는 많은 함수를 작성하는 것도 드문 일이 아닙니다. 이는 대상 고객이 REPL을 사용할 수있는 경우 (특히 대상 사용자가 자신 인 경우) 완벽하게 유용 할 수 있습니다.
sepp2k

1
@MasonWheeler : 단 하나의 간단한 카운터 예 : n 의 pi를 개념적으로 계산 하는 데 I / O가 필요하지 않습니다. "만"수학 및 변수입니다. 필요한 입력은 n 이고 반환 값은 Pi ( n 자리)입니다.
Joachim Sauer

1
@Joachim Sauer 결국 결과를 화면에 인쇄하거나 사용자에게보고하려고합니다. 그리고 처음에는 어떤 곳에서 상수를 프로그램에로드하려고합니다. 따라서, 만약 당신이 pedantic하고 싶다면, 모든 유용한 프로그램은 환경에 의해 프로그래머에게 암시적이고 항상 숨겨져있는 사소한 경우에도 IO를 수행해야합니다
blueberryfields

3

순전히 기능적인 언어에서는 은행 계좌 개체를 스트림 변환기 기능으로 프로그래밍합니다. 이 개체는 계정 소유자 (또는 누구든지)로부터 잠재적으로 무한한 응답 스트림에 이르기까지 무한한 요청 스트림에서 함수로 간주됩니다. 이 함수는 초기 잔액으로 시작하여 입력 스트림에서 각 요청을 처리하여 새 잔액을 계산 한 다음 재귀 호출로 피드백하여 나머지 스트림을 처리합니다. (SICP가이 책의 다른 부분에서 스트림 트랜스포머 패러다임을 논의한 것을 기억합니다.)

이 패러다임의 더 정교한 버전은 논의 "기능 반응 프로그래밍"라고 에 StackOverflow 여기에 .

스트림 트랜스포머를 수행하는 순진한 방법에는 몇 가지 문제가 있습니다. 공간을 낭비하면서 모든 기존 요청을 유지하는 버그가있는 프로그램을 작성할 수 있습니다 (사실, 매우 쉽습니다). 더 심각하게, 현재 요청에 대한 응답을 향후 요청에 따라 만들 수 있습니다. 이러한 문제에 대한 해결책은 현재 연구 중입니다. 닐 크리슈나 스와미 는 그들 뒤에있는 힘입니다.

면책 조항 : 나는 순수한 기능 프로그래밍 교회에 속하지 않습니다. 사실, 나는 어떤 교회에 속해 있지 않습니다 :-)


당신이 성전에 속해 있다고 생각합니까? - P
Gnijuohz

1
자유로운 사고의 사원. 설교자는 없습니다.
Uday Reddy

2

프로그램을 100 % 유용하게 만드는 것은 불가능합니다. 부작용이 필요하지 않으면 전체 생각이 일정한 컴파일 시간으로 줄어들 수 있습니다. 철회 예와 같이 대부분의 절차를 기능적으로 만들 수 있지만 결국 부작용이있는 절차가 필요합니다 (사용자의 입력, 콘솔로 출력). 즉, 대부분의 코드를 기능적으로 만들 수 있으며 해당 부분은 자동으로 쉽게 테스트 할 수 있습니다. 그런 다음 디버깅이 필요한 입 / 출력 / 데이터베이스 / ...를 수행하기 위해 명령 코드를 작성하지만 대부분의 코드를 깨끗하게 유지하면 너무 많은 작업이 필요하지 않습니다. 철회 예를 사용하겠습니다.

(define +no-founds+ "Insufficient funds")

;; functional withdraw
(define (make-withdraw balance amount)
    (if (>= balance amount)
        (- balance amount)
        +no-founds+))

;; functional atm loop
(define (atm balance thunk)
  (let* ((amount (thunk balance)) 
         (new-balance (make-withdraw balance amount)))
    (if (eqv? new-balance +no-founds+)
        (cons +no-founds+ '())
        (cons (list 'withdraw amount 'balance new-balance) (atm new-balance thunk)))))

;; functional balance-line -> string 
(define (balance->string x)
  (if (eqv? x +no-founds+)
      (string-append +no-founds+ "\n")
      (if (null? x)
          "\n"
          (let ((first-token (car x)))
            (string-append
             (cond ((symbol? first-token) (symbol->string first-token))
                   (else (number->string first-token)))
             " "
             (balance->string (cdr x)))))))

;; functional thunk to test  
(define (input-10 x) 10) ;; define a purly functional input-method

;; since all procedures involved are functional 
;; we expect the same result every time.
;; we use this to test atm and make-withdraw
(apply string-append (map balance->string (atm 100 input-10)))

;; no program can be purly functional in any language.
;; From here on there are imperative dirty procedures!

;; A procedure to get input from user is needed. 
;; Side effects makes it imperative
(define (user-input balance)
  (display "You have $")
  (display balance)
  (display " founds. How much to withdraw? ")
  (read))

;; We need a procedure to print stuff to the console 
;; as well. Side effects makes it imperative
(define (pretty-print-result x)
  (for-each (lambda (x) (display (balance->string x))) x))

;; use imperative procedure with atm.
(pretty-print-result (atm 100 user-input))

프로 시저 내에서 임시 변수를 설정하고 변경해야 할 수도 있지만 거의 모든 언어에서 동일한 작업을 수행하고 동일한 결과 (버그가 적음)를 생성 할 수 있지만 절차만큼 오래 걸리지는 않습니다. 실제로는 기능적으로 작동합니다 (매개 변수만으로 결과가 결정됨). 나는 당신이 약간의 LISP를 프로그래밍 한 후에 어떤 언어로든 더 나은 프로그래머가된다고 믿습니다. :)


프로그램의 기능 부분과 비 순수 기능 부분에 대한 광범위한 예제와 현실적인 설명과 FP가 중요한 이유에 대해 +1하십시오.
Zelphir Kaltstahl

1

할당은 상태 공간을 할당 전과 할당 후의 두 부분으로 나누기 때문에 잘못된 작동입니다. 이로 인해 프로그램 실행 중에 변수가 어떻게 변경되는지 추적하는 데 어려움이 있습니다. 기능적 언어에서 다음 사항이 과제를 대체합니다.

  1. 반환 값에 직접 연결된 함수 매개 변수
  2. 기존 객체를 수정하는 대신 반환 할 다른 객체를 선택합니다.
  3. 게으른 평가 된 새 값 만들기
  4. 메모리에 있어야하는 것뿐만 아니라 가능한 모든 객체를 나열
  5. 부작용 없음

이것은 제기 된 질문을 다루지 않는 것 같습니다. 순수한 기능 언어로 은행 계좌 개체를 어떻게 프로그래밍합니까?
Uday Reddy

한 은행 계좌 기록에서 다른 은행 계좌 기록으로 변환하는 기능 일뿐입니다. 핵심은 이러한 변형이 발생할 때 기존 객체를 수정하는 대신 새 객체를 선택한다는 것입니다.
tp1

한 은행 계좌 레코드를 다른 은행 계좌 레코드로 변환 할 때 고객이 이전 레코드가 아닌 새 레코드에서 다음 트랜잭션을 수행하기를 원합니다. 고객의 "연락처"는 현재 레코드를 가리 키도록 지속적으로 업데이트되어야합니다. 이것이 "수정"의 기본 개념입니다. 은행 계좌 "개체"는 은행 계좌 레코드가 아닙니다.
Uday Reddy
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.