首頁  >  文章  >  web前端  >  函數式程式設計的介紹與歸納總結(附程式碼)

函數式程式設計的介紹與歸納總結(附程式碼)

yulia
yulia原創
2018-09-11 17:01:311750瀏覽

最近在看函數式程式設計入門經典,自己總結歸納了一下,有興趣或是有需要的可以看一下。

1.、什麼是函數式程式設計

函數式程式設計主要是基於數學函數和它的思想,那麼我們先複習一下數學中的函數即
y = f(x)
即函數f(x) 以x 為參數,以y 為結果,x 和y 可以是任意的數字,其中包含了幾個關鍵點:
1、函數必須總是接收一個參數
2、函數必須總是傳回一個值
3、函數應該根據接收到的參數(例如x),而不是外部環境運行
4、對於給定的x,只會輸出唯一的y
讓我們從一個例子中來理解

// 一个计税函数
var percentValue = 5;
var calculateTax = (value) => {return value/100 * (100 + percentValue)};

用數學中的函數來分析一下這個函數,首先看第三條,函數應該根據接收到的參數,而不是外部環境運作。這裡的函數 calculateTax 依賴外部的 percentValue,因此,函數在數學的意義上就不能稱為函數,那麼我們可以怎麼將它改造成數學意義上的函數呢?很簡單

var calculateTax = (value,percentValue) => {return value/100 * (100 percentValue)};

#現在這個函數就可以被稱為一個真正的函數了。
現在讓我們用簡單的技術術語定義函數式程式設計:函數式程式設計是一種範式,只依賴輸入輸出就可以完成自身邏輯的函數。這保證了當函數多次呼叫時依然可以傳回相同的結果。函數不會改變任何外部環境的變數。

2 、引用透明性

根據函數的定義,我們可以得出結論:所有函數對於相同的輸入輸出都會傳回相同的值。函數的這個屬性被稱為引用透明性
來舉個例子

var identity = (i) => { return i };

我們定義了一個簡單的函數,在函數的內部不依賴全域變量,它會簡單的回傳輸入。現在假設它被應用於其他函數呼叫之間,如

sum(4,5) + identity(1);

根據引用透明性的定義,我們可以把它轉換為

sum(4,5) + 1;

該過程被稱為替換模型,因此你可以直接替換函數的結果(主要是因為函數的邏輯不依賴與其他全域變數)。
由於函數對於給定的輸入結果返回相同的值,實際上我們就可以快取它了,例如我們有一個函數 “factorial”來計算階乘。它接收一個參數以計算其階乘,例如 5 的階乘是 120。當使用者第二次輸入5 的階乘時,由於引用透明性(對於相同輸入返回相同結果)所以我們知道結果是120,但是機器並不知道,我們需要讓機器這個結果緩存下來以便以後調用直接返回結果,而不必再計算一遍。由此可看出引用透明性和可緩存程式碼在函數式程式設計中的重要性。

3、函數式、宣告式與抽象

函數式程式設計主張聲明式程式設計與寫抽象的程式碼
什麼是聲明式程式設計

#假設我們要列印出一個陣列中的所有元素,我們可以採用以下方法

var array = [1,2,3];
for(let i = 0; i < array.length; i++){
    console.log(array[i])
}

在這段程式碼中,我們精確的告訴了程式碼該如何做。如:取得數組長度,循環數組,用索引取得每個元素。這就是命令式程式設計。命令式程式設計主張告訴編譯器怎麼做。

來看另一種方式

var arr = [1,2,3];
arr.forEach((ele) => { console.log(ele) })

上面這段程式碼我們移除了取得數組長度,循環數組,用索引取得數組元素等。我們只關心要做什麼就行(即打印數組元素),獲取數組長度,循環等都由機器幫我們做了,我們只需要關心做什麼,而不是怎麼做,這就是聲明式編程

函數式程式設計主張以抽象的方式創建函數,這些函數能夠在程式碼的其他地方被重複使用。

4、 純函數

什麼是純函數?純函數式對給定的輸入傳回相同輸出的函數,例如

var double = (value) => value * 2;

上面的 double 就是一個純函數,因為對於相同的輸入總是會傳回相同的輸出。純函數遵循引用透明性,因此我們可以直接用 10 取代 double(5)。所以純函數的最了不起的地方是什麼?讓我們來看看


1.4.1 純函數產生可測試的程式碼

不純的函數具有副作用,以下以先前的課稅函數以例進行說明

var percentValue = 5;
var calculateTax = (value) => {return value/100 * (100 + percentValue)};

這個函數不是純函數,主要因為它以來外部環境計算其邏輯,當外部環境改變時,它會影響結果。因此,純函數的主要特徵是不依賴任何外部變量,也不應該改變任何外部變量。如果改變了外部變數可能會引起其他函數的行為的改變,即產生副作用,這會使系統的行為變得難以預測。


1.4.2 合理的程式碼

我們應該透過函數的名字來推理出函數的作用,例如 double 函數###
var double = (value) => value * 2;

我们可以通过函数名轻易的推出这个函数会把给定的数值加倍,因此根据引用透明性,我们可以直接把 double(5) 替换成 10。还有一个例子,Math.max(3,4,5,6) 结果是什么?虽然我们只看到了函数的名字,但是我们很容易看出结果,我们看到实现了吗?并没有,为什么,就是因为 Math.max 是纯函数啊!!!

5、 并发代码

纯函数允许我们并发的执行代码,因为纯函数不会改变它的环境,这意味着我们根本不需要担心同步问题。当然,js 是单线程的,但是如果项目中使用了 webworker 来并发执行任务,该怎么办?或者有一段 Node 环境中的服务端代码需要并发的执行函数,又该怎么办呢?

// 非纯函数代码
let global = &#39;something&#39;
let function1 = (input) => {
    // 处理 input
    // 改变 global
    global = "somethingElse"
}
let function2 = () => {
    if(global === "something"){
        // 业务逻辑
    }
}

如果我们需要并发的执行 function1 和 function2,假设 function1 在 function2 之前执行,就会改变 function2 的执行结果,所以并发执行这些代码就会造成不良的影响,现在把这些函数改为纯函数。

let function1 = (input,global) => {
    // 处理 input
    // 改变 global
    global = "somethingElse"
}
let function2 = (global) => {
   if(global === "something"){
        // 业务逻辑
    }
}

此处我们把 global 作为两个函数的参数,让它们变成纯函数,这样并发执行的时候就不会有问题了。

6、可缓存

既然纯函数对于给定的输入总能返回相同的输出,那么我们就能缓存函数的输出,例如

var doubleCache = (value) => {
    const cache = {};
    return function(value){
        if(!cache[value]){
            cache[value] = value * 2
            console.log(&#39;first time&#39;)
        }
        return cache[value];
    }
}
var double = doubleCache();
double(2) // first time,4
double(2) // 4
// 或者直接使用立即执行函数
var double = ((value) => {
    const cache = {};
    return function(value){
        if(!cache[value]){
            cache[value] = value * 2
            console.log(&#39;first time&#39;)
        }
        return cache[value];
    }
})()
double(2) // first time,4
double(2) // 4

这个函数中,假设我们第一次输入 5,cache 中并没有,于是执行代码,由于闭包的存在,cache[5] = 10,第二次我们调用的时候,cache[5] 存在,所以直接 return 10,看到了吗?这就是纯函数的魅力!!!别忘记这是因为纯函数的引用透明性。

7、 管道与组合

纯函数应该被设计为一次只做一件事,并且根据函数名就知道它所做的事情。
比如 linux 系统下有很多日常任务的命令,如 cat 用于打印文件内容,grep 用于搜索文件,wc 用于计算行数,这些命令一次只解决一个问题,但是我们可以用管道或组合来完成复杂的任务。假设我们需要在一个文件中找到一个特定的名称并统计它的出现次数,在命令行要输入如下指令
cat jsBook | grep -i “composing” | wc
上面的命令通过组合多个函数解决了我们的问题。组合不是 linux 命令独有的,它们是函数式编程范式的核心。
我们把它们称为函数式组合。来看一个 compose 函数的例子

var add1 = (value) =>{ return value+1 };
var double = (value) => {return value*2 };
var compose = (a,b) => {
    return (c) => {
       return a(b(c));
    }
}
var doubleAndAdd1 = compose(add1,double);
doubleAndAdd1(5) // 打印 5 * 2 + 1 = 11

compose 函数返回一个函数,将 b 的结果作为 a 的参数,这里就是将 double 的结果作为 add1 的参数,来实现了函数的组合。

8、 纯函数是数学函数

还记得我们之前的缓存函数吗,假设我们多次调用 double 对象,那么 cache 中就会变成这样

{
    1: 2,
    2: 4,
    3: 6,
    4: 8,
    5: 10
}

假设我们设置 double 的输入范围限制为 1 - 5,而且我们已经为这个范围建立的 cache 对象,因此只要参照 cache 就能根据指定输入返回指定输出。也就是一一对应的关系。
那么数学函数的定义是什么呢?
在数学中,函数是一种输入集合和可允许的输出集合之间的关系,具有如下属性:每个输入都精确地关联一个输出。函数的输入称为参数,输出称为值。对于一个给定的函数,所有被允许的输入集合称为该函数的定义域,而被允许的输出集合称为值域。

上面的定义和纯函数完全一致,例如在 double 中,你能找到定义域和值域吗?当然可以!通过这个例子,可以很容易看到数学函数的思想被借鉴到函数式范式的世界

9、 我们要做什么?

我们将通过学习,构建出一个 ES6-Functional 的函数式库,通过构建的过程,我们将理解如何使用 JavaScript 函数,以及如何在日常工作中应用函数式编程。

10、小结

这一节我们只是简单的介绍了函数式编程的概念,以及什么是纯函数,其中最重要的就是引用透明性。然后研究了几个短小的例子,通过例子来加深对函数式编程的理解。接下来我们将一步一步深入了解函数式编程。

以上是函數式程式設計的介紹與歸納總結(附程式碼)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn