ฉันเป็น Haskell / JS สองภาษาและ Haskell เป็นหนึ่งในภาษาที่ทำเรื่องใหญ่เกี่ยวกับฟังก์ชั่นความบริสุทธิ์ดังนั้นฉันคิดว่าฉันจะให้มุมมองจาก Haskell เห็นมัน
ดังที่คนอื่น ๆ ได้กล่าวไว้ใน Haskell การอ่านตัวแปรที่ไม่แน่นอนนั้นโดยทั่วไปถือว่าไม่บริสุทธิ์ มีความแตกต่างระหว่างตัวแปรและคำจำกัดความในตัวแปรที่สามารถเปลี่ยนแปลงได้ในภายหลังคำจำกัดความจะเหมือนกันตลอดไป ดังนั้นหากคุณได้ประกาศconst
แล้ว (สมมติว่ามันเป็นเพียงnumber
และไม่มีโครงสร้างภายในที่ไม่แน่นอน) การอ่านจากสิ่งนั้นจะใช้คำจำกัดความซึ่งบริสุทธิ์ แต่คุณต้องการสร้างแบบจำลองอัตราแลกเปลี่ยนที่เปลี่ยนแปลงไปตามกาลเวลาและนั่นต้องมีความไม่แน่นอนบางอย่างจากนั้นคุณก็จะกลายเป็นสิ่งสกปรก
เพื่ออธิบายสิ่งต่างๆประเภทนั้นไม่บริสุทธิ์ (เราสามารถเรียกพวกเขาว่า“ผลกระทบ” และการใช้งานของพวกเขา“effectful” เมื่อเทียบกับ“บริสุทธิ์”) ใน Haskell เราทำในสิ่งที่คุณอาจเรียกmetaprogramming metaprogramming วันนี้มักจะหมายถึงมาโครซึ่งไม่ใช่สิ่งที่ฉันหมายถึง แต่เพียงความคิดในการเขียนโปรแกรมเพื่อเขียนโปรแกรมอื่นโดยทั่วไป
ในกรณีนี้ใน Haskell เราเขียนการคำนวณบริสุทธิ์ซึ่งคำนวณโปรแกรมที่มีประสิทธิภาพซึ่งจะทำสิ่งที่เราต้องการ ดังนั้นจุดรวมของแฟ้มแหล่งที่มา Haskell (อย่างน้อยหนึ่งที่อธิบายถึงโปรแกรมที่ไม่ห้องสมุด) คือการอธิบายคำนวณบริสุทธิ์สำหรับ effectful 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()));
}
จากนั้นคุณสามารถอธิบายงาน cron เพื่อให้ URL และค้นหาพนักงานบางคนและแจ้งหัวหน้างานของพวกเขาในวิธีการทำงานอย่างหมดจดเป็น
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);