成为一位函数式码农系列之五

本文为本人介入的前端早读课民众号《成为函数式码农》系列翻译的第五篇,第六篇仍在翻译中,以下为别的五篇的地点。

成为一位函数式码农系列之一

成为一位函数式码农系列之二

成为一位函数式码农系列之三

成为一位函数式码农系列之四

成为一位函数式码农系列之六

原文地点 译者:墨白 校正:野草

刚最先进修函数式编程时,明白函数式编程的中心观点是最重要的,有些时刻也是最难的一步。但实在没必要肯定云云。这个没有准确的答案。

援用通明(referential transparency)

援用通明是一个富有想象力的优秀术语,它是用来形貌纯函数可以被它的表达式平安的替代。经由历程一个例子来协助明白这个术语。

在代数中,你有一个下面的公式:
y = x + 10

然后被示知:
x = 3

你可以把x的值带入到之前的方程中,获得:
y = 3 + 10

注重这个方程依然是有效的。我们可以应用纯函数做一些雷同范例的替代。

下面是一个Elm的要领,在传入的字符串双方加上单引号:

quote str =
  "'" ++ str ++ "'"

下面是运用它的代码:

findError key =
  "Unable to find " ++ (quote key)

代码中,当查询key值失利时,findError构建了一个报错信息。

因为quote要领是纯函数,我们可以简朴地将quote函数体(仅仅只是个表达式)替代掉在findError中的要领挪用:

findError key =
  "Unable to find " ++ ("'" ++ str ++ "'")

这就是我所说的“反向重构”(它对我而言有更多的意义),一个可以被递次员或许递次(比方编译器和测试递次)用来推理代码的历程。

这在推导递归函数时特别有效。

实行递次

大部分递次是单线程的,即有且只需一段代码在当前实行。纵然你有多线程的递次,大部分递次依然壅塞守候I/O去完成,比方,file,network等等。

这也是当我们编写代码的时刻,我们很天然斟酌按序次来编写代码:

1. 拿到面包
2. 把2片面包放入烤面包机
3. 挑选加热时候
4. 按下最先按钮
5. 守候面包片弹出
6. 掏出烤面包
7. 拿黄油
8. 拿黄油刀
9. 制造黄油面包

在这个例子中,有两个自力的操纵:拿黄油以及加热面包。它们在step9时最先变得互相依靠。

我们可以在step1到6的时刻做step7和8因为它们之间互相自力。

但当我们最先做的时刻,事变最先庞杂了:

线程1:
--------
1. 拿到面包
2. 把2片面包放入烤面包机
3. 挑选加热时候
4. 向下推杆
5. 守候面包片弹出
6. 掏出烤面包

线程2:
1. 拿黄油
2. 拿黄油刀
3. 守候线程1完成
4. 制造黄油面包

如果线程1失利,线程2怎样办?怎样谐和这两个线程?烤面包这一步骤在哪一个线程运转:线程1,线程2或许二者?

我们完全可以不去思索这些庞杂的,只让我们的递次单线程运转,这更简朴。

然则,只需可以提拔我们递次的效力,那就是值得的,我们要支付勤奋来写很多线程递次。

但是,关于多线程,存在两个重要的题目。起首,多线程递次异常难写、读、明白、测试以及debug。

第二,一些言语,比方JavaScript,并不支撑多线程,就算有些言语支撑多线程,对它的支撑也很弱。

然则,如果运转递次并不重要而且一切都是并行实行的呢?

只管这听起来有些猖獗,但实在并不像听起来那末杂沓。让我们来看一下Elm的代码来抽象的明白它:

buildMessage message value =
    let
        upperMessage =
            String.toUpper message
        quotedValue =
            "'" ++ value "'"
    in
        upperMessage ++ ": " ++ value

这里的buildMessage接收messagevalue,然后,天生大写的message,冒号和在单引号中value

注重到upperMessagequotedValue是自力的。我们怎样晓得的呢?

关于自力,有两点必需必需满足。起首,它们必需是纯函数。这很重要,因为它们必需不会被别的要领的运转影响到。

如果它们不是纯函数,那末我们永久不能够晓得它们是不是自力。那种状况下,我们不能不依靠于它们在递次中挪用的递次来肯定它们的实行递次。这是一切敕令式言语的事情道理。

第二点必需满足的就是一个函数的输出值不能作为别的函数的输入值。如果存在这类状况,那末我们不能不守候个中一个完成才实行下一个。

在上面的代码示例中,upperMessagequotedValue二者都是纯的而且没有一个须要依靠别的的输出。

因而,这两个要领可以在任何递次下实行。

编译器可以自行决议实行的递次,而不须要递次员的工资介入。这只需在纯函数式编程言语中才实用,因为在平常编程言语中是很难去(不是不能够)预估差别递次带来的副作用。

纯函数式言语内里,实行的递次是可以由编译器决议的

鉴于没法频频加速CPU的运转速率,这一做法是异常有益的。生产商也不停增添CPU内核芯片的数目,这就意味着可以在硬件这一层面完成代码的并行处置惩罚。

但遗憾的是,我们没法经由历程敕令式的言语充分应用这些芯片,而只是发挥了它们很小一部分的功用。如果要充分应用就要彻底转变递次的体系结构。

运用纯函数言语,我们就有愿望在不转变任何代码的状况下充分地发挥CPU芯片的功用并获得优越效果。

范例解释

在静态范例言语中,范例是内联定义的。 这里经由历程一些Java代码来申明:
<pre>
public static String quote(String str) {

return "'" + str + "'";

}
</pre>
注重范例是怎样同函数定义内联在一起的。当你有泛型时,它变的更糟:
<pre>
private final Map<Integer, String> getPerson(Map<String, String> people, Integer personId) {
// …
}
</pre>

我已给定义范例的字段加粗了以使其越发显眼,但它们看往来来往依然和函数定义胶葛在一起。你不能不很小心肠去找到这些变量的名字。

如果是用奇异的动态范例言语,这不是一个题目。在JavaScript中,我们如许写代码:

var getPerson = function(people, personId) {
    // ...
};

如许的代码没有任何的烦琐的范例信息更容易浏览。唯一的题目就是我们摒弃了范例检测的平安特征。我们可以很简朴的传入这些参数,比方,一个Number范例的people以及一个Object范例的personId

除非递次运转,不然我们发明不了如许的题目,而如许的题目也能够在代码上线以后几个月才涌现。而如许的题目在Java中不会涌现,因为它没法经由历程编译。

然则,如果我们能同时具有这二者的优秀点呢?JavaScript的语法简朴性以及Java的平安性。

实在我们是可以的。下面是一个带范例解释的用Elm写的要领:

add : Int -> Int -> Int
add x y =
    x + y

请注重范例信息是在零丁的代码行上面的。而恰是如许的支解使得其有所差别。

如今你能够以为范例解释有错字。 我晓得我第一次见到它的时刻。 我以为第一个 - >应该是一个逗号。 但并没有错别字。

当你看到它加上隐含的括号,代码就清楚多了:

add : Int -> (Int -> Int)

这示意,add是一个要领,它接收单个Int范例的参数,返回一个要领,这个要领接收一个Int范例的参数,而且返回一个Int范例的值。

这里另有一个带括号范例解释的代码:

doSomething : String -> (Int -> (String -> String))
doSomething prefix value suffix =
    prefix ++ (toString value) ++ suffix

上面的代码示意doSomething是一个要领,它接收单个范例为String的参数而且返回一个函数,返回的函数接收单个范例为Int的参数,而且再次返回一个函数,此次返回的函数接收一个范例为String的参数,而且返回一个String

注重为何每一个要领都只接收一个参数呢?这是因为每一个要领在Elm内里都是柯里化。

因为括号老是隐含在右侧,它们并非必需。所以我们可以简写成:

doSomething : String -> Int -> String -> String

当我们传递函数作为参数时,括号是必要的。 没有它们,范例解释将是不明确的。 比方:

takes2Params : Int -> Int -> String
takes2Params num1 num2 =
    -- do something

与下面的并差别:

takes1Param : (Int -> Int) -> String
takes1Param f =
    -- do something

takes2Param是一个接收两个参数的函数,两个参数都是Int范例。但是,takes1Param须要接收一个参数,这个参数为函数,而函数须要接收两个Int范例的参数。

下面是map的范例解释:

map : (a -> b) -> List a -> List b
map f list =
    // ...

上面须要括号是因为f(a -> b)范例,即接收范例为a的单个参数并返回范例为b的某个函数.

这里范例a是代指任何范例。 当范例是大写时,它是一个显式范例,比方,String。 当范例为小写时,它可所以任何范例。 这里的a可所以String,但也可所以Int。

如果你看到(a -> a),那末,就是指input范例以及output范例是雷同的。它们究竟是什么范例并不重要,重要的是它们必需婚配。

但在map这一示例中,有如许一段(a -> b)。这意味着它既能返回一个差别的范例,也能返回一个雷同的范例。

然则一旦a的范例肯定了,(TODO the whole signature)a在整段代码中就必需为这个范例。比方,如果a是一个Intb是一个String,那末这段代码就相当于:

(Int -> String) -> List Int -> List String

上面就是一切的a都被替代成Int,一切的b都被替代成String

List Int范例意味着一个值都为Int范例的列表,List String意味着一个值都为String范例的列表。如果你已在Java或许其他的言语中运用过泛型,那末这个观点你应该是熟习的。

在这个系列文章的末了,我将会讨论怎样运用你在一样平常生涯中学到的东西,比方,函数式编程以及Elem。

    原文作者:望舒
    原文地址: https://segmentfault.com/a/1190000008844212
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞