2017年9月

负载均衡器如何帮助你解决分布式系统的复杂性。

Ship with tug

原生云应用旨在利用分布式系统的性能、可扩展性和可靠性优势。不幸的是,分布式系统往往以额外的复杂性为代价。由于你程序的各个组件跨网络分布,并且这些网络有通信障碍或者性能降级,因此你的分布式程序组件需要能够继续独立运行。

为了避免程序状态的不一致,分布式系统设计应该有一个共识,即组件会失效。没有什么比在网络中更突出了。因此,在其核心,分布式系统在很大程度上依赖于负载平衡——请求分布于两个或多个系统,以便在面临网络中断时具有弹性,并在系统负载波动时水平缩放时。

随着分布式系统在原生云程序的设计和交付中越来越普及,负载平衡器在现代应用程序体系结构的各个层次都影响了基础设施设计。在大多数常见配置中,负载平衡器部署在应用程序前端,处理来自外部世界的请求。然而,微服务的出现意味着负载平衡器可以在幕后发挥关键作用:即管理服务之间的流。

因此,当你使用原生云程序和分布式系统时,负载均衡器将承担其他角色:

  • 作为提供缓存和增加安全性的反向代理,因为它成为外部客户端的中间人。
  • 作为通过提供协议转换(例如 REST 到 AMQP)的 API 网关
  • 它可以处理安全性(即运行 Web 应用程序防火墙)。
  • 它可能承担应用程序管理任务,如速率限制和 HTTP/2 支持。

鉴于它们的扩展能力远大于平衡流量, 负载平衡器 load balancer 可以更广泛地称为 应用交付控制器 Application Delivery Controller (ADC)。

开发人员定义基础设施

从历史上看,ADC 是由 IT 专业人员购买、部署和管理的,最常见运行企业级架构的应用程序。对于物理负载平衡器设备(如 F5、Citrix、Brocade等),这种情况在很大程度上仍然存在。具有分布式系统设计和临时基础设施的云原生应用要求负载平衡器与它们运行时的基础设施 (如容器) 一样具有动态特性。这些通常是软件负载均衡器(例如来自公共云提供商的 NGINX 和负载平衡器)。云原生应用通常是开发人员主导的计划,这意味着开发人员正在创建应用程序(例如微服务器)和基础设施(Kubernetes 和 NGINX)。开发人员越来越多地对负载平衡 (和其他) 基础设施的决策做出或产生重大影响。

作为决策者,云原生应用的开发人员通常不会意识到企业基础设施需求或现有部署的影响,同时要考虑到这些部署通常是新的,并且经常在公共或私有云环境中进行部署。云技术将基础设施抽象为可编程 API,开发人员正在定义应用程序在该基础设施的每一层的构建方式。在有负载平衡器的情况下,开发人员会选择要使用的类型、部署方式以及启用哪些功能。它们以编程的方式对负载平衡器的行为进行编码 —— 随着程序在部署的生存期内增长、收缩和功能上进化时,它如何动态响应应用程序的需要。开发人员将基础设施定义为代码 —— 包括基础设施配置及其运维。

开发者为什么定义基础设施?

编写如何构建和部署应用程序的代码实践已经发生了根本性的转变,它体现在很多方面。简而言之,这种根本性的转变是由两个因素推动的:将新的应用功能推向市场所需的时间(上市时间)以及应用用户从产品中获得价值所需的时间(获益时间)。因此,新的程序写出来就被持续地交付(作为服务),无需下载和安装。

上市时间和获益时间的压力并不是新的,但由于其他因素的加剧,这些因素正在加强开发者的决策权力:

  • 云:通过 API 将基础设施定义为代码的能力。
  • 伸缩:需要在大型环境中高效运维。
  • 速度:马上需要交付应用功能,为企业争取竞争力。
  • 微服务:抽象框架和工具选择,进一步赋予开发人员基础设施决策权力。

除了上述因素外,值得注意的是开源的影响。随着开源软件的普及和发展,开发人员手中掌握了许多应用程序基础设施 - 语言、运行时环境、框架、数据库、负载均衡器、托管服务等。微服务的兴起使应用程序基础设施的选择民主化,允许开发人员选择最佳的工具。在选择负载平衡器的情况下,那些与云原生应用的动态特质紧密集成并响应的那些人将超人一等。

总结

当你在仔细考虑你的云原生应用设计时,请与我一起讨论“在云中使用 NGINX 和 Kubernetes 进行负载平衡”。我们将检测不同公共云和容器平台的负载平衡功能,并通过一个宏应用的案例研究。我们将看看它是如何被变成较小的、独立的服务,以及 NGINX 和 Kubernetes 的能力是如何拯救它的。


作者简介:

Lee Calcote 是一位创新思想领袖,对开发者平台和云、容器、基础设施和应用的管理软件充满热情。先进的和新兴的技术一直是 Calcote 在 SolarWinds、Seagate、Cisco 和 Pelco 时的关注重点。他是技术会议和聚会的组织者、写作者、作家、演讲者,经常活跃在技术社区。


via: https://www.oreilly.com/learning/developer-defined-application-delivery

作者:Lee Calcote 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

1、概述

现在能够在 Windows 10 和 Windows 服务器上运行 Docker 容器了,它是以 Ubuntu 作为宿主基础的。

想象一下,使用你喜欢的 Linux 发行版——比如 Ubuntu——在 Windows 上运行你自己的 Linux 应用。

现在,借助 Docker 技术和 Windows 上的 Hyper-V 虚拟化的力量,这一切成为了可能。

2、前置需求

你需要一个 8GB 内存的 64 位 x86 PC,运行 Windows 10 或 Windows Server。

只有加入了 Windows 预览体验计划(Insider),才能运行带有 Hyper-V 支持的 Linux 容器。该计划可以让你测试预发布软件和即将发布的 Windows。

如果你特别在意稳定性和隐私(Windows 预览体验计划允许微软收集使用信息),你可以考虑等待 2017 年 10 月发布的Windows 10 Fall Creator update,这个版本可以让你无需 Windows 预览体验身份即可使用带有 Hyper-V 支持的 Docker 技术。

你也需要最新版本的 Docker,它可以从 http://dockerproject.org 下载得到。

最后,你还需要确认你安装了 XZ 工具,解压 Ubuntu 宿主容器镜像时需要它。

3、加入 Windows 预览体验计划(Insider)

如果你已经是 Windows 预览体验计划(Insider)成员,你可以跳过此步。否则在浏览器中打开如下链接:

https://insider.windows.com/zh-cn/getting-started/

要注册该计划,使用你在 Windows 10 中的微软个人账户登录,并在预览体验计划首页点击“注册”,接受条款并完成注册。

然后你需要打开 Windows 开始菜单中的“更新和安全”菜单,并在菜单左侧选择“Windows 预览体验计划”。

如果需要的话,在 Windows 提示“你的 Windows 预览体验计划账户需要关注”时,点击“修复”按钮。

4、 Windows 预览体验(Insider)的内容

从 Windows 预览体验计划面板,选择“开始使用”。如果你的微软账户没有关联到你的 Windows 10 系统,当提示时使用你要关联的账户进行登录。

然后你可以选择你希望从 Windows 预览体验计划中收到何种内容。要得到 Docker 技术所需要的 Hyper-V 隔离功能,你需要加入“快圈”,两次确认后,重启 Windows。重启后,你需要等待你的机器安装各种更新后才能进行下一步。

5、安装 Docker for Windows

Docker Store 下载 Docker for Windows。

下载完成后,安装,并在需要时重启。

重启后,Docker 就已经启动了。Docker 要求启用 Hyper-V 功能,因此它会提示你启用并重启。点击“OK”来为 Docker 启用它并重启系统。

6、下载 Ubuntu 容器镜像

Canonical 合作伙伴镜像网站下载用于 Windows 的最新的 Ubuntu 容器镜像。

下载后,使用 XZ 工具解压:

C:\Users\mathi\> .\xz.exe -d xenial-container-hyper-v.vhdx.xz
C:\Users\mathi\>

7、准备容器环境

首先创建两个目录:

创建 C:\lcow它将用于 Docker 准备容器时的临时空间。

再创建一个 C:\Program Files\Linux Containers ,这是存放 Ubuntu 容器镜像的地方。

你需要给这个目录额外的权限以允许 Docker 在其中使用镜像。在管理员权限的 Powershell 窗口中运行如下 Powershell 脚本:

param(
[string] $Root
)
# Give the virtual machines group full control
$acl = Get-Acl -Path $Root
$vmGroupRule = new-object System.Security.AccessControl.FileSystemAccessRule("NT VIRTUAL MACHINE\Virtual Machines", "FullControl","ContainerInherit,ObjectInherit", "None", "Allow")
$acl.SetAccessRule($vmGroupRule)
Set-Acl -AclObject $acl -Path $Root

将其保存为set_perms.ps1并运行它。

提示,你也许需要运行 Set-ExecutionPolicy -Scope process unrestricted 来允许运行未签名的 Powershell 脚本。

C:\Users\mathi\> .\set_perms.ps1 "C:\Program Files\Linux Containers"
C:\Users\mathi\>

现在,将上一步解压得到的 Ubuntu 容器镜像(.vhdx)复制到 C:\Program Files\Linux Containers 下的 uvm.vhdx

8、更多的 Docker 准备工作

Docker for Windows 要求一些预发布的功能才能与 Hyper-V 隔离相配合工作。这些功能在之前的 Docker CE 版本中还不可用,这些所需的文件可以从 master.dockerproject.org 下载。

master.dockerproject.org 下载 dockerd.exedocker.exe,并将其放到安全的地方,比如你自己的文件夹中。它们用于在下一步中启动 Ubuntu 容器。

9、 在 Hyper-V 上运行 Ubuntu 容器

你现在已经准备好启动你的容器了。首先以管理员身份打开命令行(cmd.exe),然后以正确的环境变量启动 dockerd.exe

C:\Users\mathi\> set LCOW_SUPPORTED=1
C:\Users\mathi\> .\dockerd.exe -D --data-root C:\lcow

然后,以管理员身份启动 Powershell 窗口,并运行 docker.exe 为你的容器拉取镜像:

C:\Users\mathi\> .\docker.exe pull ubuntu

现在你终于启动了容器,再次运行 docker.exe,让它运行这个新镜像:

C:\Users\mathi\> .\docker.exe run -it ubuntu

恭喜你!你已经成功地在 Windows 上让你的系统运行了带有 Hyper-V 隔离的容器,并且跑的是你非常喜欢的 Ubuntu 容器。

10、获取帮助

如果你需要一些 Hyper-V Ubuntu 容器的起步指导,或者你遇到一些问题,你可以在这里寻求帮助:

Canonical 的 Dustin Kirkland 宣布该公司最近与微软合作让 Ubuntu 容器可以运行在带有 Hyper-V 隔离的 Windows 系统上。

如果你曾经想象过在 Windows 机器上使用你喜欢的 GNU/Linux 发行版(比如 Ubuntu)来运行 Linux 应用,那么现在有个好消息,你可以在 Windows 10 和 Windows 服务器上运行 Docker 容器了。

该技术利用 Ubuntu Linux 操作系统作为宿主基础,通过 Docker 容器镜像和 Hyper-V 虚拟化在 Windows 上运行 Linux 应用。你所需的只是一台 8GB 内存的 64 位 x86 PC,以及加入了 Windows Insider 计划。

“Canonical 和微软合作交付了一种真正特别的体验——在 Windows 10 和 Windows 服务器上运行带有 Hyper-V 隔离的 Ubuntu 容器,”Canonical 的 Ubuntu 产品与战略副总裁 Dustin Kirkland 说,“只需要一分钟就能跑起来!”

在他最近写的一篇博客文章中,Dustin Kirkland 分享了一篇教程,提供了简单易行的指南和截屏,对这种技术感兴趣的人可以去看看。不过该技术目前还只能运行在 Windows 10 和 Windows 服务器上。

根据这篇指南,你只需要在 Windows PowerShell 中运行 docker run -it ubuntu bash 即可启动带有 Hyper-V 隔离的 Ubuntu 容器。如果你在该教程中遇到了困难,你可以加入官方的 Ubuntu ForumsAsk Ubuntu 寻求支持。此外,在 Windows 10 上,Ubuntu 也可以作为 app 从 Windows 商店上得到

Oracle 日前宣布,选择将 Eclipse 基金会作为 Java EE(Java 平台企业版)的新家。Oracle 是与 Java EE 的两个最大的贡献者 IBM 和 Red Hat 一同做出的该决定。

Oracle 软件布道师 David Delabassee 在博文中说,“…… Eclipse 基金会积极参与了 Java EE 及相关技术的发展,具有丰富的经验。这能帮助我们快速移交 Java EE,创建社区友好的流程来推进该平台的发展,并充分利用如 MicroProfile 这样的互补项目。我们期待这一合作。”

Eclipse 基金会的执行总监 Mike Milinkovich 对这次移交持乐观态度,他说,这正是企业级 Java 所需要的,也是社区所期望的。

他说,“开源模式已经一再被时间所证实是成功创新和协作的最佳方式。随着企业更多地转向以云为中心的模式,很显然 Java EE 需要有更快速的创新步伐。移交给 Eclipse 基金会对于供应商来说是一次巨大的机会,他们并不总是有最好的合作机会。我们为个人、小型公司、企业和大型供应商提供开放合作的机会。这将为他们提供一个可靠的平台,让他们可以协作前进,并将支持 Java EE 所需的更快的创新步伐。”

Milinkovich 说,Java EE 成为获准项目也将经历所有的 Eclipse 项目的同样的获准流程。他期待 “Java EE” 融合为一个包含大量子项目的顶级项目。该平台现在包含近 40 个 Java JSR。

Delabassee 说,Oracle 计划将其主导的 Java EE 技术和相关的 GlassFish 技术重新授权给 Eclipse 基金会,包括参考实现、技术兼容性工具包(TCK)和“相关项目文档”。并计划给该平台“重新定名”,但此事尚未确定。

这一移交何时进行还未确定,但 Oracle 希望在 “Java EE 8 完成后尽快进行,以促进快速转型”,Delabassee 承诺,在移交期间,Oracle 将继续支持现有的 Java EE 许可用户,包括升级到 Java EE 8 的许可用户。该公司也将继续支持现有的 WebLogic 服务器版本中的 Java EE,包括之后的 WebLogic 服务器版本中的 Java EE 8。

Delabassee 写道,“我们相信这一计划将使我们可以继续支持现有的 Java EE 标准,同时将其演进为更开放的环境。还有许多工作需要去做,但我们相信正走在一条正确的道路上。”

今天我在 libcurl 内部又做了一个小改动,使其做更少的 malloc。这一次,泛型链表函数被转换成更少的 malloc (这才是链表函数应有的方式,真的)。

研究 malloc

几周前我开始研究内存分配。这很容易,因为多年前我们 curl 中就已经有内存调试和日志记录系统了。使用 curl 的调试版本,并在我的构建目录中运行此脚本:

#!/bin/sh
export CURL_MEMDEBUG=$HOME/tmp/curlmem.log
./src/curl http://localhost
./tests/memanalyze.pl -v $HOME/tmp/curlmem.log

对于 curl 7.53.1,这大约有 115 次内存分配。这算多还是少?

内存日志非常基础。为了让你有所了解,这是一个示例片段:

MEM getinfo.c:70 free((nil))
MEM getinfo.c:73 free((nil))
MEM url.c:294 free((nil))
MEM url.c:297 strdup(0x559e7150d616) (24) = 0x559e73760f98
MEM url.c:294 free((nil))
MEM url.c:297 strdup(0x559e7150d62e) (22) = 0x559e73760fc8
MEM multi.c:302 calloc(1,480) = 0x559e73760ff8
MEM hash.c:75 malloc(224) = 0x559e737611f8
MEM hash.c:75 malloc(29152) = 0x559e737a2bc8
MEM hash.c:75 malloc(3104) = 0x559e737a9dc8

检查日志

然后,我对日志进行了更深入的研究,我意识到在相同的代码行做了许多小内存分配。我们显然有一些相当愚蠢的代码模式,我们分配一个结构体,然后将该结构添加到链表或哈希,然后该代码随后再添加另一个小结构体,如此这般,而且经常在循环中执行。(我在这里说的是我们,不是为了责怪某个人,当然大部分的责任是我自己……)

这两种分配操作将总是成对地出现,并被同时释放。我决定解决这些问题。做非常小的(小于 32 字节)的分配也是浪费的,因为非常多的数据将被用于(在 malloc 系统内)跟踪那个微小的内存区域。更不用说堆碎片了。

因此,将该哈希和链表代码修复为不使用 malloc 是快速且简单的方法,对于最简单的 “curl http://localhost” 传输,它可以消除 20% 以上的 malloc。

此时,我根据大小对所有的内存分配操作进行排序,并检查所有最小的分配操作。一个突出的部分是在 curl_multi_wait() 中,它是一个典型的在 curl 传输主循环中被反复调用的函数。对于大多数典型情况,我将其转换为使用堆栈。在大量重复的调用函数中避免 malloc 是一件好事。

重新计数

现在,如上面的脚本所示,同样的 curl localhost 命令从 curl 7.53.1 的 115 次分配操作下降到 80 个分配操作,而没有牺牲任何东西。轻松地有 26% 的改善。一点也不差!

由于我修改了 curl_multi_wait(),我也想看看它实际上是如何改进一些稍微更高级一些的传输。我使用了 multi-double.c 示例代码,添加了初始化内存记录的调用,让它使用 curl_multi_wait(),并且并行下载了这两个 URL:

http://www.example.com/
http://localhost/512M

第二个文件是 512 兆字节的零,第一个文件是一个 600 字节的公共 html 页面。这是 count-malloc.c 代码

首先,我使用 7.53.1 来测试上面的例子,并使用 memanalyze 脚本检查:

Mallocs: 33901
Reallocs: 5
Callocs: 24
Strdups: 31
Wcsdups: 0
Frees: 33956
Allocations: 33961
Maximum allocated: 160385

好了,所以它总共使用了 160KB 的内存,分配操作次数超过 33900 次。而它下载超过 512 兆字节的数据,所以它每 15KB 数据有一次 malloc。是好是坏?

回到 git master,现在是 7.54.1-DEV 的版本 - 因为我们不太确定当我们发布下一个版本时会变成哪个版本号。它可能是 7.54.1 或 7.55.0,它还尚未确定。我离题了,我再次运行相同修改的 multi-double.c 示例,再次对内存日志运行 memanalyze,报告来了:

Mallocs: 69
Reallocs: 5
Callocs: 24
Strdups: 31
Wcsdups: 0
Frees: 124
Allocations: 129
Maximum allocated: 153247

我不敢置信地反复看了两遍。发生什么了吗?为了仔细检查,我最好再运行一次。无论我运行多少次,结果还是一样的。

33961 vs 129

在典型的传输中 curl_multi_wait() 被调用了很多次,并且在传输过程中至少要正常进行一次内存分配操作,因此删除那个单一的微小分配操作对计数器有非常大的影响。正常的传输也会做一些将数据移入或移出链表和散列操作,但是它们现在也大都是无 malloc 的。简单地说:剩余的分配操作不会在传输循环中执行,所以它们的重要性不大。

以前的 curl 是当前示例分配操作数量的 263 倍。换句话说:新的是旧的分配操作数量的 0.37% 。

另外还有一点好处,新的内存分配量更少,总共减少了 7KB(4.3%)。

malloc 重要吗?

在几个 G 内存的时代里,在传输中有几个 malloc 真的对于普通人有显著的区别吗?对 512MB 数据进行的 33832 个额外的 malloc 有什么影响?

为了衡量这些变化的影响,我决定比较 localhost 的 HTTP 传输,看看是否可以看到任何速度差异。localhost 对于这个测试是很好的,因为没有网络速度限制,更快的 curl 下载也越快。服务器端也会相同的快/慢,因为我将使用相同的测试集进行这两个测试。

我相同方式构建了 curl 7.53.1 和 curl 7.54.1-DEV,并运行这个命令:

curl http://localhost/80GB -o /dev/null

下载的 80GB 的数据会尽可能快地写到空设备中。

我获得的确切数字可能不是很有用,因为它将取决于机器中的 CPU、使用的 HTTP 服务器、构建 curl 时的优化级别等,但是相对数字仍然应该是高度相关的。新代码对决旧代码!

7.54.1-DEV 反复地表现出更快 30%!我的早期版本是 2200MB/秒增加到当前版本的超过 2900 MB/秒。

这里的要点当然不是说它很容易在我的机器上使用单一内核以超过 20GB/秒的速度来进行 HTTP 传输,因为实际上很少有用户可以通过 curl 做到这样快速的传输。关键在于 curl 现在每个字节的传输使用更少的 CPU,这将使更多的 CPU 转移到系统的其余部分来执行任何需要做的事情。或者如果设备是便携式设备,那么可以省电。

关于 malloc 的成本:512MB 测试中,我使用旧代码发生了 33832 次或更多的分配。旧代码以大约 2200MB/秒的速率进行 HTTP 传输。这等于每秒 145827 次 malloc - 现在它们被消除了!600 MB/秒的改进意味着每秒钟 curl 中每个减少的 malloc 操作能额外换来多传输 4300 字节。

去掉这些 malloc 难吗?

一点也不难,非常简单。然而,有趣的是,在这个旧项目中,仍然有这样的改进空间。我有这个想法已经好几年了,我很高兴我终于花点时间来实现。感谢我们的测试套件,我可以有相当大的信心做这个“激烈的”内部变化,而不会引入太可怕的回归问题。由于我们的 API 很好地隐藏了内部,所以这种变化可以完全不改变任何旧的或新的应用程序……

(是的,我还没在版本中发布该变更,所以这还有风险,我有点后悔我的“这很容易”的声明……)

注意数字

curl 的 git 仓库从 7.53.1 到今天已经有 213 个提交。即使我没有别的想法,可能还会有一次或多次的提交,而不仅仅是内存分配对性能的影响。

还有吗?

还有其他类似的情况么?

也许。我们不会做很多性能测量或比较,所以谁知道呢,我们也许会做更多的愚蠢事情,我们可以收手并做得更好。有一个事情是我一直想做,但是从来没有做,就是添加所使用的内存/malloc 和 curl 执行速度的每日“监视” ,以便更好地跟踪我们在这些方面不知不觉的回归问题。

补遗,4/23

(关于我在 hacker news、Reddit 和其它地方读到的关于这篇文章的评论)

有些人让我再次运行那个 80GB 的下载,给出时间。我运行了三次新代码和旧代码,其运行“中值”如下:

旧代码:

real    0m36.705s
user    0m20.176s
sys     0m16.072s

新代码:

real    0m29.032s
user    0m12.196s
sys     0m12.820s

承载这个 80GB 文件的服务器是标准的 Apache 2.4.25,文件存储在 SSD 上,我的机器的 CPU 是 i7 3770K 3.50GHz 。

有些人也提到 alloca() 作为该补丁之一也是个解决方案,但是 alloca() 移植性不够,只能作为一个孤立的解决方案,这意味着如果我们要使用它的话,需要写一堆丑陋的 #ifdef


via: https://daniel.haxx.se/blog/2017/04/22/fewer-mallocs-in-curl/

作者:DANIEL STENBERG 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

我们来解释函数式编程的什么,它的优点是哪些,并且给出一些函数式编程的学习资源。

 title=

这要看您问的是谁, 函数式编程 functional programming (FP)要么是一种理念先进的、应该广泛传播的程序设计方法;要么是一种偏学术性的、实际用途不多的编程方式。在这篇文章中我将讲解函数式编程,探究其优点,并推荐学习函数式编程的资源。

语法入门

本文的代码示例使用的是 Haskell 编程语言。在这篇文章中你只需要了解的基本函数语法:

even :: Int -> Bool
even = ...    -- 具体的实现放在这里

上述示例定义了含有一个参数的函数 even ,第一行是 类型声明,具体来说就是 even 函数接受一个 Int 类型的参数,返回一个 Bool 类型的值,其实现跟在后面,由一个或多个等式组成。在这里我们将忽略具体实现方法(名称和类型已经足够了):

map :: (a -> b) -> [a] -> [b]
map = ...

这个示例,map 是一个有两个参数的函数:

  1. (a -> b) :将 a 转换成 b 的函数
  2. [a]:一个 a 的列表,并返回一个 b 的列表。(LCTT 译注: 将函数作用到 [a] (List 序列对应于其它语言的数组)的每一个元素上,将每次所得结果放到另一个 [b] ,最后返回这个结果 [b]。)

同样我们不去关心要如何实现,我们只感兴趣它的定义类型。ab 是任何一种的的 类型变量 type variable 。就像上一个示例中, aInt 类型, bBool 类型:

map even [1,2,3]

这个是一个 Bool 类型的序列:

[False,True,False]

如果你看到你不理解的其他语法,不要惊慌;对语法的充分理解不是必要的。

函数式编程的误区

我们先来解释一下常见的误区:

  • 函数式编程不是命令行编程或者面向对象编程的竞争对手或对立面,这并不是非此即彼的。
  • 函数式编程不仅仅用在学术领域。这是真的,在函数式编程的历史中,如像 Haskell 和 OCaml 语言是最流行的研究语言。但是今天许多公司使用函数式编程来用于大型的系统、小型专业程序,以及种种不同场合。甚至还有一个面向函数式编程的商业用户[33的年度会议;以前的那些程序让我们了解了函数式编程在工业中的用途,以及谁在使用它。
  • 函数式编程与 monad 无关 ,也不是任何其他特殊的抽象。在这篇文章里面 monad 只是一个抽象的规定。有些是 monad,有些不是。
  • 函数式编程不是特别难学的。某些语言可能与您已经知道的语法或求值语义不同,但这些差异是浅显的。函数式编程中有大量的概念,但其他语言也是如此。

什么是函数式编程?

核心是函数式编程是只使用纯粹的数学函数编程,函数的结果仅取决于参数,而没有副作用,就像 I/O 或者状态转换这样。程序是通过 组合函数 function composition 的方法构建的:

(.) :: (b -> c) -> (a -> b) -> (a -> c)
(g . f) x = g (f x)

这个 中缀 infix 函数 (.) 表示的是二个函数组合成一个,将 g 作用到 f 上。我们将在下一个示例中看到它的使用。作为比较,我们看看在 Python 中同样的函数:

def compose(g, f):
  return lambda x: g(f(x))

函数式编程的优点在于:由于函数是确定的、没有副作用的,所以可以用结果替换函数,这种替代等价于使用使 等式推理 equational reasoning 。每个程序员都有使用自己代码和别人代码的理由,而等式推理就是解决这样问题不错的工具。来看一个示例。等你遇到这个问题:

map even . map (+1)

这段代码是做什么的?可以简化吗?通过等式推理,可以通过一系列替换来分析代码:

map even . map (+1)
map (even . (+1))         -- 来自 'map' 的定义
map (\x -> even (x + 1))  -- lambda 抽象
map odd                   -- 来自 'even' 的定义

我们可以使用等式推理来理解程序并优化可读性。Haskell 编译器使用等式推理进行多种程序优化。没有纯函数,等式推理是不可能的,或者需要程序员付出更多的努力。

函数式编程语言

你需要一种编程语言来做函数式编程吗?

在没有 高阶函数 higher-order function (传递函数作为参数和返回函数的能力)、lambdas (匿名函数)和 泛型 generics 的语言中进行有意义的函数式编程是困难的。 大多数现代语言都有这些,但在不同语言中支持函数式编程方面存在差异。 具有最佳支持的语言称为 函数式编程语言 functional programming language 。 这些包括静态类型的 HaskellOCamlF#Scala ,以及动态类型的 ErlangClojure

即使是在函数式语言里,可以在多大程度上利用函数编程有很大差异。有一个 类型系统 type system 会有很大的帮助,特别是它支持 类型推断 type inference 的话(这样你就不用总是必须键入类型)。这篇文章中没有详细介绍这部分,但足以说明,并非所有的类型系统都是平等的。

与所有语言一样,不同的函数的语言强调不同的概念、技术或用例。选择语言时,考虑它支持函数式编程的程度以及是否适合您的用例很重要。如果您使用某些非 FP 语言,你仍然会受益于在该语言支持的范围内的函数式编程。

不要打开陷阱之门

回想一下,函数的结果只取决于它的输入。但是,几乎所有的编程语言都有破坏这一原则的“功能”。空值、 实例类型 type case instanceof)、类型转换、异常、 边际效用 side-effect ,以及无尽循环的可能性都是陷阱,它打破等式推理,并削弱程序员对程序行为正确性的理解能力。(所有语言里面,没有任何陷阱的语言包括 Agda、Idris 和 Coq。)

幸运的是,作为程序员,我们可以选择避免这些陷阱,如果我们受到严格的规范,我们可以假装陷阱不存在。 这个方法叫做 轻率推理 fast and loose reasoning 。它不需要任何条件,几乎任何程序都可以在不使用陷阱的情况下进行编写,并且通过避免这些可以而获得等式推理、可组合性和可重用性。

让我们详细讨论一下。 这个陷阱破坏了等式推理,因为异常终止的可能性没有反映在类型中。(你可以庆幸文档中甚至没有提到能抛出的异常)。但是没有理由我们没有一个可以包含所有故障模式的返回类型。

避开陷阱是语言特征中出现很大差异的领域。为避免例外, 代数数据类型 algebraic data type 可用于模型错误的条件下,就像:

-- new data type for results of computations that can fail
--
data Result e a = Error e | Success a

-- new data type for three kinds of arithmetic errors
--
data ArithError = DivByZero | Overflow | Underflow

-- integer division, accounting for divide-by-zero
--
safeDiv :: Int -> Int -> Result ArithError Int
safeDiv x y =
  if y == 0
    then Error DivByZero
    else Success (div x y)

在这个例子中的权衡你现在必须使用 Result ArithError Int 类型,而不是以前的 Int 类型,但这也是解决这个问题的一种方式。你不再需要处理异常,而能够使用轻率推理 ,总体来说这是一个胜利。

自由定理

大多数现代静态类型语言具有 范型 generics (也称为 参数多态性 parametric polymorphism ),其中函数是通过一个或多个抽象类型定义的。 例如,看看这个 List(序列)函数:

f :: [a] -> [a]
f = ...

Java 中的相同函数如下所示:

static <A> List<A> f(List<A> xs) { ... }

该编译的程序证明了这个函数适用于类型 a任意选择。考虑到这一点,采用轻率推理的方法,你能够弄清楚该函数的作用吗?知道类型有什么帮助?

在这种情况下,该类型并不能告诉我们函数的功能(它可以逆转序列、删除第一个元素,或许多其它的操作),但它确实告诉了我们很多信息。只是从该类型,我们可以推演出该函数的定理:

  • 定理 1 :输出中的每个元素也出现于输入中;不可能在输入的序列 a 中添加值,因为你不知道 a 是什么,也不知道怎么构造一个。
  • 定理 2 :如果你映射某个函数到列表上,然后对其应用 f,其等同于对映射应用 f

定理 1 帮助我们了解代码的作用,定理 2 对于程序优化提供了帮助。我们从类型中学到了这一切!其结果,即从类型中获取有用的定理的能力,称之为 参数化 parametricity 。因此,类型是函数行为的部分(有时是完整的)规范,也是一种机器检查机制。

现在你可以利用参数化了。你可以从 map(.) 的类型或者下面的这些函数中发现什么呢?

  • foo :: a -> (a, a)
  • bar :: a -> a -> a
  • baz :: b -> a -> a

学习功能编程的资源

也许你已经相信函数式编程是编写软件不错的方式,你想知道如何开始?有几种学习功能编程的方法;这里有一些我推荐(我承认,我对 Haskell 偏爱):

  • UPenn 的 CIS 194: 介绍 Haskell 是函数式编程概念和 Haskell 实际开发的不错选择。有课程材料,但是没有讲座(您可以用几年前 Brisbane 函数式编程小组的 CIS 194 系列讲座
  • 不错的入门书籍有 《Scala 的函数式编程》 、 《Haskell 函数式编程思想》 , 和 《Haskell 编程原理》。
  • Data61 FP 课程 (即 NICTA 课程)通过 类型驱动开发 type-driven development 来教授基础的抽象概念和数据结构。这是十分困难,但收获也是丰富的,其起源于培训会,如果你有一名愿意引导你函数式编程的程序员,你可以尝试。
  • 在你的工作学习中使用函数式编程书写代码,写一些纯函数(避免不确定性和异常的出现),使用高阶函数和递归而不是循环,利用参数化来提高可读性和重用性。许多人从体验和实验各种语言的美妙之处,开始走上了函数式编程之旅。
  • 加入到你的地区中的一些函数式编程小组或者学习小组中,或者创建一个,也可以是参加一些函数式编程的会议(新的会议总是不断的出现)。

总结

在本文中,我讨论了函数式编程是什么以及不是什么,并了解到了函数式编程的优势,包括等式推理和参数化。我们了解到在大多数编程语言中都有一些函数式编程功能,但是语言的选择会影响受益的程度,而 Haskell 是函数式编程中语言最受欢迎的语言。我也推荐了一些学习函数式编程的资源。

函数式编程是一个丰富的领域,还有许多更深入(更神秘)的主题正在等待探索。我没有提到那些具有实际意义的事情,比如:

  • lenses 和 prisms (是一流的设置和获取值的方式;非常适合使用嵌套数据);
  • 定理证明(当你可以证明你的代码正确时,为什么还要测试你的代码?);
  • 延迟评估(让您处理潜在的无数的数据结构);
  • 分类理论(函数式编程中许多美丽实用的抽象的起源);

我希望你喜欢这个函数式编程的介绍,并且启发你走上这个有趣和实用的软件开发之路。

本文根据 CC BY 4.0 许可证发布。

(题图: opensource.com)


作者简介:

红帽软件工程师。对函数式编程,分类理论,数学感兴趣。Crazy about jalapeños.


via: https://opensource.com/article/17/4/introduction-functional-programming

作者:Fraser Tweedale 译者:MonkeyDEcho 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出