참조 투명성

참조 투명성와 참조 투명도는 컴퓨터 프로그램의 일부 속성이다.  프로그램 동작의 변경없이 관련 값을 대체할 수 있다면 표현식을 참조 상 투명하다고 할 수 있다. 그 결과, 참조 상 투명한 함수를 평가하게 되면 동일한 인자에 대해 동일한 값을 반환해야 한다. 그러한 함수를 순수 함수라고 부른다. 참조상 투명하지 않은 표현식은 참조상 불투명 하다고 한다.
수학에서 모든 함수 응용은 수학적 함수를 구성하는 요소의 정의에 의해 참조 상 투명하다. 하지만, 프로그래밍에서는 그런 케이스가 항상 맞는 것은 아니며, 함축된 의미의 오해를 피하고자 프로시저와 메소드라는 용어가 사용된다. 함수형 프로그래밍에서는 오로지 참조 상 투명한 함수 만이 고려된다. 어떤 프로그래밍 언어들은 참조 투명성을 보장하기 위한 수단을 제공한다. 어떤 함수형 프로그래밍 언어들은 모든 함수에 대해 참조 투명성을 강제하기도 한다.
참조 투명성의 중요성은 프로그래머와 컴파일러가 프로그램 동작을 재작성 시스템으로써 추론할 수 있게 한다는 것이다. 이것은 정확성 증명, 알고리즘 단순화, 코드 깨짐없는 코드 수정의 원조, 메모이제이션, 공통 하위 표현식 제거, 지연 평가 혹은 병렬성을 수단으로 하는 코드 최적화를 도울 수 있다.
참조 투명성은 어떤 임의의 시점에 주어진 임의의 입력 집합에 대해 동일한 결과를 요구하는데, 그러므로 참조 상 투명한 표현식은 확정성을 갖는다.

역사

그 개념(용어는 아니지만)은 Alfred North Whitehead와 Bertrand Russel의 Principia Mathematica (1910-13)에서 시작된 것으로 보인다. 그것은 Word and Object (1960)에서 Willard Van Orman Quine이 쓴 분석 철학에서 채택되었다:
단일 용어 t가 용어 또는 문장 ψ(t)에서 순수하게 참조될 때마다, 포함하는 용어 혹은 문장 φ(ψ(t))에서 순수하게 참조되는 경우, 봉쇄 모드 φ는 참조 상 투명하다.
이 용어는 현재의 사용법, 프로그래밍 언어의 변수에 대한 논의, Christopher Strachey의 Fundamental Concepts in Programming Languages (1967) 독창적인 강의 노트 세트에서 나타난다. 해당 강의 노트는 참고 문헌에서 Quine의 Word and Object을 참조했다.

예제와 반증

표현식과 관련된 모든 함수가 순수 함수라면, 표현식은 참조 상 투명하다. 또한, 어떤 순수하지 않는 함수는 그 값이 폐기되거나 부작용이 사소한 것이라고 한다면 표현식 내에 포함될 수도 있다.
어떤 소스로부터 입력을 반환하는 함수를 생각해보자. 의사 코드에서, 해당 함수의 호출을 GetInput(Source)라고 할 때, Source는 특정 디스크, 키보드 등이 될 수 있다. Source의 식별 값 별로, 성공적인 반환 값이 다를 것이다. 그러므로, 함수 GetInput()은 확정적이지도 참조 상 투명하지도 않다.
좀 더 교묘한 예제는 자유 변수를 갖는 함수에 관한 것으로, 즉 명시적으로 매개 변수로 전달되지 않는 어떤 입력에 의존하는 함수다. 게다가, 전역 변수, (동적 바인딩을 위한) 현재 실행 환경의 변수 혹은 (정적 바인딩을 위한) 클로저 내 변수와 같이, 비 로컬 변수에 대한 이름 바인딩에 따라 분석된다. 이 변수가 매개 변수로 전달되는 값을 변경하지 않고 수정될 수 있다면, 해당 함수에 대한 뒤이은 호출의 결과는 매개 변수가 동일하다고 해도 다를 수 있다. 하지만, 순수 함수 프로그래밍에서 비구조화 할당은 허용되지 않으며, 그로 인해 자유 변수가 값에 정적으로 바운드되면, 정적 바인딩과 불변이성에 기인하여 비 로컬 변수와 그것의 값을 변경할 수 없으므로 함수는 여전히 참조 상 투명하다.
산술적 연산은 참조 상 투명하다: 예를 들어, 는 로 대체 가능하다. 사실, 수학적 관념에서 모든 함수는 참조 상 투명하다: 는 특정 에 대해 항상 같은 결과를 반환하므로 투명하다.
할당은 투명하지 않다. 예를 들어, C라는 표현식  은 변수 에 할당되는 값을 변경한다. 의 초기값을 10이라고 가정하면, 해당 표현식의 연속된 두 번의 평가는 각각 과 를 결과로 낸다. 분명히  을  혹은 로 대체하는 것은 프로그램에 다른 의미를 부여하므로, 해당 표현식은 참조 상 투명하지 않다. 하지만, int plusone(int x) {return x+1;}과 같은 함수를 호출하는 것은 투명하다. 암시적으로 입력 를 변경하지 않으며 그래서 부작용이 없기 때문이다.
today()는 투명하지 않은데, 그것을 평가하고 그것의 값을 대체하면("Jan 1, 2001"이라고 하자), 내일 실행할 때와 동일한 값이 아닐 것이다. 그 이유는 상태(날짜)에 의존하기 때문이다.
하스켈과 같은, 부작용이 없는 언어에서, 모든 값 에 대해 이기 때문에 equals를 equals로 대체할 수 있다. 이것은 부작용이 있는 언어에서는 불가능하다.

명령형 프로그래밍과의 대조

값을 갖는 표현식의 대체가 프로그램 실행의 특정 시점에서만 유효하다면, 그 표현식은 참조 상 투명하지 않다. 이런 시퀀스 포인트의 정의와 순서는 명령형 프로그래밍의 이론적 기초이며 명령형 프로그래밍 언어의 시맨틱의 일부이기도 하다.
하지만, 참조 상 투명한 표현식은 임의의 시점에 평가될 수 있으므로, 시퀀스 포인트 정의나 전체적인 평가 순서의 보장은 필요하지 않다. 이러한 것을 고려하지 않는 프로그래밍을 순수 함수형 프로그래밍이라고 부른다.
참조 상 투명한 스타일의 코드 작성의 한 가지 장점은 주어진 지능적인 컴파일러와 정적 코드 분석이 더 쉬워지며, 자동으로 더 나은 코드 향상 변형이 가능하다. 예를 들어, C로 프로그래밍을 할 때, 루프 내에서 비용이 비싼 함수에 대한 호출을 포함하고는 경우 성능 페널티가 있을텐데, 이는 함수 호출을 프로그램의 결과에 영향이 없도록 루프 밖으로 빼내는 경우에도 마찬가지다. 프로그래머는 아마도 소스 코드 가독성을 희생하여, 호출의 수동 코드 모션을 실행해야 할 것이다. 하지만, 해당 함수 호출이 참조 상 투명하다는 것을 컴파일러가 알 수 있다면, 이러한 변형을 자동으로 수행할 수 있다.
참조 투명성을 강제하는 언어의 주요 단점은 연속적인 단계를 갖는 명령형 프로그래밍의 스타일에 자연스럽게 맞춰진 연산 표현식을 더 어색하고 덜 간결하게 만든다는 것이다. 그러한 언어들은 명확한 문법과 모나드와 같은 언어의 순수 함수에 대한 성질을 유지하는 반면, 종종 앞서 말한 일들을 좀 더 쉽게 만들기 위한 메커니즘을 받아들인다.

또 다른 예제

예제를 보면, 두 개의 함수가 있는데, 하나는 참조 상 불투명하고 다른 하나는 참조 상 투명하다:

 int globalValue = 0;

 int rq(int x)
 {
   globalValue++;
   return x + globalValue;
 }

 int rt(int x)
 {
   return x + 1;
 }
함수 rt는 참조 상 투명한데, 이는 x = y일 때, rt(x) = rt(y)임을 의미한다. 예를 들어, rt(6) = 6 + 1 = 7, rt(4) = 4 + 1 = 5 등을 말한다. 하지만, 우리는 rq에 대해 그렇다고 얘기할 수 없는데 수정 가능한 글로벌 변수를 사용하기 때문이다.
rq의 참조 불투명성은 프로그램에 관한 추론을 더 어렵게 만든다. 예를 들어, 다음 구문에 관해 추론하려고 한다고 하자:

integer p = rq(x) + rq(y) * (rq(x) - rq(x));
누군가 이 구문을 다음과 같이 단순화하려고 할 수도 있다:

integer p = rq(x) + rq(y) * (0);
integer p = rq(x) + 0;
integer p = rq(x);
하지만, 이것은 rq에 적용할 수 없는데 rq(x)가 실행될 때마다 다른 값을 평가할 수 있기 때문이다. 전달되지 않고 rq에 대한 호출마다 수정될 수 있는 전역 변수에 rq의 반환 값이 의존하고 있음을 기억하자. 이것은 x - x = 0과 같은 수학적 정체성이 더 이상은 유지되지 않음을 의미한다.
그러한 수학적 정체성은 rt와 같은 참조 투명한 함수에게는 유지될 것이다.
하지만, 좀 더 복잡한 분석을 사용해 해당 구문을 다음과 같이 단순화할 수 있다:

integer a = globalValue; integer p = x + a + 1 + (y + a + 2) * (x + a + 3 - (x + a + 4)); globalValue = globalValue + 4;
integer a = globalValue; integer p = x + a + 1 + (y + a + 2) * (x + a + 3 - x - a - 4)); globalValue = globalValue + 4;
integer a = globalValue; integer p = x + a + 1 + (y + a + 2) * -1; globalValue = globalValue + 4;
integer a = globalValue; integer p = x + a + 1 - y - a - 2; globalValue = globalValue + 4;
integer p = x - y - 1; globalValue = globalValue + 4;
이것은 좀 더 많은 단계를 필요로 하며 컴파일러 최적화를 위해 실행 불가능한 코드에 대한 어느 정도의 통찰력을 요구한다.
그러므로 참조 투명성으로 인해 우리는 코드에 대해 추론할 수 있게 되어 좀 더 견고한 프로그램과 테스트를 통해 찾을 수 없는 버그를 발견할 가능성 그리고 최적화 기회 탐색에 대한 가능성을 높일 수 있을 것이다.