Tôi là một người song ngữ Haskell / JS và Haskell là một trong những ngôn ngữ tạo ra sự quan tâm lớn về độ tinh khiết của chức năng, vì vậy tôi nghĩ rằng tôi sẽ cung cấp cho bạn viễn cảnh từ cách Haskell nhìn thấy nó.
Như những người khác đã nói, trong Haskell, đọc một biến có thể thay đổi thường được coi là không trong sạch. Có một sự khác biệt giữa các biến và định nghĩa trong đó các biến có thể thay đổi sau đó, các định nghĩa là giống nhau mãi mãi. Vì vậy, nếu bạn đã khai báo nó const
(giả sử nó chỉ là mộtnumber
và không có cấu trúc bên trong có thể thay đổi), đọc từ đó sẽ sử dụng một định nghĩa, đó là thuần túy. Nhưng bạn muốn mô hình tỷ giá hối đoái thay đổi theo thời gian, và điều đó đòi hỏi một số khả năng biến đổi và sau đó bạn rơi vào tình trạng không tinh khiết.
Để mô tả những thứ không trong sạch đó (chúng ta có thể gọi chúng là hiệu ứng của Google, và cách sử dụng của chúng có hiệu lực, thay vì sử dụng tinh khiết) trong Haskell, chúng tôi làm những gì bạn có thể gọi là siêu lập trình . Ngày nay, siêu lập trình thường đề cập đến các macro không phải là ý tôi, mà chỉ là ý tưởng viết một chương trình để viết một chương trình khác nói chung.
Trong trường hợp này, trong Haskell, chúng tôi viết một phép tính thuần túy để tính toán một chương trình hiệu quả mà sau đó sẽ làm những gì chúng tôi muốn. Vì vậy, toàn bộ điểm của tệp nguồn Haskell (ít nhất, một tệp mô tả chương trình, không phải thư viện) là để mô tả một tính toán thuần túy cho một chương trình hiệu quả mà nó tạo ra main
. Sau đó, công việc của trình biên dịch Haskell là lấy tệp nguồn này, thực hiện tính toán thuần túy đó và đặt chương trình hiệu quả đó dưới dạng thực thi nhị phân ở đâu đó trên ổ cứng của bạn để chạy sau khi rảnh rỗi. Nói cách khác, có một khoảng cách, giữa thời điểm khi tính toán thuần túy chạy (trong khi trình biên dịch làm cho việc thực thi) và thời gian khi chương trình hiệu quả chạy (bất cứ khi nào bạn chạy thực thi).
Vì vậy, đối với chúng tôi, các chương trình hiệu quả thực sự là một cấu trúc dữ liệu và về bản chất chúng không làm bất cứ điều gì chỉ bằng cách được đề cập (chúng không có hiệu ứng * bên cạnh giá trị trả về; giá trị trả về của chúng chứa hiệu ứng của chúng). Đối với một ví dụ rất nhẹ về lớp TypeScript mô tả các chương trình bất biến và một số thứ bạn có thể làm với chúng,
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()));
}
}
Điều quan trọng là nếu bạn có Program<x>
thì không có tác dụng phụ nào xảy ra và đây là những thực thể hoàn toàn có chức năng. Ánh xạ một chức năng qua một chương trình không có bất kỳ tác dụng phụ nào trừ khi chức năng đó không phải là một chức năng thuần túy; giải trình tự hai chương trình không có bất kỳ tác dụng phụ nào; Vân vân.
Vì vậy, ví dụ về cách áp dụng điều này trong trường hợp của bạn, bạn có thể viết một số hàm thuần trả lại các chương trình để lấy người dùng bằng ID và thay đổi cơ sở dữ liệu và tìm nạp dữ liệu JSON, như
// 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()));
}
và sau đó bạn có thể mô tả một công việc định kỳ để cuộn URL và tra cứu một số nhân viên và thông báo cho người giám sát của họ theo cách hoàn toàn có chức năng như
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!'
))
);
Vấn đề là mọi chức năng đơn lẻ ở đây là một chức năng hoàn toàn thuần túy; không có gì thực sự xảy ra cho đến khi tôi thực sự action.run()
thiết lập nó thành chuyển động. Ngoài ra, tôi có thể viết các chức năng như,
// 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()]));
}
và nếu JS đã hủy bỏ lời hứa, chúng tôi có thể có hai chương trình đua nhau và lấy kết quả đầu tiên và hủy lần thứ hai. (Ý tôi là chúng ta vẫn có thể, nhưng nó trở nên ít rõ ràng hơn để làm gì.)
Tương tự trong trường hợp của bạn, chúng tôi có thể mô tả thay đổi tỷ giá hối đoái với
declare const exchangeRate: Program<number>;
function dollarsToEuros(dollars: number): Program<number> {
return exchangeRate.map(rate => dollars * rate);
}
và exchangeRate
có thể là một chương trình nhìn vào một giá trị có thể thay đổi,
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);
});
nhưng ngay cả như vậy, chức năng này dollarsToEuros
này bây giờ là một hàm thuần túy từ một số đến một chương trình tạo ra một số và bạn có thể suy luận về nó theo cách cân bằng xác định mà bạn có thể suy luận về bất kỳ chương trình nào không có tác dụng phụ.
Tất nhiên, chi phí là cuối cùng bạn phải gọi nó .run()
ở đâu đó , và điều đó sẽ không trong sạch. Nhưng toàn bộ cấu trúc tính toán của bạn có thể được mô tả bằng một tính toán thuần túy và bạn có thể đẩy tạp chất đến lề của mã.
function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);