순전히 기능적인 언어에 대한 오해?


39

나는 종종 다음과 같은 진술 / 논쟁에 직면한다 :

  1. 순수한 기능적 프로그래밍 언어는 부작용을 허용하지 않습니다 (따라서 유용한 프로그램이 외부 세계와 상호 작용할 때 부작용이 있기 때문에 실제로는 거의 사용되지 않습니다).
  2. 순수 기능 프로그래밍 언어는 상태를 유지하는 프로그램을 작성할 수 없습니다 (많은 응용 프로그램에서 상태가 필요하기 때문에 프로그래밍이 매우 어색합니다).

나는 기능적 언어의 전문가는 아니지만 지금까지 이러한 주제에 대해 이해하고 있습니다.

포인트 1과 관련하여 순전히 기능적인 언어로 환경과 상호 작용할 수 있지만 부작용을 유발하는 코드 (기능)를 명시 적으로 표시해야합니다 (예 : 모나 딕 유형의 하스켈). 또한 내가 선호하는 작업 방식이 아니더라도 부작용에 의한 컴퓨팅 (파괴적으로 데이터 업데이트)도 가능해야합니다 (모나 딕 유형 사용?).

포인트 2와 관련하여 내가 아는 한 여러 계산 단계 (하스켈에서 모나 딕 타입 사용)를 통해 값을 스레딩하여 상태를 나타낼 수는 있지만 실제 경험이 없으며 이해가 다소 모호합니다.

따라서 위의 두 진술은 어떤 의미로든 정확합니까, 아니면 순전히 기능적인 언어에 대한 오해 일 뿐입니 까? 그들이 오해라면 어떻게 되었습니까? (1) 부작용을 구현하고 (2) 상태를 사용하여 계산을 구현하는 Haskell 관용적 방법을 설명하는 (아마도 작은) 코드 스 니펫을 작성할 수 있습니까?


7
나는 이것이 대부분의 '순수한'기능적 언어를 정의하는 것에 달려 있다고 생각합니다.
jk.

@jk : '순수한'기능 언어를 정의하는 문제를 피하려면 Haskell 의미의 순도 (잘 정의 된)를 가정하십시오. 어떤 조건에서 기능적 언어가 '순수한'것으로 간주 될 수 있는지는 미래의 질문의 주제 일 수 있습니다.
Giorgio

두 답변 모두 명확하게 설명하는 아이디어가 많이 포함되어 있으므로 어느 것을 수락할지 선택하기가 어려웠습니다. 추가 의사 코드 예제로 인해 sepp2k의 답변을 수락하기로 결정했습니다.
Giorgio

답변:


26

이 답변의 목적을 위해 함수가 참조 적으로 투명한 기능적 언어를 의미하는 "순전히 기능적인 언어"를 정의합니다. 즉, 동일한 인수를 사용하여 동일한 함수를 여러 번 호출하면 항상 동일한 결과가 생성됩니다. 이것은 순수한 기능적 언어의 일반적인 정의입니다.

순수한 기능적 프로그래밍 언어는 부작용을 허용하지 않습니다 (따라서 유용한 프로그램이 외부 세계와 상호 작용할 때 부작용이 있기 때문에 실제로는 거의 사용되지 않습니다).

참조 투명성을 달성하는 가장 쉬운 방법은 실제로 부작용을 허용하지 않는 것이며 실제로 언어가 있습니다 (주로 도메인 특정 언어). 그러나 확실히 유일한 방법은 아니며 가장 보편적 인 순수한 기능 언어 (Haskell, Clean 등)는 부작용을 허용합니다.

또한 부작용이없는 프로그래밍 언어는 실제로 거의 쓸모가 없다고 생각합니다. 도메인 특정 언어는 아니지만 범용 언어조차도 부작용을 제공하지 않고 언어가 매우 유용 할 수 있다고 생각합니다. . 콘솔 응용 프로그램에는 적합하지 않지만 기능적 반응 패러다임과 같은 부작용없이 GUI 응용 프로그램을 훌륭하게 구현할 수 있다고 생각합니다.

포인트 1과 관련하여 순전히 기능적인 언어로 환경과 상호 작용할 수 있지만이를 소개하는 코드 (기능)를 명시 적으로 표시해야합니다 (예 : 모나 딕 유형을 사용하여 Haskell에서).

그것은 약간 단순화되었습니다. 부작용이있는 기능을 C ++의 const-correctness와 유사하지만 일반적인 부작용과 같이 표시해야하는 시스템만으로는 참조 투명성을 확보하기에 충분하지 않습니다. 프로그램이 동일한 인수로 함수를 여러 번 호출 할 수없고 다른 결과를 얻을 수 없도록해야합니다. 당신은 같은 것을 만들어서 그렇게 할 수 있습니다readLine함수가 아니거나 (하스켈이 IO 모나드로하는 것) 같은 인자로 부수적 인 함수를 여러 번 호출하는 것을 불가능하게 만들 수 있습니다 (Clean이하는 것). 후자의 경우 컴파일러는 부작용 함수를 호출 할 때마다 새로운 인수를 사용하여 동일한 인수를 부작용 함수에 두 번 전달하는 모든 프로그램을 거부합니다.

순수 기능 프로그래밍 언어는 상태를 유지하는 프로그램을 작성할 수 없습니다 (많은 응용 프로그램에서 상태가 필요하기 때문에 프로그래밍이 매우 어색합니다).

다시 말하지만, 순전히 기능적인 언어는 변경 가능한 상태를 매우 잘 허용하지 않을 수 있지만, 위의 부작용으로 설명한 것과 동일한 방식으로 구현하면 순수하고 여전히 변경 가능한 상태를 가질 수 있습니다. 실제로 변경 가능한 상태는 또 다른 형태의 부작용입니다.

즉, 함수형 프로그래밍 언어는 변경 가능한 상태, 특히 순수한 상태를 권장하지 않습니다. 그리고 그것이 프로그래밍을 어색하게 만드는 것이라고 생각하지 않습니다. 때때로 (그러나 종종 그런 것은 아니지만) 변경 가능한 상태는 성능이나 명확성을 잃지 않고 피할 수 없지만 (하스켈과 같은 언어에는 변경 가능한 상태를위한 기능이 있습니다) 대부분의 경우 가능합니다.

그들이 오해라면 어떻게 되었습니까?

많은 사람들이 단순히 "같은 인수로 호출 할 때 함수가 동일한 결과를 가져와야합니다"를 읽고 readLine가변 상태를 유지 하는 것과 같은 코드 나 코드 를 구현할 수 없다고 결론 내립니다 . 따라서 그들은 순전히 기능적인 언어가 참조 투명성을 손상시키지 않고 이러한 것들을 소개하는 데 사용할 수있는 "속임수"를 알지 못합니다.

또한 변경 가능한 상태는 기능적 언어에서는 크게 실망하지 않기 때문에 순전히 기능적인 언어에서는 전혀 허용되지 않는다고 가정하는 것이 큰 도약은 아닙니다.

(1) 부작용을 구현하고 (2) 상태를 사용하여 계산을 구현하는 Haskell 관용적 방법을 설명하는 (아마도 작은) 코드 스 니펫을 작성할 수 있습니까?

Pseudo-Haskell의 응용 프로그램은 사용자에게 이름을 묻고 인사합니다. Pseudo-Haskell은 방금 발명 한 언어로 Haskell의 IO 시스템을 가지고 있지만보다 일반적인 구문, 더 설명적인 함수 이름을 사용하며 표기법이 없습니다 do(IO 모나드의 작동 방식을 방해 하지 않기 때문에) :

greet(name) = print("Hello, " ++ name ++ "!")
main = composeMonad(readLine, greet)

단서 여기 즉 readLine타입의 값 IO<String>composeMonad유형의 인수를 취하는 함수이다 IO<T>(몇몇 유형 T)과 유형의 인수를 취하는 함수 다른 인수 T입력 값을 반환 IO<U>(몇몇 유형 U). print문자열을 가져 와서 type 값을 반환하는 함수입니다 IO<void>.

type 값은 type IO<A>값을 생성하는 지정된 작업을 "인코딩"하는 값입니다 A. 의 조치를 수행 한 다음 의 조치를 인 코드 composeMonad(m, f)하는 새 IO값을 생성합니다 . 여기서 값은의 조치를 수행하여 생성합니다 .mf(x)xm

가변 상태는 다음과 같습니다.

counter = mutableVariable(0)
increaseCounter(cnt) =
    setIncreasedValue(oldValue) = setValue(cnt, oldValue + 1)
    composeMonad(getValue(cnt), setIncreasedValue)

printCounter(cnt) = composeMonad( getValue(cnt), print )

main = composeVoidMonad( increaseCounter(counter), printCounter(counter) )

다음 mutableVariable은 모든 유형의 값을 가져와 Ta를 생성하는 함수입니다 MutableVariable<T>. 이 함수 getValue는 현재 값을 생성하는를 가져 MutableVariable오고 반환합니다 IO<T>. setValuea MutableVariable<T>와 a를 가져 와서 값을 설정하는를 T반환합니다 IO<void>. 첫 번째 인수가 합리적인 값을 생성하지 않고 두 번째 인수가 모나드를 반환하는 함수가 아닌 다른 모나드 composeVoidMonad라는 composeMonad점을 제외하고 는 동일 IO합니다.

Haskell에는 구문상의 설탕이 있는데,이 전체 시련은 덜 고통 스럽지만, 변하기 쉬운 상태는 언어가 실제로 원하지 않는 것임이 여전히 분명합니다.


많은 아이디어를 명확하게 설명합니다. 코드의 마지막 줄은 이름을 사용하는 조각해야 counter즉, increaseCounter(counter)?
Giorgio

@Giorgio 그렇습니다. 결정된.
sepp2k

1
@Giorgio 내 게시물에서 명시 적으로 언급하지 않은 한 가지는 반환 된 IO 작업 main이 실제로 실행되는 작업이라는 것입니다. IO를 반환하는 것 외에는 ( 이름에 끔찍한 악의적 인 기능을 사용하지 않고) 액션 main을 실행할 수있는 방법이 없습니다 . IOunsafe
sepp2k

승인. scarfridge는 또한 파괴적인 IO가치를 언급했다 . 패턴 일치를 나타내는 지 여부, 즉 대수 데이터 형식의 값을 해체 할 수 있다는 사실을 이해하지 못했지만 패턴 일치를 사용하여 값으로 처리 할 수는 없습니다 IO.
Giorgio

16

순수한 언어 와 순수한 기능 사이에 차이가 있기 때문에 혼란 스럽습니다 . 기능부터 시작하겠습니다. 함수는 (동일한 입력으로) 항상 같은 값을 반환하고 관찰 가능한 부작용을 일으키지 않으면 순수 합니다. 일반적인 예는 f (x) = x * x와 같은 수학 함수입니다. 이제이 함수의 구현을 고려하십시오. ML과 같이 일반적으로 순수한 기능 언어로 간주되지 않는 언어조차도 대부분의 언어에서 순수합니다. 이 동작을 가진 Java 또는 C ++ 메소드조차 순수한 것으로 간주 될 수 있습니다.

그렇다면 순수한 언어는 무엇입니까? 엄밀히 말하면 순수한 언어는 순수하지 않은 함수를 표현할 수 없을 것으로 기대할 수 있습니다. 이것을 순수한 언어 의 이상적 정의 라고하자 . 이러한 행동은 매우 바람직하다. 왜? 순수한 함수로만 구성된 프로그램의 장점은 프로그램의 의미를 변경하지 않고 함수 응용 프로그램을 해당 값으로 바꿀 수 있다는 것입니다. 결과를 알고 나면 계산 방식을 잊을 수 있기 때문에 프로그램에 대해 추론하기가 매우 쉽습니다. 또한 순도는 컴파일러가 특정 공격적인 최적화를 수행 할 수있게합니다.

내부 상태가 필요한 경우 어떻게해야합니까? 계산 전의 상태를 입력 매개 변수로 추가하고 계산 후의 상태를 결과의 일부로 추가하여 순수한 언어로 상태를 모방 할 수 있습니다. 대신 Int -> Bool당신은 같은 것을 얻습니다 Int -> State -> (Bool, State). 종속성을 명시 적으로 만드십시오 (모든 프로그래밍 패러다임에서 모범 사례로 간주 됨). BTW에는 이러한 상태 모방 기능을 더 큰 상태 모방 기능으로 결합하는 특히 우아한 방법 인 모나드가 있습니다. 이런 식으로 당신은 확실히 순수한 언어로 "상태를 유지"할 수 있습니다. 그러나 당신은 그것을 명시 적으로 만들어야합니다.

이것이 내가 외부와 상호 작용할 수 있다는 것을 의미합니까? 결국 유용한 프로그램이 유용하려면 실제 세계와 상호 작용해야합니다. 그러나 입력과 출력은 분명히 순수하지 않습니다. 특정 바이트를 특정 파일에 쓰는 것이 처음으로 좋습니다. 그러나 정확히 동일한 작업을 두 번 실행하면 디스크가 가득 차서 오류가 반환 될 수 있습니다. 분명히 파일에 쓸 수있는 순수한 언어 (이상적 의미)는 존재하지 않습니다.

그래서 우리는 딜레마에 직면 해 있습니다. 우리는 대부분 순수한 기능을 원하지만 일부 부작용은 절대적으로 필요하며 순수한 것은 아닙니다. 이제 순수한 언어에 대한 현실적인 정의 는 순수한 부분을 다른 부분과 분리 할 수단이 있어야한다는 것입니다. 이 메커니즘은 불순한 작업이 순수한 부분으로 몰래 들어 가지 않도록해야합니다.

Haskell에서는 IO 유형으로 수행됩니다. 안전하지 않은 메커니즘없이 IO 결과를 삭제할 수 없습니다. 따라서 IO 모듈 자체에 정의 된 함수로만 IO 결과를 처리 할 수 ​​있습니다. 운 좋게도 매우 유연한 조합기가있어 IO 결과를 가져 와서 함수가 다른 IO 결과를 반환하는 한 함수에서 처리 할 수 ​​있습니다. 이 결합자를 bind (또는 >>=) 라고 하며 유형이 IO a -> (a -> IO b) -> IO b있습니다. 이 개념을 일반화하면 모나드 클래스에 도달하고 IO가 해당 인스턴스의 인스턴스가됩니다.


4
나는 Haskell ( unsafe이름으로 함수를 무시하는 )이 당신의 이상 주의적 정의를 어떻게 충족시키지 않는지 실제로 알지 못합니다. Haskell에는 불순한 기능이 없습니다 (다시 무시 unsafePerformIO하고 공동으로).
sepp2k

4
readFile그리고 writeFile항상 같은 돌아갑니다 IO같은 인수 주어진 값입니다. 그래서이 개 코드 조각을 예 let x = writeFile "foo.txt" "bar" in x >> xwriteFile "foo.txt" "bar" >> writeFile "foo.txt" "bar"같은 일을 할 것입니다.
sepp2k

3
@AidanCully "IO 기능"은 무엇을 의미합니까? IO Something? 유형의 값을 반환하는 함수 그렇다면, 같은 인수 두번 IO 함수를 호출 할 수 있도록 완벽하게 가능하다 : putStrLn "hello" >> putStrLn "hello"- 여기 모두 호출하는 putStrLn같은 인수가 있습니다. 물론 앞에서 말했듯이 두 호출 모두 동일한 IO 값을 가지므로 문제가되지 않습니다.
sepp2k

3
@scarfridge writeFile "foo.txt" "bar"함수 호출을 평가해도 조치가 실행 되지 않으므로 평가하면 오류가 발생할 수 없습니다 . 이전 예제에서 with let버전 let이 두 개가 없는 버전에서 IO 실패를 일으킬 수있는 기회가 한 번 뿐이라고 말하면 틀린 것입니다. 두 버전 모두 IO 실패에 대한 두 가지 기회가 있습니다. 이후 let버전이 통화를 평가하기 위해 writeFile한 번하지 않고 버전을 동시에 let평가하여 그것을 두 번, 당신은 함수가 호출 빈도를 중요하지 않습니다 것을 볼 수 있습니다. 그것은 얼마나 자주 결과가 중요합니다 ...
sepp2k

6
@AidanCully "모 노드 메커니즘"은 암시 적 매개 변수를 전달하지 않습니다. 이 putStrLn함수는 정확히 하나의 인수를 취하는데 이는 유형 String입니다. 당신이 저를 믿지 않는다면, 그 유형을보십시오 : String -> IO (). 확실히 타입의 인수를 취하지 않습니다 IO-그것은 그 타입의 값을 생성합니다.
sepp2k
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.