참조 투명성을 깨는 부작용


11

스칼라의 함수형 프로그래밍은 참조 투명성을 깨뜨리는 부작용의 영향을 설명합니다.

부작용, 이는 참조 투명성을 위반 한 것을 의미합니다.

SICP의 일부를 읽었으며 , "대체 모델"을 사용하여 프로그램을 평가하는 방법에 대해 설명합니다.

내가으로 참조 투명성 (RT)로 치환 모델을 이해하고, 당신은 간단한 부분으로 기능을 해제 구성 할 수 있습니다. 표현식이 RT 인 경우 표현식을 분해하고 항상 동일한 결과를 얻을 수 있습니다.

그러나 위의 인용문에서 알 수 있듯이 부작용을 사용하면 대체 모델이 깨질 수 있습니다.

예:

val x = foo(50) + bar(10)

경우 foobar 하지 않는 부작용을 가지고, 다음 중 하나 기능을 실행하는 것입니다 항상 동일한 결과를 반환합니다 x. 그러나 부작용이있는 경우 렌치를 대체 모델에 방해 / 투사시키는 변수를 변경합니다.

나는이 설명에 익숙하다고 느끼지만 완전히 이해하지는 못한다.

RT를 깨는 부작용과 관련하여 저를 수정하고 구멍을 채우십시오. 대체 모델에 대한 영향도 논의하십시오.

답변:


20

참조 투명성에 대한 정의부터 시작하겠습니다 .

표현식은 프로그램의 동작을 변경하지 않고 값으로 대체 할 수있는 경우 참조 적으로 투명하다고합니다 (즉, 동일한 효과를 가지며 동일한 입력에서 출력되는 프로그램을 생성 함).

즉, 프로그램의 어느 부분에서나 2 + 5를 7로 바꿀 수 있으며 프로그램은 여전히 ​​작동해야합니다. 이 과정을 대체 라고 합니다. 대체는 프로그램의 다른 부분에 영향을주지 않고 2 + 5를 7로 대체 할 수있는 경우에만 유효합니다 .

Baz함수 Foo와 그 Bar안에 클래스가 있다고 가정 해 봅시다 . 단순화하기 위해, 우리는 그냥 말할 것이다 FooBar모두 돌아갑니다. 그래서 전달 된 값을 Foo(2) + Bar(5) == 7사용자가 예상하는대로를. 참조 투명도는 프로그램의 어느 위치에서나 식 Foo(2) + Bar(5)을 식 으로 바꿀 수 7있으며 프로그램은 여전히 ​​동일하게 작동합니다.

그러나 Foo전달 된 값을 반환했지만 Bar전달 된 값 과 마지막으로 제공된Foo 값을 반환하면 어떻게됩니까? 클래스 Foo내의 지역 변수에 값을 저장하면 쉽게 할 수 있습니다 Baz. 해당 지역 변수의 초기 값이 0이면 표현식 Foo(2) + Bar(5)7처음 호출 할 때 의 예상 값을 리턴하지만 호출 할 9때 두 번째로 리턴 합니다.

이것은 참조 투명성을 두 가지 방법으로 위반합니다. 첫째, Bar는 호출 될 때마다 동일한 표현식을 리턴하도록 계산할 수 없습니다. 둘째, 부작용이 발생했습니다. 즉 Foo를 호출하면 Bar의 반환 값에 영향을 미칩니다. Foo(2) + Bar(5)7과 같은 것을 더 이상 보장 할 수 없으므로 더 이상 대체 할 수 없습니다.

이것이 참조 투명도의 의미입니다. 참조 적으로 투명한 함수는 프로그램의 다른 코드에 영향을주지 않으면 서 일부 값을 받아들이고 해당 값을 반환하며 항상 동일한 입력이 주어지면 동일한 출력을 반환합니다.


5
그래서 파괴 RT사용에서 비활성화 당신을 substitution model.에 큰 문제가 없다 (가) 사용 할 수있는 substitution model프로그램에 대한 이유에 그것을 사용의 힘을?
케빈 메러디스

맞습니다.
Robert Harvey

1
+1 놀라 울 정도로 명확하고 이해하기 쉬운 답변. 감사합니다.
Racheet

2
또한 이러한 함수가 투명하거나 "순수한"경우 실제로 실행되는 순서가 중요하지 않은 경우 foo () 또는 bar ()가 먼저 실행되는지 상관하지 않으며 경우에 따라 필요하지 않은 경우 평가하지 않을 수도 있습니다.
Zachary K

1
RT의 또 다른 장점은 고가의 참조 적으로 투명한 표현식을 캐시 할 수 있다는 것입니다 (한 번 또는 두 번 평가하면 정확히 동일한 결과가 생성되므로).
dcastro

3

벽을 쌓으려고하는데 크기와 모양이 다른 상자가 여러 개 있다고 가정 해보십시오. 벽의 특정 L 자형 구멍을 채워야합니다. L 자 모양 상자를 찾거나 적절한 크기의 직선 상자 두 개를 대체 할 수 있습니까?

기능적 세계에서 답은 두 솔루션 모두 작동한다는 것입니다. 기능적인 세상을 만들 때 안에 무엇이 있는지보기 위해 상자를 열 필요는 없습니다.

명령적인 세계에서는 모든 상자의 내용물을 검사하고 다른 상자의 내용물 비교 하지 않고 벽을 짓는 것은 위험합니다 .

  • 일부는 강한 자석을 포함하고 잘못 정렬되면 벽에서 다른 자기 상자를 밀어냅니다.
  • 일부는 매우 뜨겁거나 차가우므로 인접한 공간에 놓으면 심하게 반응합니다.

좀 더 은유 적으로 시간을 허비하기 전에 멈추겠다고 생각하지만, 요점을 밝히기를 바랍니다. 기능성 벽돌에는 숨겨진 놀라움이 없으며 완전히 예측할 수 있습니다. 큰 크기를 대체하기 위해 항상 올바른 크기와 모양의 작은 블록을 사용할 수 있고 크기와 모양이 같은 두 상자 사이에 차이가 없으므로 참조 투명성이 있습니다. 명령형 벽돌을 사용하면 적절한 크기와 모양을 가진 것으로 충분하지 않습니다. 벽돌을 어떻게 구성했는지 알아야합니다. 참조 투명하지 않습니다.

순수한 기능적 언어에서, 당신이 볼 필요가있는 것은 그 기능을 알기위한 함수의 서명입니다. 물론, 당신은 얼마나 잘 수행보고 내부 볼 수도 있습니다,하지만 당신은하지 않습니다 보기에.

명령형 언어에서는 어떤 놀라움이 숨어 있을지 전혀 모릅니다.


"순수한 기능 언어에서, 당신이 볼 필요가있는 것은 그 기능을 알기위한 함수의 서명입니다." – 그것은 사실이 아닙니다. 예, 파라 메트릭 다형성의 가정하에 우리는 유형의 기능이 있다는 결론을 내릴 수 (a, b) -> a에만 할 수있다 fst기능과 형태의 함수는 a -> a단지 수 있습니다 identity기능,하지만 당신은 반드시 유형의 기능에 대해 아무것도 말할 수 없다 (a, a) -> a예를 들어,.
Jörg W Mittag

2

대체 모델을 참조하면 (참조 투명도 (RT)) 함수를 가장 간단한 부분으로 분해 할 수 있습니다. 표현식이 RT 인 경우 표현식을 분해하고 항상 동일한 결과를 얻을 수 있습니다.

그렇습니다. 직관이 아주 옳습니다. 보다 정확한 정보를 얻으려면 다음과 같이하십시오.

말했듯이 모든 RT 표현식에는 single"결과" 가 있어야합니다 . 즉, factorial(5)프로그램에 표현이 주어지면 항상 동일한 "결과"를 산출해야합니다. 따라서 특정 factorial(5)프로그램이 프로그램에 있고 120을 산출하면 시간에 관계없이 확장 / 계산 된 "단계 순서"에 관계없이 항상 120을 산출해야합니다 .

예 : factorial기능.

def factorial(n):
    if n == 1:
        return 1
    return n * factorial(n - 1)

이 설명에는 몇 가지 고려 사항이 있습니다.

우선, 다른 평가 모델 (적용 대 정상 순서 참조)은 동일한 RT 표현에 대해 다른 "결과"를 산출 할 수 있습니다.

def first(y, z):
  return y

def second(x):
  return second(x)

first(2, second(3)) # result depends on eval. model

위의 코드에서, first그리고 second정상 순서 및 실용적 순서에 따라 평가 referentially 경우 투명하고, 또, 마지막 표현식이 다른 "결과"산출 (후자 하에서 발현은 정지되지 않음).

.... 따옴표로 "결과"를 사용합니다. 식을 중지 할 필요가 없으므로 값이 생성되지 않을 수 있습니다. 따라서 "결과"를 사용하는 것은 흐릿합니다. computations평가 모델 하에서 RT 표현식이 항상 동일하다고 말할 수 있습니다 .

셋째, foo(50)다른 위치에서 다른 표현으로 프로그램에 두 가지가 나타나는 것을 볼 필요가있을 수 있습니다 . 각각 서로 다른 결과를 얻을 수 있습니다. 예를 들어, 언어가 동적 범위를 허용하면 어휘 적으로 동일하지만 두 표현식이 다릅니다. 펄에서 :

sub foo {
    my $x = shift;
    return $x + $y; # y is dynamic scope var
}

sub a {
    local $y = 10;
    return &foo(50); # expanded to 60
}

sub b {
    local $y = 20;
    return &foo(50); # expanded to 70
}

동적 범위 오도 그것이 쉬운 일이 생각 할 수 있도록하기 때문에이 x유일한 입력 foo현실에서,이 때, x하고 y. 차이점을 보는 한 가지 방법은 프로그램을 동적 범위가없는 동등한 것으로 변환하는 것입니다. 즉, 매개 변수를 명시 적으로 전달하는 대신 정의하는 대신 호출자에서 명시 적으로 foo(x)정의 foo(x, y)하고 전달 y합니다.

요점은, 우리는 항상 function사고 방식에있다 : 표현에 대한 특정 입력이 주어지면, 상응하는 "결과"가 주어진다. 동일한 입력을 주면 항상 동일한 "결과"를 기대해야합니다.

이제 다음 코드는 어떻습니까?

def foo():
   global y
   y = y + 1
   return y

y = 10
foo() # yields 11
foo() # yields 12

foo재정의가 있기 때문에 절차는 RT를 나누기. 즉, 우리 y는 한 지점에서 정의하고 나중에는 동일하게 재정의했습니다 y. 위의 perl 예제에서 ys는 서로 다른 바인딩이지만 동일한 문자 이름 "y"를 공유합니다. 여기서 ys는 실제로 동일합니다. 이것이 우리가 (재) 할당이 메타 작업 이라고 말하는 이유 입니다. 실제로 프로그램의 정의를 변경하고 있습니다.

대략 사람들은 일반적으로 다음과 같이 차이점을 묘사합니다. 부작용이없는 환경에서는의 매핑이 있습니다 input -> output. "제 국적"설정에서는 시간에 따라 변경 될 수 input -> ouput있는 컨텍스트 state가 있습니다.

이제는 해당 값을 표현식으로 대체하는 대신, state각 연산마다 변환을 적용 해야합니다 (물론 state계산을 수행하기 위해 표현식을 참조 할 수도 있음 ).

따라서 부작용이없는 프로그램에서 식을 계산하기 위해 알아야 할 모든 것이 입력일 경우, 명령형 프로그램에서 각 계산 단계에 대한 입력과 전체 상태를 알아야합니다. 추론은 처음으로 큰 타격을 입었습니다 (문제가있는 절차를 디버그하려면 입력 코어 덤프 가 필요함 ). 메모와 같은 특정 트릭은 실용적이지 않습니다. 그러나 동시성과 병렬 처리는 훨씬 더 어려워집니다.


1
메모를 언급 한 것이 좋습니다. 외부에서 볼 수없는 내부 상태의 예로 사용할 수 있습니다. 메모를 사용하는 함수는 내부적으로 상태와 돌연변이를 사용하더라도 참조 적으로 투명합니다.
Giorgio
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.