javascript - 這是一個純函數嗎?




function functional-programming (7)

大多數 sources 將純函數定義為具有以下兩個屬性:

  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 在不經意間使用過。)

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

假設我們從數據庫獲取匯率,並且匯率每天都在變化。

現在,無論我 今天 調用此函數多少次,它都會為輸入 100 提供相同的輸出。 但是,明天可能會給我不同的輸出。 我不確定這是否違反第一個條件。

IOW,函數本身不包含任何使輸入突變的邏輯,但是它依賴於將來可能會改變的外部常量。 在這種情況下,絕對可以每天更改。 在其他情況下,可能會發生; 可能不會。

我們可以將此類函數稱為純函數嗎? 如果答案是否定的,那麼我們如何將其重構為一個?


我們可以將此類函數稱為純函數嗎? 如果答案是否定的,那麼我們如何將其重構為一個?

正如您適當指出的那樣, “明天可能會給我帶來不同的輸出” 。 在這種情況下,答案將是巨大的 “否” 。 如果您對 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 函數令人滿意的前提下,我們可以得出結論, fetchFromDatabasedollarToEuro 上的投影必須令人滿意。 或“ fetchFromDatabase 是純淨的”語句表示 dollarToEuro 是純淨的(因為 fetchFromDatabasex 的標量因數對 dollarToEuro 基礎

從原始帖子中,我可以了解到 fetchFromDatabase 是一個函數時間。 讓我們改善重構工作,以使理解變得透明,從而清楚地將 fetchFromDatabase 為純函數:

fetchFromDatabase =(timestamp)=> {/ *這裡是實現* /};

最終,我將重構功能如下:

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();

因此,可以通過簡單地證明 fetchFromDatabase 正確調用 fetchFromDatabase (或其派生的 exchangeRate )來進行單元測試。


一個純粹主義者的答案(“我”從字面上是我,因為我認為這個問題沒有一個 正式的 “正確”答案):

在像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

我實用主義者的回答:

sources

在計算機編程中,純函數是具有以下屬性的函數:

  1. 對於相同的參數,其返回值是相同的(局部靜態變量,非局部變量,可變參考變量或來自I / O設備的輸入流無變化)。
  2. 它的評估沒有副作用(本地靜態變量,非本地變量,可變引用參數或I / O流不會發生突變)。

換句話說,只關係到函數的行為方式,而不是函數的實現方式。 只要一個特定的函數擁有這2個屬性-不管它如何實現,都是純函數。

現在執行您的功能:

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

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

這是不純淨的,因為它不符合要求2:可傳遞地依賴IO。

我同意上述說法有誤,請參閱其他答案以了解詳細信息: https://.com/a/58749249/251311 : https://.com/a/58749249/251311

其他相關資源:


就像其他答案所說的那樣,您實現了 dollarToEuro

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

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

確實是純淨的,因為程序運行時不會更新匯率。 但是,從概念上講, dollarToEuro 似乎應該是一個不純函數,因為它使用的是最新匯率。 解釋此差異的最簡單方法是,您尚未實現 dollarToEuro 而是實現了 dollarToEuroAtInstantOfProgramStart

這裡的關鍵是要計算貨幣換算需要幾個參數,並且由 dollarToEuro 純正版本將提供所有這些參數。 最直接的參數是要轉換的美元數量以及匯率。 但是,由於要從已發布的信息中獲取匯率,因此現在需要提供三個參數:

  • 兌換金額
  • 諮詢匯率的歷史權威
  • 交易發生的日期(以索引歷史權限)

這裡的歷史權限是您的數據庫,並且假設該數據庫未受到損害,則在特定日期始終會返回相同的匯率結果。 因此,結合使用這三個參數,您可以編寫一般 dollarToEuro 的完全純淨,自給自足的版本,看起來可能像這樣:

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

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

您的實現會在創建函數時立即捕獲歷史權限和交易日期的常量值-歷史權限是您的數據庫,捕獲的日期是您啟動程序的日期-剩下的就是美元金額,由調用者提供。 總是獲取最新值的不純版本的 dollarToEuro 本質上隱式採用了date參數,將其設置為函數調用的瞬間,這不是純粹的,因為您永遠不能使用相同的參數兩次調用函數。

如果您想擁有一個仍然可以獲取最新值的 dollarToEuro 的純版本,則仍然可以綁定歷史授權,但是不綁定date參數,並要求調用方提供日期作為參數,以結束像這樣:

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

dollarToEuro(100, Date.now());

從技術上講,您在計算機上執行的任何程序都是不純正的,因為它最終會編譯為“不純”的指令,例如“將該值移入 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");

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 可能會不同意我的看法。 他在 answer 說,以下示例中的 dollarToEuro 函數是不純的,因為“它 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 函數是純函數。


正如其他人所說,讀取可變變量通常被認為是不純的。 如果您已將其聲明為 const 那麼(假設它只是一個 number 並且沒有可變的內部結構)將是純淨的。

為了將這樣的代碼重構為純粹的函數式編程語言(例如Haskell),我們進行了 元編程 ,也就是說,我們編寫了處理不可變程序的代碼。 這些程序實際上並不執行任何操作,而是簡單地存在。

因此,對於一個非常輕量級的類示例,它描述了不可變程序以及您可以使用它們進行的一些操作,

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() 啟動它 之前,實際上什麼都沒有發生 。

同樣,我們可以有

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 現在也是從數字到生成數字的程序的純函數,並且您可以以確定性的方程方式進行推理,從而可以推理出沒有副作用的任何程序。


此函數不是純函數,它依賴於外部變量,幾乎肯定會更改該變量。

因此,該函數使您所做的第一點失敗,對於相同的參數,它不會返回相同的值。

要使此函數“純淨”,請傳入 exchangeRate 作為參數。

然後,這將滿足兩個條件。

  1. 當傳遞相同的值和匯率時,它將始終返回相同的值。
  2. 它也沒有副作用。

示例代碼:

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

dollarToEuro(100, fetchFromDatabase())

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 ,以避免重新分配。





functional-programming