이것이 순수한 기능입니까?


117

대부분의 소스 는 다음 두 가지 속성을 갖는 순수한 함수를 정의합니다.

  1. 반환 값은 동일한 인수에 대해 동일합니다.
  2. 평가에는 부작용이 없습니다.

그것은 나에게 관련된 첫 번째 조건입니다. 대부분의 경우 판단하기 쉽습니다. 이 기사에 표시된대로 다음 JavaScript 함수를 고려 하십시오. )

순수한:

const add = (x, y) => x + y;

add(2, 4); // 6

더러운:

let x = 2;

const add = (y) => {
  return x += y;
};

add(4); // x === 6 (the first time)
add(4); // x === 10 (the second time)

두 번째 함수가 후속 호출에 대해 다른 출력을 제공하여 첫 번째 조건을 위반한다는 것을 쉽게 알 수 있습니다. 따라서 불순합니다.

내가 얻는이 부분.


이제 내 질문에 대해 주어진 금액을 달러로 유로로 변환하는이 함수를 고려하십시오.

(편집- const첫 번째 줄에서 사용 let. 실수로 이전에 사용됨 )

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

db에서 환율을 가져오고 매일 변경한다고 가정합니다.

자, 오늘 이 함수를 몇 번이나 호출 하더라도 입력에 대해 동일한 출력을 제공합니다.100 . 그러나 내일 다른 출력을 줄 수 있습니다. 이것이 첫 번째 조건을 위반하는지 확실하지 않습니다.

함수 자체에는 입력을 변경하는 논리가 포함되어 있지 않지만 앞으로 변경 될 수있는 외부 상수에 의존합니다. 이 경우 매일 바뀔 것이라고 확신합니다. 다른 경우에는 발생할 수 있습니다. 그렇지 않을 수도 있습니다.

그러한 함수를 순수한 함수라고 부를 수 있습니까? 대답이 '아니요'라면 어떻게 리팩터링 할 수 있습니까?


6
JS 같은 그런의 동적 언어의 순도는 매우 복잡한 주제 :function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);
zerkms

29
순도는 프로그램 동작을 변경하지 않고 코드 수준에서 함수 호출을 결과 값으로 대체 할 수 있음을 의미합니다.
bob

1
부작용을 구성하고 이론적 인 용어로 더 자세한 내용을 보려면 cs.stackexchange.com/questions/116377/…을
Gilles 'SO-Stop

3
오늘의 기능은 (x) => {return x * 0.9;}입니다. 내일, 당신은 또한 아마도 순수한 다른 기능을 가질 것 (x) => {return x * 0.89;}입니다. 실행할 (x) => {return x * exchangeRate;}때 마다 함수 가 작성 되며 해당 함수는 exchangeRate변경할 수 없기 때문에 순수 합니다.
user253751 2018

2
이것은 당신이 순수한, 당신이 사용할 수있는 확인하려면, 불순한 기능이없는 const dollarToEuro = (x, exchangeRate) => { return x * exchangeRate; }; , 순수한 기능을 위해 Its return value is the same for the same arguments.1 년간 .. 이후에는 어떤 문제가 항상 유지해야 일초
Vikash Tiwari 보낸

답변:


133

dollarToEuro의 반환 값은 인수하지 않은 외부 변수에 따라 달라집니다; 따라서 기능이 불완전합니다.

대답은 '아니요'입니다. 그렇다면 어떻게 함수를 순수하게 리팩터링 할 수 있습니까?

하나의 옵션은에 전달하는 것입니다 exchangeRate. 이런 식으로 인수가 일 때마다 (something, somethingElse)출력은 다음과 같이 보장 됩니다 something * somethingElse.

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

함수형 프로그래밍의 경우 피해야합니다. let항상 const재 할당을 피하기 위해 사용하십시오 .


6
자유 변수를 가지고하지 않는 것은 아니다 : 함수가 순수하기위한 요구 사항 const add = x => y => x + y; const one = add(42);여기에 모두 addone순수 함수입니다.
zerkms

7
const foo = 42; const add42 = x => x + foo;<-이것은 또 다른 순수한 함수이며 자유 변수를 다시 사용합니다.
zerkms

8
@zerkms-CertainPerformance가 다른 용어를 사용한다고 단언 하더라도이 질문에 대한 귀하의 답변을보고 싶어합니다. 나는 그것이 복제 될 것이라고 생각하지 않으며, 특히 인용 될 때 (위의 Wikipedia 기사보다 더 좋은 출처를 가지고 있지만, 그것이 우리가 얻는 전부라면 여전히 승리 할 것입니다.) (어떤 종류의 부정적인 빛으로이 의견을 읽기가 쉬울 것입니다. 제가 진실하다는 것을 믿으십시오. 그러한 답변이 훌륭하고 그것을 읽고 싶습니다.)
TJ Crowder

17
나는 당신과 @zerkms가 모두 틀렸다고 생각합니다. 당신은 dollarToEuro당신의 대답에서 예제 의 함수가 자유 변수에 의존하기 때문에 불완전 하다고 생각하는 것 같습니다 exchangeRate. 터무니없는 말입니다. zerkms가 지적했듯이 함수의 순도는 자유 변수가 있는지 여부와 관련이 없습니다. 그러나 zerkms는 데이터베이스 의 dollarToEuro기능에 따라 기능이 불완전하다고 생각하기 때문에 잘못되었습니다 exchangeRate. 그는 "IO에 전 이적으로 의존하기 때문에 불순하다"고 말했다.
Aadit M Shah

9
(cont) 다시 말하지만, 그것은 자유 변수 dollarToEuro이기 때문에 불순 하다는 것을 암시하기 때문에 터무니 없습니다 exchangeRate. exchangeRate자유 변수가 아닌 경우 , 즉 인수 인 dollarToEuro경우 순수함을 나타냅니다. 그러므로 그것은 dollarToEuro(100)불순하지만 dollarToEuro(100, exchangeRate)순수한 것을 암시합니다 . 두 경우 모두 exchangeRate데이터베이스에서 제공되는 데이터에 의존하기 때문에 분명히 터무니 없습니다 . 유일한 차이점은 함수 exchangeRate내에서 자유 변수 인지 여부 dollarToEuro입니다.
Aadit M Shah

76

기술적으로 컴퓨터에서 실행하는 모든 프로그램은 결국 "이 값을 다음으로 이동 eax"및 "이 값을eax 불완전합니다. 그다지 도움이되지 않습니다.

대신 블랙 박스를 사용한 순도에 대해 생각 합니다. 동일한 입력이 주어질 때 일부 코드가 항상 동일한 출력을 생성하는 경우 순수한 것으로 간주됩니다. 이 정의에 따르면 내부적으로 불완전한 메모 테이블을 사용하더라도 다음 함수도 순수합니다.

const fib = (() => {
    const memo = [0, 1];

    return n => {
      if (n >= memo.length) memo[n] = fib(n - 1) + fib(n - 2);
      return memo[n];
    };
})();

console.log(fib(100));

순도를 확인하기 위해 블랙 박스 방법을 사용하기 때문에 내부에 신경 쓰지 않습니다. 마찬가지로 블랙 박스 방법론을 사용하여 순도에 대해 생각하고 있기 때문에 모든 코드가 결국 기계 명령을 부정확하게 변환하는 것을 신경 쓰지 않습니다. 내부는 중요하지 않습니다.

이제 다음 기능을 고려하십시오.

const greet = name => {
    console.log("Hello %s!", name);
};

greet("World");
greet("Snowman");

은 IS greet기능은 순수 또는 불순한? 블랙 박스 방법론에 따라 동일한 입력 (예 :) World을 제공하면 항상 동일한 출력을 화면에 인쇄합니다 (예 :) Hello World!. 그런 의미에서 순수하지 않습니까? 아뇨. 순수하지 않은 이유는 화면에 무언가를 인쇄하는 것이 부작용이기 때문입니다. 우리의 블랙 박스가 부작용을 일으키면 순수하지 않습니다.

부작용은 무엇입니까? 이것이 참조 투명성 의 개념 이 유용한 곳입니다. 함수가 참조 적으로 투명한 경우 해당 함수의 응용 프로그램을 항상 결과로 바꿀 수 있습니다. 이것은 함수 인라이닝 과 동일하지 않습니다 .

함수 인라이닝에서 우리는 프로그램의 의미를 변경하지 않고 함수 적용을 함수 본문으로 대체합니다. 그러나 참조 적으로 투명한 기능은 프로그램의 의미를 변경하지 않고 항상 리턴 값으로 대체 될 수 있습니다. 다음 예를 고려하십시오.

console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");

여기서 우리는 정의를 인라인 greet하고 프로그램의 의미를 변경하지 않았습니다.

이제 다음 프로그램을 고려하십시오.

undefined;
undefined;

여기서는 greet함수 의 응용 프로그램을 반환 값으로 바꾸었고 프로그램의 의미를 변경했습니다. 더 이상 인사말을 화면에 인쇄하지 않습니다. 그것이 인쇄가 부작용으로 간주되는 이유이며, 그 이유는greet 기능이 불완전한 입니다. 참조 용으로 투명하지 않습니다.

이제 다른 예를 살펴 보겠습니다. 다음 프로그램을 고려하십시오.

const main = async () => {
    const response = await fetch("https://time.akamai.com/");
    const serverTime = 1000 * await response.json();
    const timeDiff = time => time - serverTime;
    console.log("%d ms", timeDiff(Date.now()));
};

main();

분명히 main기능은 불완전합니다. 그러나 그 timeDiff기능은 순수하거나 불완전합니까? serverTime불완전한 네트워크 호출에서 오는 것이 무엇인지에 따라 다르지만 동일한 입력에 대해 동일한 출력을 반환하고 부작용이 없기 때문에 참조 적으로 투명합니다.

zerkms 는 아마도이 시점에서 나와 동의하지 않을 것입니다. 그의 대답 에서 그는 dollarToEuro다음 예제 의 기능은 "IO에 전 이적으로 의존하기 때문에"불완전하다고 말했다.

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

exchangeRate데이터베이스 에서 나온 사실 이 관련이 없기 때문에 그와 동의하지 않아야합니다. 내부 세부 사항이며 함수의 순도를 결정하는 블랙 박스 방법론은 내부 세부 사항에 신경 쓰지 않습니다.

Haskell과 같은 순전히 기능적인 언어에는 임의의 IO 효과를 실행하기위한 탈출구가 있습니다. 이 unsafePerformIO이름은이며 이름에서 알 수 있듯이 올바르게 사용하지 않으면 참조 투명성이 손상 될 수 있으므로 안전하지 않습니다. 그러나 당신이하고있는 일을 알고 있다면 사용하는 것이 안전합니다.

일반적으로 프로그램 시작 부분의 구성 파일에서 데이터를로드하는 데 사용됩니다. 구성 파일에서 데이터를로드하는 것은 부적절한 IO 작업입니다. 그러나 데이터를 모든 함수에 입력으로 전달하여 부담을 느끼고 싶지는 않습니다. 따라서 우리가 사용한다면unsafePerformIO 최상위 수준에서 데이터를로드 할 수 있으며 모든 순수 기능은 변경 불가능한 전역 구성 데이터에 따라 달라질 수 있습니다.

함수가 구성 파일, 데이터베이스 또는 네트워크 호출에서로드 된 일부 데이터에 의존한다고해서 함수가 불완전하다는 것을 의미하지는 않습니다.

그러나 의미가 다른 원래 예제를 고려하십시오.

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

여기서는로 exchangeRate정의되지 않았기 때문에 const프로그램이 실행되는 동안 수정 될 것이라고 가정합니다 . 그 경우 dollarToEuro에는 확실히 불순한 기능이므로exchangeRate 수정되면 참조 투명성이 깨지기 입니다.

그러나 exchangeRate변수가 수정되지 않고 향후 수정되지 않을 경우 (즉, 상수 값인 경우)로 정의되어 있어도 let참조 투명성을 손상시키지 않습니다. 이 경우 dollarToEuro실제로 순수한 기능입니다.

exchangeRate프로그램을 다시 실행할 때마다 의 값이 변경 될 수 있으며 참조 투명성을 손상시키지 않습니다. 프로그램이 실행되는 동안 변경 될 경우에만 참조 투명성을 해제합니다.

예를 들어, timeDiff예제를 여러 번 실행 하면 다른 값 serverTime과 다른 결과를 얻을 수 있습니다. 그러나 serverTime프로그램이 실행되는 동안에는 값이 변경되지 않으므로 timeDiff기능이 순수합니다.


3
이것은 매우 유익했습니다. 감사. 그리고 나는 const내 예에서 사용 하려고했습니다.
눈사람

3
당신이 사용하는 것을 의미했다면 constdollarToEuro기능은 실제로 순수합니다. exchangeRate프로그램 의 값을 변경 하는 유일한 방법 은 프로그램을 다시 실행하는 것입니다. 이 경우 이전 프로세스와 새 프로세스가 다릅니다. 따라서 참조 투명성을 손상시키지 않습니다. 다른 인수로 함수를 두 번 호출하는 것과 같습니다. 인수는 다를 수 있지만 함수 내에서 인수의 값은 일정하게 유지됩니다.
Aadit M 샤

3
이것은 상대성에 대한 작은 이론처럼 들립니다. 상수는 절대적으로가 아니라 상대적으로 일정합니다. 분명히 여기에 유일한 정답입니다. +1.
bob

5
나는 동의는 "EAX의 내용이 값을 추가 EAX로이 값을 이동"와 ""그것이 결국 같은 지시 아래로 컴파일하기 때문에 불순한입니다 " . 만약이 eax로드 또는 명확한를 통해 - - 클리어 코드에 관계없이 결정 남아 다른 일이 일어나고 있기 때문에 순수합니다 그렇지 않으면 매우 포괄적 인 답변
3Dave

3
@Bergi : 사실, 불변의 값을 가진 순수한 언어에서, 정체성은 관련이 없습니다. 같은 값으로 평가 개의 참조 할 수있는 동일한 개체 또는 다른 개체에 두 참조 여부 관찰되는 돌연변이 참조 중 하나를 통해 객체를 다른 기준으로 검색 할 때의 값도 변경 여부를 관찰이. 돌연변이가 없으면 정체성은 무의미해진다. (AS 리치 키스 마크 야 말할 것이다 : 신원은 시간이 지남에 따라 미국의 시리즈입니다.)
요 르그 W MITTAG

23

나 순수 주의자에 대한 답변 (이 질문에는 공식적인 "올바른"답변 이 하나도 없기 때문에 "나"는 말 그대로 나입니다 )

JS와 같은 동적 인 언어에서 원숭이 패치 기본 유형에 대한 많은 가능성이 있거나 기능을 사용하여 사용자 정의 유형을 구성 Object.prototype.valueOf하는 것은 함수를 보는 것만으로 순수한지 여부를 알 수 없습니다. 부작용을 일으키기 위해.

데모 :

const add = (x, y) => x + y;

function myNumber(n) { this.n = n; };
myNumber.prototype.valueOf = function() {
    console.log('impure'); return this.n;
};

const n = new myNumber(42);

add(n, 1); // this call produces a side effect

실용 주의자의 답변 :

로부터 위키 피 디아에서 매우 정의

컴퓨터 프로그래밍에서 순수한 함수는 다음과 같은 특성을 가진 함수입니다.

  1. 리턴 값은 동일한 인수에 대해 동일합니다 (로컬 정적 변수, 로컬이 아닌 변수, 변경 가능한 참조 인수 또는 I / O 장치의 입력 스트림의 변형 없음).
  2. 평가에는 부작용이 없습니다 (로컬 정적 변수, 비 로컬 변수, 가변 참조 인수 또는 I / O 스트림의 돌연변이 없음).

다시 말해서 함수의 구현 방식이 아니라 함수의 작동 방식 만 중요합니다. 그리고 특정 함수가 이러한 두 가지 속성을 유지하는 한 정확히 어떻게 구현되었는지에 관계없이 순수합니다.

이제 당신의 기능에 :

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

그것은 요구 사항을 충족시키지 못하기 때문에 불완전합니다 .2 : 전 이적으로 IO에 의존합니다.

위의 내용이 잘못되었다는 데 동의합니다. 자세한 내용은 다른 답변을 참조하십시오 : https://stackoverflow.com/a/58749249/251311

기타 관련 자료 :


4
me답변을 제공하는 zerkms 로 @TJCrowder.
zerkms

2
예, Javascript를 사용하는 것은 보증이 아니라 자신감에 관한 것입니다.
bob

4
@bob ... 또는 차단 통화입니다.
zerkms

1
@zerkms-감사합니다. 100 % 확신합니다. 귀하 add42와 나의 주요 차이점 addX은 순전히 내 x변경 될 수 있고 귀하의 ft변경이 불가능하다는 것입니다 (따라서 add42'반환 값은에 따라 달라지지 않습니다 ft)?
TJ Crowder

5
dollarToEuro귀하의 예 에서 기능이 불완전 하다는 데 동의하지 않습니다 . 나는 왜 대답에 동의하지 않는지 설명했다. stackoverflow.com/a/58749249/783743
Aadit M Shah

14

다른 답변이 말한 것처럼, 방법은 당신이 구현 한 dollarToEuro,

let exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => { return x * exchangeRate; }; 

프로그램이 실행되는 동안 환율이 업데이트되지 않기 때문에 실제로 순수합니다. 그러나 개념적 dollarToEuro으로는 최신 환율이 무엇이든 사용한다는 점에서 불완전한 기능이어야합니다. 이러한 차이를 설명하는 가장 간단한 방법은 구현하지 않은 것입니다 dollarToEurodollarToEuroAtInstantOfProgramStart.

여기서 핵심은 통화 변환을 계산하는 데 필요한 몇 가지 매개 변수가 있으며, 순수한 버전의 장군 dollarToEuro이 모든 매개 변수를 제공한다는 것입니다. 가장 직접적인 매개 변수는 변환 할 USD의 금액과 환율입니다. 그러나 게시 된 정보에서 환율을 얻으려면 이제 세 가지 매개 변수를 제공해야합니다.

  • 교환 할 금액
  • 환율 상담을위한 역사적 권한
  • 트랜잭션이 발생한 날짜 (역사 권한을 색인화하기 위해)

여기의 역사적 권한은 데이터베이스이며 데이터베이스가 손상되지 않았다고 가정하면 특정 날짜의 환율에 대해 항상 동일한 결과를 반환합니다. 따라서이 세 가지 매개 변수를 조합하면 완전히 순수하고 자급 자족 버전의 general을 작성할 수 있습니다 dollarToEuro.

function dollarToEuro(x, authority, date) {
    const exchangeRate = authority(date);
    return x * exchangeRate;
}

dollarToEuro(100, fetchFromDatabase, Date.now());

구현은 함수가 작성된 순간에 히스토리 권한 및 트랜잭션 날짜 모두에 대한 상수 값을 캡처합니다. 히스토리 권한은 데이터베이스이며 캡처 된 날짜는 프로그램을 시작한 날짜입니다. 남은 것은 달러 금액입니다. 발신자가 제공하는 불완전한 버전은 dollarToEuro항상 최신 값을 가져옵니다. 본질적으로 날짜 매개 변수를 암시 적으로 가져와 함수가 호출되는 순간으로 설정합니다. 동일한 매개 변수를 가진 함수를 두 번 호출 할 수 없기 때문에 순수한 것은 아닙니다.

순수한 버전을 원하면 dollarToEuro최신 값을 얻을 수 있지만 여전히 기록 기관을 바인딩 할 수는 있지만 date 매개 변수는 바인딩되지 않은 상태로두고 호출자의 날짜를 인수로 요청하여 끝납니다. 이런 식으로 :

function dollarToEuro(x, date) {
    const exchangeRate = fetchFromDatabase(date);
    return x * exchangeRate;
}

dollarToEuro(100, Date.now());

@Snowman 천만에요! 더 많은 코드 예제를 추가하기 위해 답변을 약간 업데이트했습니다.
TheHansinator

8

JS의 특정 세부 사항과 형식적 정의의 추상화에서 약간 철회하고 특정 최적화를 가능하게하기 위해 어떤 조건을 유지해야하는지 이야기하고 싶습니다. 일반적으로 코드를 작성할 때주의를 기울여야합니다 (정확성을 증명하는데도 도움이 됨). 함수형 프로그래밍은 최신 유행에 대한 가이드 나 자기 부정의 맹세가 아닙니다. 문제를 해결하는 도구입니다.

다음과 같은 코드가있을 때 :

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

exchangeRate대한 두 호출간에 수정 dollarToEuro(100)이 불가능한 경우 첫 번째 호출의 결과를 메모 하고 두 번째 호출을 dollarToEuro(100)최적화 할 수 있습니다. 결과는 동일하므로 이전의 값을 기억할 수 있습니다.

검색 exchangeRate하고 수정하지 않은 함수를 호출하기 전에가 한 번 설정 될 수 있습니다. 덜 제한적 exchangeRate으로 특정 기능이나 코드 블록을 한 번 찾아보고 해당 범위 내에서 동일한 환율을 일관되게 사용하는 코드가있을 수 있습니다 . 또는이 스레드 만 데이터베이스를 수정할 수있는 경우 환율을 업데이트하지 않은 경우 다른 사용자가 데이터베이스를 변경하지 않았다고 가정 할 수 있습니다.

경우 fetchFromDatabase()자체가 상수로 평가 순수한 기능이며, exchangeRate불변, 우리는이 상수를 계산을 통해 모든 방법을 접을 수 있습니다. 이 경우를 알고있는 컴파일러는 주석에서 수행 한 것과 동일한 추론을 수행 dollarToEuro(100)하여 90.0으로 평가하고 전체 표현식을 상수 90.0으로 대체 할 수 있습니다.

그러나 fetchFromDatabase()부작용으로 간주되는 I / O를 수행하지 않으면 이름이 Least Astonishment의 원칙에 위배됩니다.


8

이 함수는 순수하지 않으며 외부 변수에 의존하며 거의 변할 것입니다.

따라서 함수는 첫 번째 포인트를 실패하며 동일한 인수에 대해 동일한 값을 리턴하지 않습니다.

이 함수를 "순수하게" exchangeRate하려면 인수로 전달하십시오.

그러면 두 가지 조건이 모두 충족됩니다.

  1. 동일한 값과 환율을 전달할 때 항상 동일한 값을 반환합니다.
  2. 또한 부작용이 없습니다.

예제 코드 :

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

dollarToEuro(100, fetchFromDatabase())

1
"거의 확실하게 변할 것입니다"--- 아닙니다 const.
zerkms 2018

7

다른 사람들이 참조 투명성에 대해 지적한 점을 확장하려면 순도를 단순히 함수 호출의 참조 투명성으로 정의 할 수 있습니다 (즉, 함수의 모든 호출은 프로그램의 의미를 변경하지 않고 반환 값으로 대체 할 수 있음).

제공하는 두 가지 속성은 모두 참조 투명도의 결과 입니다. 예를 들어, 다음 함수 f1는 매번 같은 결과를 제공하지 않기 때문에 불완전합니다 (1의 번호를 매긴 속성).

function f1(x, y) {
  if (Math.random() > 0.5) { return x; }
  return y;
}

매번 같은 결과를 얻는 것이 왜 중요합니까? 다른 결과를 얻는 것이 함수 호출이 값과 다른 의미를 갖는 한 가지 방법이므로 참조 투명성을 깨뜨립니다.

코드를 작성하고 코드 f1("hello", "world")를 실행하고 리턴 값을 얻는다고 가정 해 봅시다 "hello". 모든 통화를 찾거나 교체하면f1("hello", "world") 하고이를 대체 "hello"하면 프로그램의 의미가 변경됩니다 (모든 호출은 이제로 대체 "hello"되지만 원래 약 절반은로 평가됨 "world"). 따라서 호출 f1은 참조 적으로 투명하지 않으므로 f1불완전합니다.

함수 호출이 다른 의미를 가질 수있는 또 다른 방법은 명령문을 실행하는 것입니다. 예를 들면 다음과 같습니다.

function f2(x) {
  console.log("foo");
  return x;
}

의 반환 값 f2("bar")은 항상"bar" 이지만 값의 의미 "bar"는 호출과 다릅니다 f2("bar"). 후자는 콘솔에 기록되기 때문입니다. 하나를 다른 것으로 바꾸면 프로그램의 의미가 변경되므로 참조 적으로 투명하지 않으므로 f2불완전합니다.

당신의 여부 dollarToEuro함수가 참조 적으로 투명하고 순수한지 두 가지에 달려 있습니다.

  • 우리가 참조 적으로 투명하다고 생각하는 것의 '범위'
  • 여부 exchangeRate'범위'내에서 변화가

사용할 "최상의"범위는 없습니다. 일반적으로 프로그램의 단일 실행 또는 프로젝트 수명에 대해 생각합니다. 비유로, 모든 함수의 반환 값이 캐시된다고 상상해보십시오 (@ aadit-m-shah가 제공 한 예의 메모 테이블과 같이). 의미론?

exchangeRate사용 중이 면 에 대한 var각 호출간에 변경 될 수 있습니다 dollarToEuro. 각 호출간에 캐시 된 결과를 지워야하므로 참조 투명성이 없습니다.

사용하여 const우리가 프로그램의 실행에 대한 '범위'를 확대하고 있습니다 : 그것은의 캐시 반환 값에 안전 할 것이다 dollarToEuro프로그램이 완료 될 때까지. 함수 호출을 반환 값으로 대체하기 위해 Lisp와 같은 언어로 매크로를 사용하는 것을 상상할 수 있습니다. 이 순도는 구성 값, 명령 줄 옵션 또는 고유 ID와 같은 항목에 공통입니다. 우리는 프로그램의 하나 실행에 대해 생각에 자신을 제한하는 경우, 우리는 순도의 장점을 최대한 활용하지만, 우리는 조심해야 에서 (예를 들어, 다음 다른 실행에 넣기, 파일에 데이터를 저장) 실행됩니다. 나는에서 "순수"와 같은 함수를 호출하지 않을 추상적 인 의미 (예를 들어, 내가 사전 정의를 작성 한 경우),하지만 순수로 치료에 아무런 문제가없는 상황에서 .

우리가 프로젝트의 수명을 '범위'로 취급한다면, 우리는 "가장 참조 적으로 투명"하기 때문에 "추상적"이라고 할 수 있습니다. 가상 캐시를 지울 필요는 없습니다. 호출을 반환 값으로 대체하기 위해 디스크에서 소스 코드를 직접 다시 작성하여 "캐싱"을 수행 할 수도 있습니다. 이것은 모든 프로젝트에서 작동 합니다. 예를 들어, 함수 데이터베이스와 그 반환 값에 대한 온라인 데이터베이스를 상상할 수 있습니다. 여기서 누군가가 함수 호출을 조회하고 (DB에있는 경우) 다른 쪽의 누군가가 제공 한 반환 값을 사용할 수 있습니다. 몇 년 전에 다른 프로젝트에서 동일한 기능을 사용한 세계.


4

서면으로, 그것은 순수한 기능입니다. 부작용이 없습니다. 이 함수에는 하나의 공식 매개 변수가 있지만 두 개의 입력이 있으며 두 입력에 대해 항상 동일한 값을 출력합니다.


2

그러한 함수를 순수한 함수라고 부를 수 있습니까? 대답이 '아니요'라면 어떻게 리팩터링 할 수 있습니까?

정식으로 언급했듯이 "내일 다른 출력을 줄 수 있습니다" . 이 경우 대답은 "no" 로 울릴 것 입니다. 의도 한 동작이 dollarToEuro다음과 같이 올바르게 해석 된 경우 특히 그렇습니다 .

const dollarToEuro = (x) => {
  const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;
  return x * exchangeRate;
};

그러나 순수한 것으로 간주되는 다른 해석이 있습니다.

const dollarToEuro = ( () => {
    const exchangeRate =  fetchFromDatabase();

    return ( x ) => x * exchangeRate;
} )();

dollarToEuro 바로 위가 순수합니다.


소프트웨어 엔지니어링 관점 dollarToEuro에서 함수에 대한 종속성을 선언 해야합니다 fetchFromDatabase. 따라서 dollarToEuro다음과 같이 정의를 리팩터링하십시오 .

const dollarToEuro = ( x, fetchFromDatabase ) => {
  return x * fetchFromDatabase();
};

이 결과를 통해 fetchFromDatabase만족스럽게 기능 한다는 전제를 고려할 때 fetchFromDatabaseon 의 투영 dollarToEuro이 만족 스러워야 한다는 결론을 내릴 수 있습니다 . 또는 성명은 " fetchFromDatabase의미 순수한은" dollarToEuro이후 (순수 fetchFromDatabaseA는 기준dollarToEuro의 스칼라 배는 x.

원래 게시물에서 나는 그것이 fetchFromDatabase기능 시간 이라는 것을 이해할 수 있습니다 . 이해를 투명하게하기 위해 리팩토링 노력을 개선하여 fetchFromDatabase순수한 기능으로 명확하게 규정하자 :

fetchFromDatabase = (타임 스탬프) => {/ * 여기에 구현이 진행됩니다 * /};

궁극적으로 다음과 같이 기능을 리팩터링합니다.

const fetchFromDatabase = ( timestamp ) => { /* here goes the implementation */ };

// Do a partial application of `fetchFromDatabase` 
const exchangeRate = fetchFromDatabase.bind( null, Date.now() );

const dollarToEuro = ( dollarAmount, exchangeRate ) => dollarAmount * exchangeRate();

결과적 dollarToEuro으로 단순히 올바르게 호출 fetchFromDatabase(또는 그 파생물 exchangeRate)을 입증함으로써 단위 테스트를 수행 할 수 있습니다 .


1
이것은 매우 밝았습니다. +1. 감사.
눈사람

귀하의 답변이 더 유익하고 아마도 특정 사용 사례에 대한 더 나은 리팩토링을 찾으십시오 dollarToEuro. OP에서 다른 사용 사례가있을 수 있다고 언급했습니다. 나는 내가하려는 일을 즉시 불러 일으키기 때문에 dollarToEuro를 선택했지만 자유 변수에 의존하는 미묘한 것이있을 수 있지만 시간의 함수는 아닙니다. 그것을 염두에두고, 나는 topvoted 리 팩터가 더 접근하기 쉬운 리 팩터이며 유사한 유스 케이스를 가진 다른 사람들을 도울 수있는 리 팩터라고 생각합니다. 도움을 주셔서 감사합니다.
눈사람

-1

저는 Haskell / JS 이중 언어이고 Haskell은 함수 순도에 대해 큰 영향을 미치는 언어 중 하나이므로 Haskell이 그것을 어떻게 보는지에 대한 관점을 제시 할 것이라고 생각했습니다.

다른 사람들이 말했듯이, Haskell에서는 가변 변수를 읽는 것이 일반적으로 불완전한 것으로 간주됩니다. 변수가 나중에 변경 될 수 있다는 점에서 변수정의 에는 차이가 있으며 정의는 영원히 동일합니다. 따라서 당신 그것을 선언 const했다면 (그것이 단지 number변형 가능하고 내부 구조가 없다고 가정하면 ), 그 정의를 읽는 것은 순수한 정의를 사용하는 것입니다. 그러나 시간이 지남에 따라 변화하는 환율을 모델링하고 싶었습니다. 이는 일종의 변경이 필요하며 불순물이 생깁니다.

Haskell에서 이러한 종류의 불순한 것을 설명하기 위해 (우리는 "효과"라고 ​​부르고, "순수한"과 반대로 "유효한"효과를 사용할 수 있음) 메타 프로그래밍 이라고하는 것을 수행합니다 . 오늘날 메타 프로그래밍은 일반적으로 매크로 를 의미하는 것이 아니라, 일반적으로 다른 프로그램을 작성하기 위해 프로그램을 작성한다는 생각입니다.

이 경우, Haskell에서는 우리가 원하는 것을 수행 할 효과적인 프로그램을 계산하는 순수한 계산을 작성합니다. 따라서 Haskell 소스 파일 (적어도 라이브러리가 아닌 프로그램을 설명하는 파일)의 핵심은이라는 효과적인 프로그램에 대한 순수한 계산을 설명하는 것 main입니다. Haskell 컴파일러의 임무는이 소스 파일을 가져 와서 순수한 계산을 수행하고, 그 효과적인 프로그램을 하드 드라이브 어딘가에 나중에 실행하기 위해 바이너리 실행 파일로 넣는 것입니다. 즉, 순수한 계산이 실행되는 시간 (컴파일러가 실행 파일을 만드는 동안)과 효과적인 프로그램이 실행되는 시간 (실행 파일을 실행할 때마다) 사이에 차이가 있습니다.

따라서 우리에게 효과적인 프로그램은 실제로 데이터 구조 이며, 언급 된 것만으로 본질적으로 아무것도하지 않습니다 (반환 값 외에 부작용이 없으며, 그 리턴 값에는 효과가 있습니다). 불변의 프로그램과 그로 할 수있는 일을 기술하는 TypeScript 클래스의 초경량 예

export class Program<x> {
   // wrapped function value
   constructor(public run: () => Promise<x>) {}
   // promotion of any value into a program which makes that value
   static of<v>(value: v): Program<v> {
     return new Program(() => Promise.resolve(value));
   }
   // applying any pure function to a program which makes its input
   map<y>(fn: (x: x) => y): Program<y> {
     return new Program(() => this.run().then(fn));
   }
   // sequencing two programs together
   chain<y>(after: (x: x) => Program<y>): Program<y> {
    return new Program(() => this.run().then(x => after(x).run()));
   }
}

핵심은 Program<x>부작용이 발생하지 않았으며 완전히 기능적으로 순수한 엔티티라는 것입니다. 함수가 순수한 함수가 아닌 이상 프로그램에 함수를 맵핑하면 부작용이 없습니다. 두 프로그램의 시퀀싱에는 부작용이 없습니다. 기타

예를 들어 이것을 적용하는 방법의 예를 들어, ID로 사용자를 가져오고 데이터베이스를 변경하고 JSON 데이터를 가져 오기 위해 프로그램을 반환하는 순수한 함수를 작성할 수 있습니다.

// assuming a database library in knex, say
function getUserById(id: number): Program<{ id: number, name: string, supervisor_id: number }> {
    return new Program(() => knex.select('*').from('users').where({ id }));
}
function notifyUserById(id: number, message: string): Program<void> {
    return new Program(() => knex('messages').insert({ user_id: id, type: 'notification', message }));
}
function fetchJSON(url: string): Program<any> {
  return new Program(() => fetch(url).then(response => response.json()));
}

그리고 당신은 URL을 컬하고 일부 직원을 찾고 순수하게 기능적인 방식으로 상사에게 알리는 cron 작업을 설명 할 수 있습니다

const action =
  fetchJSON('http://myapi.example.com/employee-of-the-month')
    .chain(eotmInfo => getUserById(eotmInfo.id))
    .chain(employee => 
        getUserById(employee.supervisor_id)
          .chain(supervisor => notifyUserById(
            supervisor.id,
            'Your subordinate ' + employee.name + ' is employee of the month!'
          ))
    );

요점은 여기의 모든 단일 기능이 완전히 순수한 기능이라는 것입니다. 실제로 action.run()모션으로 설정하기 전까지는 실제로 아무 일도 일어나지 않았습니다 . 또한 다음과 같은 함수를 작성할 수 있습니다.

// do two things in parallel
function parallel<x, y>(x: Program<x>, y: Program<y>): Program<[x, y]> {
    return new Program(() => Promise.all([x.run(), y.run()]));
}

JS가 취소를 약속했다면 두 프로그램이 서로 경쟁하여 첫 번째 결과를 가져와 두 번째 프로그램을 취소 할 수 있습니다. (우리는 여전히 할 수 있지만,해야 할 일이 덜 명확 해집니다.)

마찬가지로 귀하의 경우 환율 변경을 설명 할 수 있습니다

declare const exchangeRate: Program<number>;

function dollarsToEuros(dollars: number): Program<number> {
  return exchangeRate.map(rate => dollars * rate);
}

exchangeRate변경 가능한 값으로 보이는 프로그램이 될 수있다,

let privateExchangeRate: number = 0;
export function setExchangeRate(value: number): Program<void> {
  return new Program(() => { privateExchangeRate = value; return Promise.resolve(undefined); });
}
export const exchangeRate: Program<number> = new Program(() => {
  return Promise.resolve(privateExchangeRate); 
});

그럼에도 불구하고이 함수 dollarsToEuros는 이제 숫자에서 숫자를 생성하는 프로그램에 이르기까지 순수한 함수이며 부작용이없는 프로그램에 대해 추론 할 수있는 결정적인 방정식으로 추론 할 수 있습니다.

물론 비용은 결국 .run() 어딘가에 그것을 호출해야 하며 이는 불완전 할 것입니다. 그러나 계산의 전체 구조는 순수한 계산으로 설명 할 수 있으며 불순물을 코드의 여백으로 밀어 넣을 수 있습니다.


나는 이것이 왜 다운 다운을 계속하는지 궁금하지만, 나는 여전히 그것을지지하고 (실제로 기본적으로 순수한 곳의 Haskell에서 프로그램을 조작하는 방법이다) 다운 피어를 기꺼이 탱크에 넣을 것입니다. 하지만 다운 보더가 자신이 싫어하는 점을 설명하는 의견을 남기고 자한다면 개선을 시도 할 수 있습니다.
CR Drost

예, 물론 저자 외에 왜 그렇게 많은 다운 보트가 있지만 단일 의견이 없는지 궁금합니다.
Buda Örs
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.