폐쇄는 기능적인 스타일로 간주됩니까?


33

함수형 프로그래밍에서 클로저가 불완전한 것으로 간주됩니까?

일반적으로 값을 함수에 직접 전달하여 클로저를 피할 수 있습니다. 따라서 가능한 경우 폐쇄를 피해야합니까?

그들이 불완전하고 피할 수 있다고 말하는 것이 정확하다면 왜 그렇게 많은 기능적 프로그래밍 언어가 클로저를 지원합니까?

순수한 함수 의 기준 중 하나는 " 함수 는 항상 동일한 인수 값으로 동일한 결과 값을 평가합니다 ."입니다.

가정

f: x -> x + y

f(3)항상 같은 결과를 제공하지는 않습니다. 의 인수가 아닌 f(3)값에 따라 다릅니다 . 따라서 순수한 기능은 아닙니다.yff

모든 클로저는 인수가 아닌 값에 의존하기 때문에 어떻게 클로저가 순수 할 수 있습니까? 이론적으로 닫힌 값은 일정 할 수 있지만 함수 자체의 소스 코드를 살펴 보는 것만으로는 알 수 없습니다.

이것이 나를 이끌어주는 곳은 한 상황에서는 동일한 기능이 순수하지만 다른 상황에서는 불순하다는 것입니다. 소스 코드를 연구하여 함수가 순수한지 여부를 항상 확인할 수는 없습니다 . 오히려, 그러한 구별이 이루어지기 전에 그것이 호출되는 시점에서 환경과 관련하여 그것을 고려해야 할 수도 있습니다.

나는 이것에 대해 올바르게 생각하고 있습니까?


6
나는 Haskell에서 클로저를 항상 사용하며, Haskell은 순전히 순수합니다.
Thomas Eding

5
순수한 기능적 언어에서는 y변경할 수 없으므로 출력 f(3)은 항상 동일합니다.
Lily Chung

4
y정의의 일부입니다 f명시 적으로 입력으로 표시되지 비록 f- 그것은 여전히 사건 f의 관점에서 정의된다 y(우리가 함수 f_y를 나타내는 수에 의존 만들기 위해 y명시 적)을, 따라서 변화가 y주는 다른 기능을 . 특정 기능 f_y에 대해 정의 된 특정 기능 y은 매우 순수합니다. (예를 들어, 두 가지 기능 f: x -> x + 3f: x -> x + 5있습니다 다른 우리가 그들을 표시하기 위해 같은 문자를 사용하는 일이 있더라도, 모두 순수 기능을합니다.)
ShreevatsaR

답변:


26

순도는 두 가지로 측정 할 수 있습니다.

  1. 함수는 동일한 입력이 주어지면 항상 동일한 출력을 반환합니까? 즉, 참조 적으로 투명합니까?
  2. 함수가 외부의 것을 수정합니까, 즉 부작용이 있습니까?

1에 대한 답이 예이고 2에 대한 답이 아니오이면, 함수는 순수합니다. 클로즈 오버 변수를 수정하면 클로저는 함수가 불완전하게 만듭니다.


첫 번째 항목이 결정적이지 않습니까? 아니면 순도의 일부입니까? 나는 프로그래밍의 맥락에서 "순도"라는 개념에 익숙하지 않다.

4
@JimmyHoffa : 반드시 그런 것은 아닙니다. 하드웨어 타이머의 출력을 함수에 넣을 수 있으며 함수 외부의 어떤 것도 수정되지 않습니다.
Robert Harvey

1
@RobertHarvey 이것이 함수에 대한 입력을 정의하는 방법에 관한 것입니까? Wikipedia의 인용문은 함수 인수에 중점을 두는 반면 닫힌 변수는 입력으로 간주합니다.
user2179977

8
@ user2179977 : 변수를 변경할 수 없다면 , 닫혀진 변수를 함수에 대한 추가 입력으로 간주 해서는 안됩니다 . 오히려 클로저 자체는 함수이고 다른 값으로 닫힐 때 다른 함수로 간주해야합니다 y. 예를 들어 우리 는 그 자체가 함수 인 함수 g를 정의 g(y)합니다 x -> x + y. 그런 다음 g함수 g(3)를 리턴하는 정수 함수이고, 정수를 리턴하는 정수 함수이며, 정수를 리턴하는 정수 g(2)다른 함수입니다. 세 가지 기능 모두 순수합니다.
Steve Jessop

1
@ Darkhogg : 예. 내 업데이트를 참조하십시오.
Robert Harvey

10

클로저는 가능한 가장 순수한 함수형 프로그래밍 인 Lambda Calculus로 표시되므로 "불완전"이라고 부르지 않습니다.

함수형 언어의 함수는 일급 시민이기 때문에 클로저는 "불완전"하지 않습니다. 즉, 값으로 취급 될 수 있습니다.

이것을 (가상 코드) 상상해보십시오.

foo(x) {
    let y = x + 1
    ...
}

y값입니다. 그것은의 값에 따라 다릅니다 x만, x그렇게 불변 y의 가치는 불변이다. 우리는 foo서로 다른 논증을 여러 번 불러 낼 수 y있지만, 다른 논점을 만들어 낼 수 있지만, 그 논점은 y모두 다른 범위에 있으며 다른 점에 의존 x하므로 순결은 그대로 남아 있습니다.

이제 변경해 보자.

bar(x) {
    let y(z) = x + z
    ....
}

여기서 우리는 클로저를 사용하고 있지만 (x를 닫는 중입니다), 다른 인수를 사용 foo하는 다른 호출 bar은 서로 다른 값을 생성합니다 y(함수는 값입니다). 순수는 그대로 유지됩니다.

또한 클로저는 커리와 매우 유사한 효과를 나타냅니다.

adder(a)(b) {
    return a + b
}
baz(x) {
    let y = adder(x)
    ...
}

baz실제로는 bar-와 다르지 않습니다 . 둘 다 y인수 플러스를 반환 하는 함수 값을 만듭니다 x. 실제로 Lambda Calculus에서는 클로저를 사용하여 여러 개의 인수로 함수를 생성하지만 여전히 불확실하지 않습니다.


9

다른 사람들은 일반적인 질문에 대한 답변을 훌륭하게 다루었으므로 편집에서 표시하는 혼란을 지우는 것만 살펴 보겠습니다.

클로저는 함수의 입력이 아니라 함수 본문으로 '가는'것입니다. 보다 구체적으로 말하면 함수는 본문의 외부 범위에있는 값을 말합니다.

기능이 불완전하다는 인상을 받았습니다. 일반적으로 그렇지 않습니다. 함수형 프로그래밍에서, 값은 대부분 불변 입니다. 이는 마감 값에도 적용됩니다.

다음과 같은 코드가 있다고 가정 해 봅시다.

let make y =
    fun x -> x + y

호출 make 3make 4이상 폐쇄 당신에게 두 가지 기능을 제공합니다 makey인수입니다. 그들 중 하나는 돌아올 것이고 x + 3, 다른 하나는 돌아올 것이다 x + 4. 그러나 두 가지 고유 한 기능이며 모두 순수합니다. 그것들은 같은 make기능을 사용하여 만들어 졌지만 그게 전부입니다.

메모 대부분의 시간을 몇 단락을 백업 할 수 있습니다.

  1. 순수한 Haskell에서는 변경할 수없는 값만 닫을 수 있습니다. 닫을 수있는 변경 가능한 상태가 없습니다. 그런 식으로 순수한 기능을 얻을 수 있습니다.
  2. F #과 같은 불완전한 기능 언어에서는 참조 셀과 참조 유형을 닫고 불완전한 기능을 사용할 수 있습니다. 함수가 순수한지 여부를 알기 위해 함수가 정의 된 범위를 추적해야합니다. 해당 언어로 값을 변경할 수 있는지 쉽게 알 수 있으므로 큰 문제는 아닙니다.
  3. C # 및 JavaScript와 같이 클로저를 지원하는 OOP 언어에서는 상황이 불완전한 기능적 언어와 유사하지만 변수가 기본적으로 변경 가능하므로 외부 범위를 추적하는 것이 더 까다로워집니다.

2와 3의 경우 해당 언어는 순도에 대한 보장을 제공하지 않습니다. 불순물은 폐쇄의 속성이 아니라 언어 자체의 속성입니다. 클로저는 그 자체로 그림을 많이 바꾸지 않습니다.


1
Haskell에서 변경 가능한 값을 완전히 닫을 수는 있지만 IO 모나드로 주석이 달립니다.
Daniel Gratzer

1
@jozefg 아니오, 불변 IO A값 을 닫고 클로저 유형은 IO (B -> C)그 와 비슷 합니다 . 순도 유지
Caleth

5

일반적으로 "불순"에 대한 정의를 명확히 해달라고 요청하지만이 경우에는 실제로 중요하지 않습니다. 순전히 기능적 이라는 용어를 대조한다고 가정하면 대답은 "아니오"입니다. 본질적으로 파괴적인 클로저에 대해서는 아무것도 없기 때문입니다. 언어가 클로저없이 순수하게 작동하더라도 여전히 클로저로 작동합니다. 대신 "기능이 없음"을 의미하는 경우 여전히 "아니오"입니다. 클로저는 함수 생성을 용이하게합니다.

일반적으로 데이터를 함수에 직접 전달하여 클로저를 피할 수 있습니다.

예, 그러나 함수에는 하나 이상의 매개 변수가 있으며 유형이 변경됩니다. 클로저를 사용하면 매개 변수 추가 하지 않고 변수 기반으로 함수를 작성할 수 있습니다 . 예를 들어 2 개의 인수를받는 함수가 있고 1 개의 인수 만 취하는 버전을 만들려는 경우에 유용합니다.

편집 : 자신의 편집 / 예와 관련하여 ...

가정

f : x-> x + y

f (3)이 항상 동일한 결과를 제공하지는 않습니다. f (3)은 f의 인수가 아닌 y의 값에 따라 다릅니다. 따라서 f는 순수한 함수가 아닙니다.

여기에 잘못된 단어 선택이 달려 있습니다. 당신이 한 동일한 위키 백과 기사를 인용 :

컴퓨터 프로그래밍에서, 함수에 대한이 두 문장이 모두 다음과 같은 경우 함수는 순수한 함수로 설명 될 수 있습니다.

  1. 함수는 항상 동일한 인수 값이 주어지면 동일한 결과 값을 평가합니다. 기능 결과 값은 프로그램 실행이 진행됨에 따라 또는 프로그램의 다른 실행간에 변경 될 수있는 숨겨진 정보 나 상태에 의존 할 수 없으며 I / O 장치의 외부 입력에 의존 할 수도 없습니다.
  2. 결과의 평가는 변경 가능한 객체의 돌연변이 또는 I / O 장치로의 출력과 같이 의미 적으로 관찰 가능한 부작용 또는 출력을 유발하지 않습니다.

y변경이 불가능하다고 가정하면 (보통 기능적 언어의 경우) 조건 1이 충족됩니다.의 모든 값에 x대해의 값은 f(x)변경되지 않습니다. 이것은 y상수와 다르지 않고 x + 3순수한 사실에서 분명해야합니다 . 또한 돌연변이 나 I / O가 진행되지 않는 것이 분명합니다.


3

매우 빠르게 : "like like to replace like"인 경우 대체는 "referentially transparent"이고 모든 효과가 반환 값에 포함 된 경우 함수는 "pure"입니다. 두 가지 모두 정확하게 만들 수 있지만, 동일하지 않거나 서로를 암시하는 것도 중요합니다.

이제 폐쇄에 대해 이야기합시다.

지루한 (주로 순수한) "폐쇄 물"

람다 용어를 평가할 때 변수를 환경 조회로 해석하기 때문에 클로저가 발생합니다. 따라서 평가 결과로 람다 항을 반환하면 변수 내부의 변수가 정의 될 때 가져온 값을 "닫습니다".

평범한 람다 미적분학에서 이것은 사소한 것이며 전체 개념은 사라집니다. 이를 입증하기 위해 다음은 비교적 가벼운 람다 미적분 해석기입니다.

-- untyped lambda calculus values are functions
data Value = FunVal (Value -> Value)

-- we write expressions where variables take string-based names, but we'll
-- also just assume that nobody ever shadows names to avoid having to do
-- capture-avoiding substitutions

type Name = String

data Expr
  = Var Name
  | App Expr Expr
  | Abs Name Expr

-- We model the environment as function from strings to values, 
-- notably ignoring any kind of smooth lookup failures
type Env = Name -> Value

-- The empty environment
env0 :: Env
env0 _ = error "Nope!"

-- Augmenting the environment with a value, "closing over" it!
addEnv :: Name -> Value -> Env -> Env
addEnv nm v e nm' | nm' == nm = v
                  | otherwise = e nm

-- And finally the interpreter itself
interp :: Env -> Expr -> Value
interp e (Var name) = e name          -- variable lookup in the env
interp e (App ef ex) =
  let FunVal f = interp e ef
      x        = interp e ex
  in f x                              -- application to lambda terms
interp e (Abs name expr) =
  -- augmentation of a local (lexical) environment
  FunVal (\value -> interp (addEnv name value e) expr)

주목해야 할 중요한 부분 addEnv은 새로운 이름으로 환경을 확장 할 때입니다. 이 함수는 해석 된 Abs견인 항 (람다 항) 의 "내부"만 호출됩니다 . Var용어를 평가할 때마다 환경이 "검색" 되므로 해당 용어 는 을 포함하는 견인에 의해 포착 된 Var모든 항목으로 해석됩니다 .NameEnvAbsVar

이제 다시 일반 LC 용어로 이것은 지루합니다. 그것은 바운드 변수가 누군가가 관심을 갖는 한 상수라는 것을 의미합니다. 이들은 환경에서 해당 시점까지의 어휘 범위로 표시 한 값으로 직접 및 즉시 평가됩니다.

이것은 또한 (거의) 순수합니다. 람다 미적분학에서 용어의 유일한 의미는 반환 값에 의해 결정됩니다. 유일한 예외는 Omega 용어로 구체화되는 비 종료의 부작용입니다.

-- in simple LC syntax:
--
-- (\x -> (x x)) (\x -> (x x))
omega :: Expr
omega = App (Abs "x" (App (Var "x") 
                          (Var "x")))
            (Abs "x" (App (Var "x") 
                          (Var "x")))

재미있는 (불순한) 폐쇄

이제 특정 배경에서 우리가 닫은 변수와 상호 작용할 수 없다는 개념이 없기 때문에 위의 일반 LC에 설명 된 폐쇄는 지루합니다. 특히 "클로저"라는 단어는 다음 Javascript와 같은 코드를 호출하는 경향이 있습니다.

> function mk_counter() {
  var n = 0;
  return function incr() {
    return n += 1;
  }
}
undefined

> var c = mk_counter()
undefined
> c()
1
> c()
2
> c()
3

이것은 n내부 함수에서 변수를 닫았으며 incr호출 incr이 해당 변수와 의미있게 상호 작용 한다는 것을 보여줍니다 . mk_counter순수하지만 incr결정적으로 불완전합니다 (참조 적으로 투명하지 않음).

이 두 인스턴스의 차이점은 무엇입니까?

"가변"의 개념

우리가 평범한 LC 의미에서 대체와 추상화가 무엇을 의미하는지 살펴보면, 그것들은 분명히 평범하다는 것을 알 수 있습니다. 변수는 말 그대로 즉각적인 환경 조회에 지나지 않습니다 . 람다 추상화는 말 그대로없는 것이 더 내부 표현을 평가하기 위해 증강 환경을 만드는 것보다. 이 모델 에는 변형이 허용되지 않기 때문에 mk_counter/로 보았던 행동의 종류에 대한 incr여지가 없습니다.

많은 사람들에게 이것은 "가변"의 의미, 변화의 핵심입니다. 그러나 의미 론자들은 LC에 사용 된 변수의 종류와 Javascript에 사용 된 "변수"의 종류를 구별하려고합니다. 그렇게하기 위해, 이들은 후자를 "돌연변이 가능한 셀"또는 "슬롯"이라고 부르는 경향이있다.

"+ X X"를 허용하지 않는다 (수학) 표현 :이 용어는 더 "알 수없는"같은 것을 의미 수학에서 "변수"의 긴 사용 기록을 다음 x시간에 따라 변화하는, 그 대신에 관계없이 의미가있는 것을 의미한다 (단일, 상수) 값 x이 사용됩니다.

따라서 슬롯에 값을 넣고 빼는 기능을 강조하기 위해 "슬롯"이라고합니다.

혼란을 더하기 위해 Javascript에서 이러한 "슬롯"은 변수와 동일하게 보입니다.

var x;

하나를 작성하고 우리가 쓸 때

x;

해당 슬롯에 현재 저장된 값을 찾는다는 의미입니다. 이를 더 명확하게하기 위해 순수한 언어는 슬롯을 이름을 (수학적, 람다 미적분학) 이름으로 생각하는 경향이 있습니다. 이 경우 슬롯에서 가져 오거나 넣을 때 명시 적으로 레이블을 지정해야합니다. 이러한 표기법은 다음과 같은 경향이 있습니다.

-- create a fresh, empty slot and name it `x` in the context of the 
-- expression E
let x = newSlot in E

-- look up the value stored in the named slot named `x`, return that value
get x

-- store a new value, `v`, in the slot named `x`, return the slot
put x v

이 표기법의 장점은 이제 우리가 수학 변수와 가변 슬롯을 확고하게 구분한다는 것입니다. 변수는 슬롯을 값으로 사용할 수 있지만 변수에 의해 명명 된 특정 슬롯은 해당 범위에서 일정합니다.

이 표기법을 사용하여 mk_counter예제를 다시 작성할 수 있습니다 (이번에는 Haskell과 유사한 구문이지만 Haskell과 같은 의미론으로 결정됨).

mkCounter = 
  let x = newSlot 
  in (\() -> let old = get x 
             in get (put x (old + 1)))

이 경우 우리는이 가변 슬롯을 조작하는 절차를 사용하고 있습니다. 그것을 구현하기 위해서는 다음과 같은 이름의 일정한 환경뿐만 아니라 폐쇄해야합니다.x 뿐만 아니라 필요한 모든 슬롯이 포함 된 가변 환경 합니다. 이것은 사람들이 너무 좋아하는 "폐쇄"라는 일반적인 개념에 더 가깝습니다.

다시 말하지만, mkCounter매우 불순합니다. 또한 참조 적으로 불투명합니다. 그러나 통지 부작용 이름 캡처 또는 폐쇄 대신의 캡처에서 발생하지 않도록 변경할 세포 와 같은 그것의 측면 초래 운영 get하고 put.

궁극적으로 이것이 귀하의 질문에 대한 최종 답변이라고 생각합니다. 순도는 (수학적) 변수 캡처의 영향을받지 않지만 대신 캡처 된 변수로 명명 된 가변 슬롯에서 수행되는 부작용으로 인해 발생합니다.

LC에 가까워 지거나 순도를 유지하려고 시도하지 않는 언어에서만이 두 개념이 너무 자주 혼동되어 혼란을 초래합니다.


1

아닙니다. 클로저는 함수형 프로그래밍에서 일반적인 경우 인 닫힌 값이 일정하거나 (클로저 나 다른 코드에 의해 변경되지 않는 한) 함수가 불완전하지 않게합니다.

대신 항상 값을 인수로 전달할 수는 있지만 상당한 어려움 없이는 그렇게 할 수 없습니다. 예를 들어 (커피 스크립트) :

closedValue = 42
return (arg) -> console.log "#{closedValue} #{arg}"

당신의 제안에 의해, 당신은 단지 돌아올 수 있습니다 :

return (arg, closedValue) -> console.log "#{closedValue} #{arg}"

이 함수는 이 시점에서 호출 되지 않고 방금 정의 되었으므로 원하는 값을 closedValue함수가 실제로 호출되는 지점 으로 전달하는 방법을 찾아야합니다 . 기껏해야 이것은 많은 커플 링을 만듭니다. 최악의 경우, 호출 지점에서 코드를 제어하지 않으므로 사실상 불가능합니다.

클로저를 지원하지 않는 언어의 이벤트 라이브러리는 일반적으로 임의의 데이터를 콜백으로 다시 전달할 수있는 다른 방법을 제공하지만 예쁘지 않으며 라이브러리 관리자와 라이브러리 사용자 모두에게 많은 복잡성을 유발합니다.

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