我使用 Ruby 作为主要编程语言已经有 5 年,我想是时候展望一下 Ruby 的未来,并且留意是否有更好的语言出现。
Ruby 的作者 Matz 在 Ruby Conf 2014 上做的演讲,指出了未来 Ruby 3.0 的几个关键改进:
Matz 说这些特性有的还停留在设想,未必真的会实现。
这些特性都贴合了目前 Ruby 的弱点或者现实的项目需要,下面我一个个展开讨论。
Static typing,我第一次看到这个词感觉好日子要到头了。我选择 Ruby 的一个重要原因是它的动态类型,我不想写类型声明!!!
不过看完 Matz 的演讲和一些相关文章后,我感觉不用这么悲观了,因为有可能不需要改变目前写代码的习惯。用 Matz 的例子做说明:
def foo(x) x.to_int # now all x must have .to_int end foo(1) # OK foo('a') # no good! doesn't have to_int!
Matz 设想的静态类型实现是非显式、基于方法签名的静态类型。在上面的例子中,并没有显式指定 x
的类型,但是静态分析器会判断出 x
需要有一个 to_int
方法,当传入一个字符串的时候,字符串没有 to_int
方法就会报错,就好像指定了类型一样。
理想情况下,静态分析器可以检查出部分类型错误,及早发现问题。有观点认为 Ruby 不适合大型项目的其中一个原因是缺乏类型检查,运行中发生类型错误是不能容许的。
但我很怀疑静态分析对发现错误有多大作用,因为类型错误只是实际项目中错误的一小部分,更常见并且严重的错误是逻辑错误。很容易可以写一个没有类型错误但有逻辑错误的代码:
def more_than?(x, y) x < y end more_than?(1, 2) # return true, but we need false
要处理逻辑错误,需要多写测试,收集系统异常,校对关键数据。类型错误通常在单元测试阶段就能暴露出来。
静态类型系统确实能减少运行时的错误,提升数据的完整性,所以很容易误导人们觉得只要能通过编译让程序跑起来,那它基本上就没什么 bug 了。人们在用强静态类型系统的语言写程序时似乎很少依赖单元测试,当然这也可能只是我的想象罢了。
—— Steve Yegge,弱类型机制够不够强。
另一个问题是,对于 Ruby 这么动态的语言,静态检查可能很难实现。
It’s going to take serious research, not brute-force hacking; the answer will probably look more like a PhD thesis than a pull request.
—— Tom Stuart, Consider static typing 。
我觉得这是一个学术价值大于现实价值的改进,所以对此持观望态度,有也很好,没也无妨。
社区希望 Ruby 提供真正并发支持的呼声由来已久。
现在 Ruby 并非没有并发能力,只是支持得不彻底。Ruby 应用通常使用多进程的方式实现并发,例如 Unicorn、Resque;另外也有用多线程方式实现并发,例如 Puma、Sidekiq。
多进程问题是对内存需求大,通常 CPU 并没有被充分利用就达到了主机内存上限。而由于 Ruby 中仍有 GIL,无法利用多核,多线程只有在 IO 密集的时候才能发挥作用,计算密集的时候对比单线程并没有提升,还多了切换线程的开销。
此前 Matz 对移除 GIL 有一个模糊的计划:
My vague plan for Ruby GIL is 1) add more abstract concurrency e.g. actors 2) add warning when using threads directly 3) then remove GIL
— Yukihiro Matsumoto (@yukihiro_matz) 2014年8月1日在 RedDotRuby 2015 上,Matz 提出了更多可能的并发模型:
之所以需要抽象的并发模型,是因为基于线程和数据共享的并发模型很容易引入 bug。
其实我们现在就可以在 Ruby 中尝试 Actor 模型,那就是项目 Celluloid 。它也是项目 Sidekiq 的基础,所以不少生产环境应用已经在底层用到了 Actor 模型。
我非常期待 Ruby 在并发能力上的改进。
JIT 是 Just-in-time compilation(即时编译)的缩写。JIT 是优化性能的一个手段,通过把代码编译成机器码从而提高运行效率。有名的通过 JIT 提高性能的例子有 JavaScript V8 引擎。
我不熟悉编译原理,也不工作在这一层面,所以没有去做太多了解。
要补充的是,一般的 Web 应用的性能瓶颈并不在编程语言的性能,多在于数据库 IO。这可以通过缓存、优化查询语句、增加数据冗余解决。编程语言更多时候时作为胶水存在:执行业务逻辑,繁重的任务转交外部组件处理。
能提高性能总是好事,没有人不喜欢提高性能。性能提高能减少 Ruby 是否到达一定规模就需要更换语言的质疑。
现在正处于第二次文化复兴,每年都有不少新语言发布。学习一些新语言可以更好的理解编程语言的发展趋势。如果新语言比现在用的更好,切换到新语言进行工作也不赖。
带着这个想法,我学习了最近新兴起的语言,它们都有可能成为下一个主流语言。由于我只进行了浅层的学习(看完官方文档的程度),所以不要以我的看法作为是否选择这些语言的判断。
Rust is a systems programming language that runs blazingly fast, prevents nearly all segfaults, and guarantees thread safety.
http://www.rust-lang.org/
Rust 是 Mozilla 推出的语言,主要目标领域是系统编程。
在众多特性中,我觉得比较有趣的是以下特性。
默认情况下变量是不可变的,要改变数据时只有返回新的数据。
let x = 5; x = 10; // compile error !
变量不可变不是 Rust 首创的,而是从一些函数式语言中借鉴而来。变量不可变的好处是没有了数据竞争,非常适合用于并发计算。
为了灵活性也允许使用可变变量,这时候需要显式声明 mut
:
let mut x = 5; x = 10;
Rust 没有自动垃圾回收,但是却不用程序员管理内存,这是怎么做到的呢?答案是 Rust 的 ownership 机制。变量存在于某个上下文环境,这个环境“拥有”这个变量。当上下文环境结束的时候,变量被一并回收。举个例子:
fn foo() { let v = vec![1, 2, 3]; }
当调用 foo()
的时候, v
被创建并分配内存;当 foo()
结束的时候, v
被回收。这看起来很像垃圾回收,不同的是 Rust 在编译期就已经确定了 v
回收的时机,而 GC 是在运行时回收。
Ownership 机制还有很多附属规则,详细的要看 官方文档 的说明。
Rust 有一个预装的项目管理工具:Cargo,它很像 Ruby 的 Gem + Bundler,因为 Cargo 的创建者之一 Yehuda Katz 也是 Bundler 的开发者。
Cargo 可以用来新建项目、下载依赖、编译源码、运行程序、运行测试等等。有了 Cargo,用 Rust 编程的体验有点接近 Ruby,而不是像传统的编译语言那样要操作一整个编译工具链。
Rust 作为系统编程语言,有望成为 C++ 的替代品。Rust 的内存管理机制更严谨,可以防止指针越界这类安全漏洞。
不过也正因为编译器很严谨,所以要写很多类型声明或者引用/解引用,比较难上手。如非必要我不想这样写代码。
Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.
http://golang.org/
Go 是 Google 员工(包括 C 语言设计者之一 Ken Thompson)推出的语言。
语法的细枝末节就不多说了,我最关注的是 Go 如何支持并发 —— Goroutines。
Goroutines 是 Go 发明的名词,意指一个并发执行的方法。其实本质也是轻量级进程+无共享,由 runtime 调度分配到多个线程执行。
Goroutines 的优点是它的写法非常简单:
go list.Sort() // run list.Sort concurrently; don't wait for it.
在方法调用前加 go
关键词,这个方法就变成并发执行的了。
也可以用匿名方法并发执行一段代码:
go func() { time.Sleep(delay) fmt.Println("message") }() // Note the parentheses - must call the function.
从 Goroutines 中返回数据,或者相互间传递数据,需要用到 Channels。
用一段代码说明:
c := make(chan int) // Allocate a channel. // Start the sort in a goroutine; when it completes, signal on the channel. go func() { list.Sort() c <- 1 // Send a signal; value does not matter. }() doSomethingForAWhile() <-c // Wait for sort to finish; discard sent value.
Channel 像一个单向队列,消息(数据)从一端进入,由另一端取出。如果取数据时 channel 为空,则取数据的 goroutine 会阻塞,直到 channel 里有数据为止。Channel 也可以设置长度限制,当 channel 已满,往里面发消息的 goroutine 会阻塞,直到另一端有数据被取出。
Go 自发布以来,每年热度都不断升高。出现了一些杀手级应用,例如 Docker;国内也有公司主要以 Go 进行开发,例如七牛。Go 的形势一片大好。
让我比较在意的是,Heroku 把它的 cli 工具从 Ruby 迁移到 Go 重写,这说明 Go 笼络了一部分脚本语言用户的心——近似脚本语言的语法,获得编译语言的性能。不过在学习了语言之后,觉得 Go 还是要比 Ruby 繁琐一些,没有让我觉得有必要迁移。有兴趣的可以看这篇文章做个对比: Python, Ruby, and Golang: A Command-Line Application Comparison 。
Erlang is a programming language used to build massively scalable soft real-time systems with requirements on high availability. Some of its uses are in telecoms, banking, e-commerce, computer telephony and instant messaging. Erlang's runtime system has built-in support for concurrency, distribution and fault tolerance.
http://www.erlang.org/
Elixir is a dynamic, functional language designed for building scalable and maintainable applications.
Elixir leverages the Erlang VM, known for running low-latency, distributed and fault-tolerant systems, while also being successfully used in web development and the embedded software domain.
http://elixir-lang.org/
Erlang 并不是一个新语言,它在 1987 年由爱立信发布,用于开发电信系统,一直以来属于比较专用的语言。最近由于多核编程更受重视了,Erlang 天生用于构建并发、分布式,高可靠性系统的优点开始闪亮。
Elixir 是基于 Erlang VM 上开发的一套语言。它融合了 Erlang VM 的可靠性和函数式编程理念还有 Ruby 语法的平易近人,最近获得了不少关注。Elixir 跟 Erlang 的关系有点像 CoffeeScript 跟 JavaScript 的关系。由于 Elixir 的语法更友好,本文以 Elixir 的代码做例子。
忽略一些语法细节,Erlang 最重要的是它的并发编程模型,它也影响了前面介绍的两个语言的并发模型设计。Erlang 的并发执行单元称为 Processes,但不同于系统进程,Erlang 的 Processes 是由 VM 调度的轻量级进程。(没错,跟 Goroutines 差不多,Go 应该是学 Erlang 的。)
在 Elixir 中,用 spawn
语句生成一个 Processes:
pid = spawn fn -> 1 + 2 end
spawn
返回一个进程 id,随后可以向这个进程 id 发送消息:
send pid, {:hello, "world"}
进程使用 receive
语句接收消息:
receive do {:hello, msg} -> msg end
在前面的例子中,进程执行完毕就退出了,给它发送消息并没有什么用。现在做一个复杂点的例子,新建进程,并且等待消息:
pid = spawn fn -> receive do {:hello, msg} -> IO.puts msg end end send pid, {:hello, "world"}
新的进程在接收到消息,并打印出 "world"
之后退出。
Erlang/Elixir 中大量依赖进程和消息编程。除了进程外,Erlang 还有一个更高层的 OTP 平台,用于编写分布式、高容错的系统,是真正的为并发计算而生的语言。更多语言细节这里无法叙述,只有去看官网文档了。
值得一提的是,Elixir 的 Web 框架 Phoenix 是我看过很多类 Rails 框架中真正对 Rails 有威胁的,它既继承 Rails 框架的开发效率,又拥有 Erlang VM 的运行效率。
我个人觉得 Elixir 可以作为 Ruby 的备用语言,如果真的遇到扩展瓶颈了,迁移到 Erlang/Elixir 要比迁移到别的平台(例如 JVM)更愉快。
虽然带着更换语言的想法去学习新语言,最后发现还是 Ruby 更适合我。有几个理由:
另外从对其它语言对对比来看,我有几个看法:
从这几个看法推导出,Ruby 还有不少上升空间,值得继续投入。现在我很赞同 DHH 的这条推文:
Ruby is my favorite toy of all time: Endless fun to play with, full of wondrous magic, and the most patient, creative teacher ❤️✨
— DHH (@dhh) 2015年4月27日