本节书摘来自异步社区《Haskell并行与并发编程》一书中的第2章,第2.1节惰性求值和弱首范式,作者【英】Simon Marlow,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.1 惰性求值和弱首范式
Haskell并行与并发编程
Haskell是一门惰性语言,即表达式是在其值需要使用时才被求值2。一般来说,不必担心该过程如何发生,只要表达式在需要时求值,不需要时不被求值即可。但是,当在代码中使用了并行编程后,就需要告诉编译器一些程序运行的信息,即某些代码应该并行执行。由于对惰性求值的工作方式有一个直觉的认识将有助于有效地进行并行编程,因此本节将以GHCi作为试验工具,探讨惰性求值的一些基本概念。
下面从非常简单内容的开始:
Prelude> let x = 1 + 2 :: Int
这会将变量x绑定(bind)到表达式1 + 2(为了避免任何由重载带来的复杂性,特指定为Int类型)。此时,仅考虑Haskell语言本身,1 + 2是和3相等的,因此这里也可以写成let x = 3 :: Int,而且通过普通的Haskell代码无法将两种写法区分开。但出于并行编程的目的,这里确实在意1 + 2和3的区别,因为1 + 2是一个还未开始的计算,其值也许可以通过其他方法并行地计算出来。当然,在实际中不会对像1 + 2这样简单情况使用并行计算,然而,其作为未求值计算的本质是仍然是重要的。
此刻,称x为未求值的(unevaluated)。一般来说,在Haskell中是无法得知x是未求值的,但幸运的是,GHCi的调试器提供了一些命令,这些命令可以查看Haskell表达式的结构,但又不影响表达式本身。因此,可以通过使用这些命令来说明发生的情况。:sprint这条命令可以打印出表达式的值,但又不会引发表达式求值。
`Prelude> :sprint x
x = _`
特殊符号_表示“未求值的”,对于这种情况,读者也许听过另一个词“thunk”,即内存中表示1 + 2这个未求值计算的对象。此例中的thunk如图2-1所示。

图2-1 表示1 + 2的thunk


在图中,x是一个指向内存对象的指针,该内存对象表示函数+应用于整数1和2。
该thunk表示x将在其值需要时被求值。在GHCi中,触发求值最容易的方法是将其打印出来,也就是说,在提示符后输入x即可:
`Prelude> x
3`
现在若通过:sprint查看x的值,可以发现其已被求值:
`Prelude> :sprint x
x = 3`
从内存中的对象方面考虑,即表示1 + 2的thunk实际上被(装箱的)整数3覆盖了3。因此,以后任何对x值的查询都会立即返回结果,这就是惰性求值的工作原理。
前面的例子比较简单,再试一个稍为复杂的的示例:
`Prelude> let x = 1 + 2 :: Int
Prelude> let y = x + 1
Prelude> :sprint x
x = _
Prelude> :sprint y
y = _`
这里再次将变量x绑定到1 + 2,此外,还将变量y绑定到x + 1,然后,正如所预期的,:sprint命令显示两者均未被求值。在内存中,会有图2-2所示的结构。

图2-2 一个引用了另外的thunk的thunk


不幸的是,该结构无法被直接查看,读者只有相信这里所画的图是正确的。
现在,为了计算y的值,需要x的值,即y依赖于x。因此,对y的求值同时会导致对x的求值。这次使用不同方法来强制求值,即通过Haskell内建的seq函数。
`Prelude> seq y ()
()`
函数seq先对其第一个参数求值,这里是y,然后返回第二个参数,此例中,即()。再查看此时x和y的值:
`Prelude> :sprint x
x = 3
Prelude> :sprint y
y = 4`
正如所预期的,两者均已被求值。因此,到目前为止一般性的原则如下。
• 定义一个表达值会建立一个thunk来表示该表达式。
• 在需要其值前,thunk保持未求值状态。一旦被求值,则会被求出的值所替代。
再看一下增加一个数据结构会发生什么情况:
`Prelude> let x = 1 + 2 :: Int
Prelude> let z = (x,x)
变量z绑定到了序对(x,x),命令:sprint显示出一些有意思的内容:
Prelude> :sprint z
z = (_,_)`
这里隐含的结构如图2-3所示。
变量z本身引用了序对(x,x),但序对的两项均指向了未求值的,代表x的thunk。这说明数据结构可以使用未求值的表达式来构建。

图2-3 两项引用同一thunk的序对


下面再次将z变为thunk:
`Prelude> import Data.Tuple
Prelude Data.Tuple> let z = swap (x,x+1)`
函数swap的定义为:swap(a,b)=(b,a)。这次z和前面的一样,是未求值的:
`Prelude Data.Tuple> :sprint z
z = _`
这样的话,当使用seq来对z求值时,就能看清楚具体发生的情况:
`Prelude Data.Tuple> seq z ()
()
Prelude Data.Tuple> :sprint z
z = (_,_)`
函数seq执行后,导致作为参数的z被求值,成为一个序对,但序对的两项仍然处于未求值状态。函数seq仅对其第一个参数的第一层构造求值,不再对剩下的结构继续求值。对此有一个专门的称呼:称函数seq对第一个参数求值,使之成为弱首范式(weak head normal form)。该术语的使用是出于历史原因,因此对其不必深究。该术语常被缩写为WHNF4。名词范式在这里是“完全求值”5的意思,在2.4节会看到如何将表达式求值使之成为范式。
弱首范式的概念在下面两章会多次出现,因此值得去花些时间去理解这个概念,并对Haskell中求值是如何发生的有所体会。对此在GHCi中试验不同的表达式和:sprint命令不失为一种很好的方法。
为了完成本例,下面对x进行求值:
`Prelude Data.Tuple> seq x ()
()`
此时z会是何值?
`Prelude Data.Tuple> :sprint z
z = (_,3)`
记得变量z被定义为swap(x,x+1),即(x+1,x),前面刚对x求值了,所以z的第二个成员是已被求值的,值为3。
最后,来看一个关于列表和几个常见列表函数的例子。对于函数map的定义,读者或许已经知道,不过还是列在下面作为参考:
`map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = f x : map f xs`
函数map建立了一个惰性的数据结构。若重写map的定义而让thunk明确,可能会更清楚一些:
`map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = let

                  x'   = f xxs' = map f xsinx' : xs'`

这和前面的map的定义是一样的,但可以看到map返回的列表的头和尾各自都是thunk:f x和map f xs。也就是说,map建立了图2-4所示的结构。

图2-4 通过map建立的thunk


下面使用map定义一个简单的列表结构:
Prelude> let xs = map (+1) [1..10] :: [Int]
此时xs未被求值:
`Prelude> :sprint xs
xs = _`
接着对该列表求值,使之成为弱首范式:
`Prelude> seq xs ()
()
Prelude> :sprint xs
xs = _ : _`
目前为止,只知道xs是一个至少包含一个元素的列表。接着,对该列表应用length函数:
`Prelude> length xs
10`
函数length的定义如下:
`length :: [a] -> Int
length [] = 0
length (_:xs) = 1 + length xs`
注意到length会忽略列表的头,而只对列表的尾xs进行递归,因而对列表应用length时,列表的结构会被展开,但元素不会被求值。这点通过:sprint可以清楚地看到:
`Prelude> :sprint xs
xs = [_,_,_,_,_,_,_,_,_,_]`
GHCi注意到列表已被展开,所以改用方括号显示列表,而不再使用:。
即使通过求值的方式展开了整个列表,该列表仍不是范式(而仍然是弱首范式)。通过对列表应用一个函数,该函数需要使用列表的所有元素,就可以使之完全求值。例如sum函数:
`Prelude> sum xs
65
Prelude> :sprint xs
xs = [2,3,4,5,6,7,8,9,10,11]`
前面这些讨论,对于惰性求值这个精妙而复杂的主题仅触及了表面。幸运的是,多数情况下,编写Haskell代码无需了解或担心求值是何时进行的。的确,Haskell语言定义十分仔细,不对如何求值作明确指定;语言的实现可以自由地选择策略,只需保证结果正确。这也是程序员大多数时候所关注的。但是,当编写并行代码时,了解表达式何时被求值变得重要起来,因为只有这样才能对并行化计算进行安排。
第4章使用Par monad,对数据流进行明确的描述,是另一种使用惰性求值进行并行编程的方法。该方法牺牲了部分简洁性从而避免了一些惰性求值相关的微妙的问题。由于会出现一种方法比另一种解决问题更自然或更高效的情况,因此两种方法都值得学习。
__
1weak head normal form,函数式编程中的一种范式。——译者注
2技术上说,这并不正确。Haskell实际上是一门非严格(non-strict)的语言,惰性求值只是几种正确的实现策略之一。不过GHC使用的是惰性求值,因此这里忽略这个技术细节。
3严格地说,是被一个到该值的间接引用所覆盖,不过这些细节在这里并不重要。感兴趣的读者可以到GHC wiki上阅读关于该实现的文档,以及许多关于该设计的论文。
4即弱首范式英文weak head normal form的首字母缩写。——译者注
5即表达式里面所有的部分、各层次均被求值,不存在未求值的部分。——译者注

《Haskell并行与并发编程》——第2章,第2.1节惰性求值和弱首范式相关推荐

  1. Java7并发编程指南——第二章:线程同步基础

    Java7并发编程指南--第二章:线程同步基础 @(并发和IO流) Java7并发编程指南第二章线程同步基础 思维导图 项目代码 思维导图 项目代码 GitHub:Java7ConcurrencyCo ...

  2. Java7并发编程指南——第一章:线程管理

    Java7并发编程指南--第一章:线程管理 @(并发和IO流) Java7并发编程指南第一章线程管理 思维导图 项目代码 思维导图 项目代码 GitHub:Java7ConcurrencyCookbo ...

  3. 闭关之 C++ 函数式编程笔记(二):偏函数、组合、可变状态与惰性求值

    目录 第四章 以旧函数创建新函数 4.1 偏函数应用 4.1.1 把二元函数转成一元函数的通用方法 4.1.2 使用 std::bind 绑定值到特定的函数参数 4.1.3 二元函数参数的反转 (这节 ...

  4. 惰性求值 php,使用 JavaScript 进行函数式编程 (一) 翻译

    编程范式 编程范式是一个由思考问题以及实现问题愿景的工具组成的框架.很多现代语言都是聚范式(或者说多重范式): 他们支持很多不同的编程范式,比如面向对象,元程序设计,泛函,面向过程,等等. 函数式编程 ...

  5. 惰性求值 php,详细介绍C#函数式编程的示例代码

    public double MemoryUtilization() { //计算目前内存使用率 var pcInfo = new ComputerInfo(); var usedMem = pcInf ...

  6. python惰性求值的特点_C#教程之C#函数式编程中的惰性求值详解

    https://www.xin3721.com/eschool/python.html 惰性求值 在开始介绍今天要讲的知识之前,我们想要理解严格求值策略和非严格求值策略之间的区别,这样我们才能够深有体 ...

  7. 并发编程专题——第一章(深入理解java内存模型)

    说到并发编程,其实有时候觉得,开发中真遇到这些所谓的并发编程,场景多吗,这应该是很多互联网的在职人员,一直在考虑的事情,也一直很想问,但是又不敢问,想学习的同时,网上这些讲的又是乱七八糟,那么本章开始 ...

  8. Java并发编程 - 第三章 Java内存模型

    前言: Java 线程之间的通信对程序员完全透明,内存可见性问题很容易困扰 Java 程序员,本章将揭开 Java 内存模型神秘的面纱. 一.Java 内存模型的基础 1.1 并发编程模型的两个关键问 ...

  9. Java并发编程 - 第十一章 Java并发编程实践

    前言: 当你在进行并发编程时,看着程序的执行速度在自己的优化下运行得越来越快,你会觉得越来越有成就感,这就是并发编程的魅力.但与此同时,并发编程产生的问题和风险可能也会随之而来.本章先介绍几个并发编程 ...

最新文章

  1. 入门到放弃node系列之网络模块(二)
  2. Qt for Android 开发环境配置
  3. sip消息概念(一)
  4. 怎么把html表复制到word里,怎么把网页表格复制到word
  5. mysql订单详情的设计_订单功能模块设计与实现
  6. qn模块java脚本_Qn271 对于网络编程 反射 IO 线程的一些一本入门程序 多多联系会加快 速度 WinSock-NDIS 269万源代码下载- www.pudn.com...
  7. 使用嵌套循环,打印四行五列星星矩形(每次只能打印一个*)
  8. Go defer实现原理剖析
  9. CDH中impala 的查询返回部分结果。 已超出 199 流查询的时间序列流限制。
  10. Delphi XE7的安卓程序如何调用JAVA的JAR,使用JAVA的类?
  11. Mathematica 计算矩阵的伴随矩阵
  12. ubuntu 20 kvm 安装macos
  13. Vue学习笔记(利用网易云API实现音乐播放器 实例)
  14. 区块链搭建联盟链及控制台安装
  15. mongoDB 4.0 开启远程访问
  16. 数字化转型浪潮 金融科技公司如何扮演“引路人“角色?
  17. Spring Boot 核心注解?主要由哪几个注解组成?
  18. 关于Unity3d 2020所有国外版本下载(2020.3.0f1以前)
  19. 【Unity实用工具】TexturePacker使用教程
  20. 【正点原子FPGA连载】第十四章 串口通信实验 -摘自【正点原子】新起点之FPGA开发指南_V2.1

热门文章

  1. Java 8 中的这个接口真好用!
  2. RESTful API 设计规范精讲
  3. 面试:你知道Java中的回调机制吗?
  4. Spring AOP 增强框架 Nepxion Matrix 详解
  5. 智源计算所-互联网虚假新闻检测挑战赛(冠军)方案,开源分享
  6. 轮椅度过一生!微软CEO纳德拉26岁长子去世,半生为儿也难逃病魔
  7. 清华姚班校友陈丹琦斩获2022斯隆奖!「诺奖风向标」27位华人学者入选
  8. 华人科学家胡安明被判无罪!曾因「中国行动计划」被FBI紧盯两年,遭软禁18个月...
  9. Hinton团队CV新作:用语言建模做目标检测,性能媲美DETR
  10. AI生成的代码你敢用吗?