转载

对 Ruby 未来的思考

27 Jun 2015
对 Ruby 未来的思考

我使用 Ruby 作为主要编程语言已经有 5 年,我想是时候展望一下 Ruby 的未来,并且留意是否有更好的语言出现。

Ruby 的作者 Matz 在 Ruby Conf 2014 上做的演讲,指出了未来 Ruby 3.0 的几个关键改进:

  • Static Typing
  • Concurrency
  • JIT

Matz 说这些特性有的还停留在设想,未必真的会实现。

这些特性都贴合了目前 Ruby 的弱点或者现实的项目需要,下面我一个个展开讨论。

Static Typing

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 。

我觉得这是一个学术价值大于现实价值的改进,所以对此持观望态度,有也很好,没也无妨。

Concurrency

社区希望 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 提出了更多可能的并发模型:

  • Actor
  • Share / Borrow
  • Stream / Pipeline

之所以需要抽象的并发模型,是因为基于线程和数据共享的并发模型很容易引入 bug。

其实我们现在就可以在 Ruby 中尝试 Actor 模型,那就是项目 Celluloid 。它也是项目 Sidekiq 的基础,所以不少生产环境应用已经在底层用到了 Actor 模型。

我非常期待 Ruby 在并发能力上的改进。

JIT

JIT 是 Just-in-time compilation(即时编译)的缩写。JIT 是优化性能的一个手段,通过把代码编译成机器码从而提高运行效率。有名的通过 JIT 提高性能的例子有 JavaScript V8 引擎。

我不熟悉编译原理,也不工作在这一层面,所以没有去做太多了解。

要补充的是,一般的 Web 应用的性能瓶颈并不在编程语言的性能,多在于数据库 IO。这可以通过缓存、优化查询语句、增加数据冗余解决。编程语言更多时候时作为胶水存在:执行业务逻辑,繁重的任务转交外部组件处理。

能提高性能总是好事,没有人不喜欢提高性能。性能提高能减少 Ruby 是否到达一定规模就需要更换语言的质疑。

新语言

现在正处于第二次文化复兴,每年都有不少新语言发布。学习一些新语言可以更好的理解编程语言的发展趋势。如果新语言比现在用的更好,切换到新语言进行工作也不赖。

带着这个想法,我学习了最近新兴起的语言,它们都有可能成为下一个主流语言。由于我只进行了浅层的学习(看完官方文档的程度),所以不要以我的看法作为是否选择这些语言的判断。

Rust

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 推出的语言,主要目标领域是系统编程。

在众多特性中,我觉得比较有趣的是以下特性。

Immutable

默认情况下变量是不可变的,要改变数据时只有返回新的数据。

let x = 5; x = 10; // compile error ! 

变量不可变不是 Rust 首创的,而是从一些函数式语言中借鉴而来。变量不可变的好处是没有了数据竞争,非常适合用于并发计算。

为了灵活性也允许使用可变变量,这时候需要显式声明 mut

let mut x = 5; x = 10; 

Ownership

Rust 没有自动垃圾回收,但是却不用程序员管理内存,这是怎么做到的呢?答案是 Rust 的 ownership 机制。变量存在于某个上下文环境,这个环境“拥有”这个变量。当上下文环境结束的时候,变量被一并回收。举个例子:

fn foo() {     let v = vec![1, 2, 3]; } 

当调用 foo() 的时候, v 被创建并分配内存;当 foo() 结束的时候, v 被回收。这看起来很像垃圾回收,不同的是 Rust 在编译期就已经确定了 v 回收的时机,而 GC 是在运行时回收。

Ownership 机制还有很多附属规则,详细的要看 官方文档 的说明。

Cargo

Rust 有一个预装的项目管理工具:Cargo,它很像 Ruby 的 Gem + Bundler,因为 Cargo 的创建者之一 Yehuda Katz 也是 Bundler 的开发者。

Cargo 可以用来新建项目、下载依赖、编译源码、运行程序、运行测试等等。有了 Cargo,用 Rust 编程的体验有点接近 Ruby,而不是像传统的编译语言那样要操作一整个编译工具链。

小结

Rust 作为系统编程语言,有望成为 C++ 的替代品。Rust 的内存管理机制更严谨,可以防止指针越界这类安全漏洞。

不过也正因为编译器很严谨,所以要写很多类型声明或者引用/解引用,比较难上手。如非必要我不想这样写代码。

Go

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

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. 

Channels

从 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/Elixir

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 更适合我。有几个理由:

  1. 我已经积累了不少 Ruby 经验,用 Ruby 开发的效率更高。
  2. Ruby 有一个很活跃的社区。
  3. Ruby 现存的问题(性能/多核支持)有可能在未来解决,而这些问题目前对我并没有造成什么影响。
  4. Ruby 的语法更优美,作为每天见面的工具我更希望用一个能让我心情愉快的。

另外从对其它语言对对比来看,我有几个看法:

  1. 动态语言不是性能硬伤,因为 Erlang 就是动态语言,同时性能也很好。(另一个例子是 JavaScript V8)
  2. 编程语言总是在互相借鉴,Ruby 的依赖管理被新语言借鉴,Ruby 也可以从别的语言借鉴新特性。
  3. 轻量级进程+消息的并发模型应该是未来主流。

从这几个看法推导出,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日
正文到此结束
Loading...