저는 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()
어딘가에 그것을 호출해야 하며 이는 불완전 할 것입니다. 그러나 계산의 전체 구조는 순수한 계산으로 설명 할 수 있으며 불순물을 코드의 여백으로 밀어 넣을 수 있습니다.
function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);