Jestem dwujęzycznym językiem Haskell / JS, a Haskell jest jednym z języków, który ma duży wpływ na czystość funkcji, więc pomyślałem, że dam wam perspektywę z tego, jak Haskell to widzi.
Jak mówili inni, w Haskell, czytając zmienny zmienną jest powszechnie uważany za nieczyste. Istnieje różnica między zmiennymi a definicjami, ponieważ zmienne mogą się później zmieniać, definicje są takie same na zawsze. Jeśli więc to zadeklarowałeś const
(zakładając, że jest to tylko a number
i nie ma zmiennej struktury wewnętrznej), czytanie z tego byłoby oparte na definicji, która jest czysta. Ale chciałeś modelować kursy wymiany zmieniające się w czasie, a to wymaga pewnej zmienności, a potem wpadniesz w nieczystość.
Aby opisać tego rodzaju nieczyste rzeczy (możemy je nazwać „efektami”, a ich użycie jako „efektywne” w przeciwieństwie do „czystego”) w Haskell, robimy to, co można nazwać metaprogramowaniem . Dzisiaj metaprogramowanie zwykle odnosi się do makr, co nie mam na myśli, ale po prostu pomysł napisania programu do napisania innego programu w ogóle.
W tym przypadku w Haskell piszemy czyste obliczenia, które obliczają skuteczny program, który zrobi to, co chcemy. Tak więc cały punkt pliku źródłowego Haskell (przynajmniej taki, który opisuje program, a nie bibliotekę) polega na opisaniu czystego obliczenia efektywnego programu, który wywołuje void main
. Następnie zadaniem kompilatora Haskell jest pobranie tego pliku źródłowego, wykonanie czystego obliczenia i umieszczenie tego efektywnego programu jako binarnego pliku wykonywalnego gdzieś na dysku twardym, aby można go było później uruchomić w wolnym czasie. Innymi słowy, istnieje przerwa między czasem, w którym wykonuje się czyste obliczenia (podczas gdy kompilator czyni plik wykonywalny), a czasem, w którym działa skuteczny program (za każdym razem, gdy uruchamiasz plik wykonywalny).
Tak więc dla nas skuteczne programy są tak naprawdę strukturą danych i same w sobie nie robią nic, po prostu wspomniane (nie mają * efektów ubocznych * oprócz ich wartości zwracanej; ich wartość zwrotna zawiera ich efekty). Dla bardzo lekkiego przykładu klasy TypeScript, która opisuje niezmienne programy i niektóre rzeczy, które możesz z nimi zrobić,
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()));
}
}
Kluczem jest to, że jeśli nie masz Program<x>
żadnych efektów ubocznych, to są to istoty całkowicie funkcjonalnie czyste. Mapowanie funkcji nad programem nie wywołuje żadnych skutków ubocznych, chyba że funkcja nie była funkcją czystą; sekwencjonowanie dwóch programów nie wywołuje żadnych skutków ubocznych; itp.
Na przykład, jak zastosować to w twoim przypadku, możesz napisać kilka czystych funkcji, które zwracają programy, aby uzyskać użytkowników według ID i zmienić bazę danych i pobrać dane JSON, takie jak
// 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()));
}
a następnie możesz opisać zadanie crona, aby zwinąć adres URL i wyszukać jakiegoś pracownika i powiadomić jego przełożonego w czysto funkcjonalny sposób, jak
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!'
))
);
Chodzi o to, że każda funkcja tutaj jest funkcją całkowicie czystą; tak naprawdę nic się nie wydarzyło, dopóki nie action.run()
uruchomiłem go. Ponadto mogę pisać funkcje, takie jak,
// 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()]));
}
a gdyby JS miał anulować obietnicę, moglibyśmy ścigać się w dwóch programach i wziąć pierwszy wynik i anulować drugi. (Mam na myśli, że nadal możemy, ale staje się mniej jasne, co robić).
Podobnie w twoim przypadku możemy opisać zmiany kursów walut
declare const exchangeRate: Program<number>;
function dollarsToEuros(dollars: number): Program<number> {
return exchangeRate.map(rate => dollars * rate);
}
i exchangeRate
może być programem, który patrzy na zmienną wartość,
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);
});
ale mimo to funkcja ta dollarsToEuros
jest teraz funkcją czystą od liczby do programu, który generuje liczbę, i można o tym wnioskować w ten deterministyczny sposób równań, który można argumentować o dowolnym programie, który nie ma skutków ubocznych.
Koszt oczywiście polega na tym, że w końcu trzeba to .run()
gdzieś zadzwonić , a to będzie nieczyste. Ale całą strukturę twojego obliczenia można opisać czystym obliczeniem i możesz przesunąć nieczystości na margines swojego kodu.
function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);