听君一席话,如听一席话,解释解释“惰性求值”~

2022-09-19 11:11:48 浏览数 (1)


theme: smartblue

止观初探

我们习惯将代码编写为 一系列的命令,程序会按照它们的 顺序 进行执行:

思考以下代码:

代码语言:javascript复制
const myFunction = function(a, b, c) {
  let result1 = longCalculation1(a,b);
  let result2 = longCalculation2(b,c);
  let result3 = longCalculation3(a,c);
  if (result1 < 10) {
    return result1;
  } else if (result2 < 100) {
    return result2;
  } else {
    return result3;
  }
}

没错,再正常不过的 myFucntion,依次声明了 result1result2resulit3 3 个变量,分别赋值为 longCalculation1(a,b)longCalculation2(b,c)longCalculation3(a,c)longCalculation1/2/3 顾名思义,是一些包含很长的计算过程的函数;

然后进入 if...else if...else... 判断;

最后 return 输出;

那这段代码 合理吗?

只要调用 myFunctionlongCalculation1/2/3 都必将执行!但是实际上,我们可能不需要它们所有的运算结果;无差别 的完成 3 个很长过程的计算会很影响效率;

有了这个认识之后,我们再来改进代码:

代码语言:javascript复制
const myFunction = function(a, b, c) {
  let result1 = longCalculation1(a,b);
  if (result1 < 10) {
    return result1;
  } else {
    let result2 = longCalculation2(b,c);
    if (result2 < 100) {
     return result2;
    } else {
      let result3 = longCalculation3(a,c);
      return result3;
    }
  }
}

没错,这确实是改进过后的代码 ╮(╯▽╰)╭

虽然在结构上看,更难看了(多层嵌套,确实难受),但是:

它让 longCalculation1/2/3 不用每次都全部执行,只有在进入确定的条件,需要对值进行返回的时候,才需要计算;

这,就,是, —— 惰性求值的思想体现(不需要立即返回的值,就先别计算;)

庐山面目

来看下 wiki 释义:

惰性求值又叫惰性计算、懒惰求值,也称为传需求调用,是一个计算机编程中的一个概念,目的是要 最小化 计算机要做的工作。

在使用惰性求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值

这句话很重要!怎么理解?

比如:let result1 = longCalculation1(a,b); 这个表达式,意思是把 longCalculation1(a,b) 计算的返回值赋给 result1

在惰性求值中,赋值时,先不对 longCalculation1(a,b) 进行计算,而是等 result1 被取用的时候(在示例中,就是 return的时候)再进行计算。

那它是怎样实现的呢?

引用 Reincarnation 的回答:

通过将表达式包装成一个thunk实现的; 例如计算f (g x),实际上给f传递的参数就是一个类似于包装成(_ -&gt; (g x))的一个thunk; 然后在真正需要计算g x的时候才会调用这个thunk; 事实上这个thunk里面还包含一个boolean表示该thunk是否已经被计算过(若已经被计算过,则还包含一个返回值),用来防止重复计算;

第一节示例的 JavaScript 的代码虽然是有惰性求值的思想体现,但是其本身并不是惰性求值;

惰性求值是编程语言的特性设计,很多纯粹的函数式编程语言都支持这种设计;

比如在 Haskell 中实现上述示例:

代码语言:javascript复制
myFunction :: Int -> Int -> Int -> Int
myFunction a b c =
  let result1 = longCalculation1 a b
      result2 = longCalculation2 b c
      result3 = longCalculation3 a c
  in if result1 < 10
       then result1
       else if result2 < 100
         then result2
         else result3

看上去,这和 JavaScript 示例代码 1 一样,但是它实际上实现的却是 JavaScript 示例代码 2 的效果;

在 GHC 编译器中,result1result2, 和 result3 被存储为 “thunk” ,并且编译器知道在什么情况下,才需要去计算结果,否则将不会提前去计算!

有点像 Promise 的意思,你不告诉我 resolve/reject,我就 pending;Haskell 中,你不告诉我什么时候调用这个值,我就维持 thunk 的状态;

无限列表

在 Haskell 中可以定义一个数组,它的项是无限多的;

代码语言:javascript复制
let infList = [1..] // 定义一个 1,2,3... 不断递增的数组;

为什么在 Haskell 中行,在 JavaScript 中不行?

因为它是懒惰的,你定义归你定义,反正定义的时候,我又不用分配无穷大的内存,等你开始调用的时候,我再开始计算分配吧!

延迟计算很棒,不过事物都有两面性,这样做坏处是什么?

举例

0 人点赞