Elixir,一个巴西波兰帅哥José Valim写出来的,José 是谁?rails 的核心开发者。接触web以来发现,Ruby社区一直是一个很有才华的社区。创造性的东西层出不穷。新的理念和方式,巧夺天工。以致于对于其他社区,这些奇淫巧计更像是魔法,异端黑魔法。
程式语法发展了这么多年,OOP的方式貌似成为了工业标准。大规模网络应用的出现,高并发的应用场景越来越多,古老的函数式语言,仿佛尘封已久的史前怪兽,解除了封印重现人间。当然这么说有点夸张,不过现在函数式语言被人们的关注度越来越高。
大学期间阅读了《黑客与画家》,作者动情的描述了Lisp的神奇。怀着好奇的心态一边看《魔法书》一边学了Scheme。领略了函数式思想的皮毛,就暂且搁置了。前段时间无聊,又折腾了一下Haskell,很多数学问题用Haskell很顺手,当然好多东西都一知半解。
于是考察了下热门的所谓下一代编程语言 Clojure,Erlang,Rust,Go。Clojure是lisp方言,生态圈却是java社区,java很好很强大,但我不喜欢。Go很火,项目很多,好像没有带给我新的思想。Rust升级太猛烈,我这样的小民还是先观望。Erlang 的语法对于我实在太诡异。寻寻觅觅。每当你等得绝望的时候,公共汽车才会缓缓开来。没错,我的救命公车就是 Elixir。
Elixir,一个基于Erlang虚拟机的玩意,却不是玩具,而是一个强大又有趣的编程语言。用老爷子的话就是 Power of Erlang,Fun of Ruby。没错,具有Erlang强大性能,抽象能力,已经甜甜的语法糖,像Ruby一样有趣。
刚接触Elixir不久,已然被吸引。并且帮助我解决了以前的疑惑,给人眼前一亮的感觉。Python也是我喜欢的,下面就用 Python 和 Elixir 来简单的对比下递归思想。
据说计算机的两大思想,一是指针,其二就是递归。这些概念虽然常常听到,用到,甚至用了都不知道。多数时候,又很难说出所以然来,大概是修炼未到家,参悟不够。
Exlir 的文档上有个例子,
重复输出一个值
这个问题太简单了,任何一个开发者都能想到循环,用Python大概如下:
def repeat(msg, times):
for _ in range(times):
print msg
repeat("hello python", 5) # 打印五行 hello python
当然,你也可以这样
def repeat(msg, times):
while times:
print msg
times -= 1
repeat("hello python", 5) # 打印五行 hello python
这两段代码都使用了常见的循环控制。核心就是通过times直接或间接的改变某个循环变量,来做计算。
那么,Elixir又是如何做的呢?
defmodule Repeat do
def print(msg, times) when times <= 0 do
IO.puts msg
end
def print(msg, times) do
IO.puts msg
print(msg, times - 1)
end
end
Repeat.print('hello elixir', 5)
对于 Exlir 的实现,我们并没有用循环,而是在函数内部调用了函数,即递归。简单设想一下,我们的函数执行的功能就是打印输出,给了一个5,就说明是要打印5次,也就是函数调用五次了。通过这样的方式思考递归,会更加自然。对此,格雷厄姆在《ANSI Common Lisp》对递归有很好的解释
起初,许多人觉得递归函数很难理解。大部分的理解难处,来自于对函数使用了错误的比喻。人们倾向于把函数理解为某种机器。原物料像实参一样抵达;某些工作委派给其它函数;最后组装起来的成品,被作为返回值运送出去。如果我们用这种比喻来理解函数,那递归就自相矛盾了。机器怎可以把工作委派给自己?它已经在忙碌中了。
较好的比喻是,把函数想成一个处理的过程。在过程里,递归是在自然不过的事情了。日常生活中我们经常看到递归的过程。举例来说,假设一个历史学家,对欧洲历史上的人口变化感兴趣。研究文献的过程很可能是:
- 取得一个文献的复本
- 寻找关于人口变化的资讯
- 如果这份文献提到其它可能有用的文献,研究它们。
过程是很容易理解的,而且它是递归的,因为第三个步骤可能带出一个或多个同样的过程。
上面这个小例子,当然仅仅是递归,用Python也一样可以实现。再来看第二个问题。求一个列表的数据之和
我们先用python来实现,大概如下:
l = [1, 2, 3, 4, 5]
def sum(l):
s = 0
for i in l:
s += i
return s
print sum(l)
当然,上面的还是一个循环的问题,我们也用python的递归来实现一遍:
def sum(l):
if len(l) <= 1:
return l[0]
else:
return l[0] + sum(l[1:])
接下来,再用 Elixir写一个:
defmodule Math do
def sum(l) when (length l) <= 1 do
hd l
end
def sum(l) do
sum(tl l) + hd l
end
end
IO.puts Math.sum(l)
核心思想都是,取一个数和下一个数相加的和再和下一个数相加。函数的功能就是把 sum 和下一个数相加。当然,Elixir有更优雅的语法糖,下面两种都是使用了 Elixir的模式匹配特性
defmodule Math do
def sum([head | tail], s) do
sum(tail, head + s)
end
def sum([], s) do
s
end
end
defmodule Math do
def sum([head | tail]) when tail == [] do
head
end
def sum([head | tail]) do
sum(tail) + head
end
end
IO.puts Math.sum(l)
上面的小例子中,使用列表做参数,每次削减一点列表的递归方式,称为“递减”算法,是函数式编程的核心。当然,递归还可以处理其每个元素的方式,称为“映射(map)”算法。再看第一个问题,给一个列表,并把列表的数值加倍。
首先,还是用 python来实现,python丰富的语法,实现这个多种多样
l = [1, 2, 3, 4]
# 列表解析的方式
def double(l):
return [x * 2 for x in l]
double(l) # [2, 4, 6, 8]
# map算法
def double(l):
return map(lambda x: x * 2, l)
在python中,我们可以直接调用 map 内置函数来作计算,可是这个 map又是如何实现的呢?上面已经说了,处理每个元素的递归,是为 映射。因为我们用python的map递归来处理。
def double(l):
if not l:
return []
return [l.pop(0) * 2] + double(l)
# 当然,也可以写成一句话
def double(l):
return [l.pop(0) * 2] + double(l) if l else []
现在再来看看Elixir的实现:
l = [1, 2, 3, 4]
defmodule Math do
def double(l) when l == [] do
[]
end
def double(l) do
[(hd l) * 2] ++ double(tl l)
end
end
defmodule Math do
def double([]) do
[]
end
def double([head | tail]) do
[head * 2 | double(tail)]
end
end
Elixir的第一个实现中,算法的描述,就是取出每个列表元素来处理。而第二个算法则用了 |
这个运算符语法糖。
尝试了上面几个例子,Elixir用递归的方式来处理通常的循环
计算。虽然python也可以写出类似的代码,可是python的解释器对深层次的递归并没有优化,性能不是特别好,迭代比递归更合适。当然即使是python实现的递归算法,代码都没有Elixir的美观、优雅。Elixir的递归让整个处理过程看起来是那么的透明清晰。抽象能力相当强,上面的例子仅仅是Elixir的冰山一角,更多的特性有待
Elixir是一个迷人的东西,就像一个谜。
(文中的code仅仅为了描述算法概念,未做优化。)
推荐阅读:
Recursion
颠覆者的游戏:程序语言
《ANSI Common Lisp》 ---递归 (Recursion)
网友评论