我对 Erlang 编程理念的理解:以分布式架构师的角度写代码。
函数式编程
Erlang 里面的函数是数学里面的函数:必须有返回值。 只要是函数必然有返回值,函数是一个过程,以英文的句号为函数结束符。 函数结束之前的表达式就是该函数的返回值。 所以这也是在 Erlang 里面的函数不会看到任何 return 语句的原因。 C++ 等其他语言的函数和函数之前可以通过共享变量来实现消息传递。 Erlang 里面的函数不可以,消息的传递通过函数的传入和传出。 也只是为什么 Erlang 号称天生之处并行处理的原因, 因为他们不共享变量,也就不需要加锁。
很多人听到函数式编程都会觉得高大上或者晦涩难懂。 因为函数是编程没有 for 循环语句, 但是在我看来,关键在于会使用【列表推倒】和【尾递归】来进行循环遍历。 说到函数式编程就会拿快速排序说事,下面这个示例是 Erlang 版本的快速排序:
-module(sort).
-export([qsort/1]).
qsort([]) -> [];
qsort([Pivot | T]) ->
qsort([X || X <- T, X < Pivot]
++ [Pivot] ++
qsort([X || X <- T, X >= Pivot]).
非常简洁,[Pivot | T] 就是拿列表的第一个元素当快排中的 Pivot 。
[X || X <- T, X < Pivot]
上式就是【列表推导】, 含义就是找出列表 T 中所有元素小于 Pivot 中的元素组成一个新的列表。 不过,这个例子显然性能不高,只是一个示例。
很多人一直在鼓吹函数式语言马上就要迎来朝阳, 但是在我看来,函数式编程永远只能是小众语言, 这就像当年的 lisp machine ,被鼓吹的天花乱坠还是夭折了。 现在主流的计算机架构都是冯诺依曼体系的,并不是最适合函数式语言的生存土壤。
一切都是常量
没有变量,也就没有通过变量共享状态导致的资源竞争,也就不需要加锁。 任何状态的变化都是通过函数的输入输出来进行改变, 轻量级进程的状态变化也是靠消息传递(函数的输入输出)来实现。 这也是为什么有人说函数式编程适合高并发的原因,因为他们没有变量, 一切都是常量。
轻量进程
Erlang 里面有 spawn 函数,可以快速的创建一个 process , 这里的 process 不是操作系统的进程,而是 Erlang 自己的轻量进程。 Erlang 轻量到超乎你想象, 构建 kv 数据库的时候,甚至可以对不同的 key 分配给不同的进程。 而且进程的表示单位是 Pid ,只要知道进程的 Pid, 哪怕该进程是在别的机器上面,都可以很轻易的发送给它。 原因是 Erlang 的【天生自带RPC通信】和【自带端口映射】
天生自带RPC通信
ToPid ! Data
ToPid 是接受方进程的id , Data 可以是 Erlang 的任何类型,比如
Pid ! {name, "jb51.net"}.
也就是可以直接把任何数据结构当成消息发送,天生自带 RPC 通信。 (虽然本来 RPC 的含义是“远程过程调用”,不过其实反正就是帮你序列化了数据结构,Erlang 的 ! 操作符也是如此。)
进程端口映射
节点之间发消息在代码里面的表示也还是
ToPid ! Data
也就是在写代码的时候,根本不用考虑该进程是在哪台机器上面, 无论是本 Erlang 进程(这里的进程是操作系统级别的进程,不是 Erlang 的轻量进程) 内, 还是其他机器的进程,都不用管。 这是因为有 epmd 的存在。
Epmd是Erlang Port Mapper Daemon的缩写,在Erlang集群中相当于dns的作用,供给节点名称到端口的查询办事,epmd绑定在总所周知的4369端口上。
有了 epmd ,写分布式程序就好像写单机程序一样简单。
严密的模块化管理
Erlang 的模块类似 C++ 中的 namespace(命名空间),但是比命名空间更利于高效的软件工程管理。
在 Erlang 项目源码中处处可见如下代码。
-module(my_app).
-export([start/2, stop/1]).
-module 指明模块名,-export 指明导出的函数。 未被导出的函数都无法被外界调用。 从软件工程上看的话,这样使得模块功能和使用方法更加清晰。 使用者只需要关心如何 -export 里面的函数即可。 相比较之下 C++ 对这方面特别不规范,而 Java 通过对类声明为 public class 指明可以被外界使用, Node.js 也是使用 export 来显示声明可以被外界使用的函数。
行为模式
-module(ecomet_app).
-behaviour(application).
%% comment: Application callbacks
-export([start/2, stop/1]).
-behavior(application).
Erlang/otp 里面的【行为模式】概念等价于 OOP 里面的接口概念。 上面代码示例的意思就是该模块(ecomet_app)遵守的行为模式是(application)。 刚行为模式需要实现的两个接口函数就是 -export([start/2, stop/1]). 。
另一个示例如下是遵守监督者(supervisor)行为模式, 实现的一个接口函数是 -export([init/1]). 。
-module(ecomet_sup).
-behaviour(supervisor).
%% Supervisor callbacks
-export([init/1]).
监督者机制
Erlang/otp 的天生分布式特性在监督机制里面体现的很好, 每一个 otp 应用启动的时候,都是启动监督者(supervisor)和工作者(worker)。 他们的关系是树形结构,每个工作者的上级都会有监督者, 每个监督者的上级也可能有监督者。 当工作者异常退出的时候,监督者会根据相应的参数决定是否对工作者进行重启。 如果重启失败的话监督者也会退出,而更加上层的监督者收到信号后会对他们进行重启等处理。 这个监督者机制非常好理解,其实就是 OOP 编程里面的 try ... catch 异常处理机制。 当出现异常的时候一层一层的往上抛出,直到有人重启。
otp平台
Erlang 最强大的地方也是最让我感觉难学的地方,就是它的 otp 平台。 各种行为模式, 让我感觉就像多年以前学习 MFC 的时候, 感觉很强大,但是却总是感觉自己被按死在一条特定的轨道上面奔跑, 有种不*的疲惫感。
代码热切换
热切换也叫热升级,大部分情况下,如果需要对 C++/Java 程序进程版本升级, 则需要重启进程。 Erlang 支持热切换的意思就是可以在运行的时候进行代码升级。 升级过程不影响进程的运行, 而且在过渡阶段新旧版本还可以共存。 是不是碉堡了。这个功能对于那些需要 7x24 高可用的服务来说简直就是爽爆了。
Erlang 进程本身可以通过一个类似“后门”的控制台 erl 来实时的查看状态, 甚至直接使用控制台来修改配置等,非常方便,这对于大部分其他语言来说, 简直就是黑魔法般神奇的存在。
典型缺点
1.文档太少,出现问题搜索出来的答案也少。
2.Erlang 人才稀缺,招聘不易。
3.动态语言最典型的就是调试不易。
4.上手门槛较高。
最后,我只是 Erlang 的入门初学者, 因为工作中需要使用 ejabberd (Erlang 的开源项目), 从而学习了 Erlang , 欠缺实战经验,所以这篇文章标题起为 【Erlang初体验】。