静态版Ruby?Crystal语言试用

目录

前几天在思考,Python和JS都拥抱了类型检查(类型注释),但是Ruby却只能用Sorbet这样的影响性能的类型检查器(Ruby没有官方的类型检查工具,引入静态类型检查的gem反而降低了性能),在搜索中找到了 Crystal 这门语言。

https://crystal-lang.org/

看描述我就惊艳到了:作为一个静态语言,Crystal居然长得这么像Ruby,于是本着不妨玩玩的想法,我进行了Crystal的初试。

# A very basic HTTP server
require "http/server"

server = HTTP::Server.new do |context|
context.response.content_type = "text/plain"
context.response.print "Hello world, got #{context.request.path}!"
end

address = server.bind_tcp(8080)
puts "Listening on http://#{address}"

# This call blocks until the process is terminated
server.listen

简介(翻译自Github README)

目标

Crystal 是具有以下目标的编程语言:

  • 和Ruby相似(但不要求兼容)的语法
  • 静态类型检查,但不要求处处指定变量或者方法的类型
  • 可以call C代码
  • 对编译时进行评估和生成代码,避免boilerplate code.
  • 编译成高效的原生代码

为什么?

我们喜欢Ruby写代码的高效率

我们也喜欢C运行代码的高效率

我们想要集二者所长

我们想要编译器能理解我们,而不是我们对编译器指定类型

我们想要完整的面向对象

而且,我们不想为了让代码跑的更快而去写C代码。

下载安装

请遵循RTFM方法,Read The Fucking Manual
https://crystal-lang.org/install/

语法

Crystal的语法高度类似于 Ruby,建议先学会Ruby的语法再说Crystal。

这里只提一些比较亮点的东西。

A+B problem,但是自动泛型

Crystal有自动类型推断的功能。也就是说,大部分情况下类型声明可以直接不写,比如这样

def add(a, b)
a + b
end


puts add(1, 2) # 3
puts add("1", "2") # "12"

这段代码有些类似于C++这样写

auto add(auto a, auto b) {
return a + b;
}

我个人很喜欢 Ruby和Crystal一脉相承的一个想法:程序员的幸福最大化。 Ruby是这样的,程序员只要负责写的爽就行了,而Ruby要考虑的事情就多了(不是)

让我们看看 Rails 信条 中怎么说

早期 Ruby 的极端邪说就是把程序员的幸福度放到第一位。还把追求幸福置于驱动编程语言与生态圈前进的考量之上。

然而 Python 可能对于“用一种方法,最好只有一种方法来完成一件事”而感到自豪,而 Ruby 则喜欢自身表现力与巧妙。Java 是饱受软件工程师的强力推崇,Ruby 则在欢迎工具里就附上了自尽的绳子。Smalltalk 专注于消息传递的纯粹性,Ruby 则累积关键字和臃肿的语法构造。

Ruby 与众不同的原因是看重的事情不一样。这些考量,都是为了满足和追求软件工程师的幸福。这些追求导致了与其他编程语言的辩论,也打开了主流文化对于究竟什么是软件工程师,以及应该如何应对软件工程师的认知。

Ruby 不仅承认,而且从设计上适应和提升软件工程师的感受。不管它们是不足的、奇思妙想的,还是令人喜悦的。Matz 跨越了惊人难度的实践门槛,让机器面有喜色,且富有人性。Ruby 满满是视觉上的错觉,在我们看起来 Ruby 很简单,清晰,也很优美,背后其实是杂技般的错综复杂。这些选择不是没有代价(问问 JRuby 那些试着要对 Ruby 逆向工程的人看看!),这也是为什么,这是很值得赞扬的一件事。

这是对软件开发另一种愿景的致敬,也决定了我对 Ruby 的钟爱。这不止是简单易用,不仅是美学的元素,也不是单一的技术成就。而是一种愿景,是反文化。Ruby 是一个不适应呆板专业软件开发的人,而是专属于爱好之士的乐土。

回到刚刚的 A+B problem。 即使C++有 auto (更多静态类型语言会要求你写冗长的泛型),我们为什么不能更进一步呢?传入两个参数,把它们加起来。电脑理所应当可以从参数类型推导结果类型。那我为什么还要写呢?

我在意的是我写得爽不爽,而不是它是不是符合哪个RFC的哪一条。所以,你已经是个成熟的编程语言了,该学会揣摩我到底要写什么类型了。我很喜欢。

面向对象,真的

Ruby和Crystal都是特别面向对象的语言。比绝大多数自称面向对象的语言还要面向对象。

举个例子,Ruby/Crystal支持这样的写法

3.times do |i|
puts i
end
# 输出:
# 0
# 1
# 2

这是因为哪怕是数字3也被视为一个对象,是Object的子类,可以有自己的methods

所以在Crystal内可以写出这样极其直观的代码

puts 3.seconds # 00:00:03
puts 3.minute # 00:03:00
puts 3.hours # 03:00:00
puts 3.years # Time::MonthSpan(@value=36)

这些也可以传入到 sleep 中作为参数。sleep 3.seconds 即为sleep3秒,所有人一眼就能看懂,再也不用担心什么sleep传入的int参数到底是毫秒还是秒的问题。

相似的,由于一切皆对象,可以轻松的这样把一个对象转换为JSON:

require "json"

puts (
{
a: 1,
b: 2,
c: {
a: 2,
b: [1,2,3,4, {
str: "hello"
}]
}
}.to_json
)

# {"a":1,"b":2,"c":{"a":2,"b":[1,2,3,4,{"str":"hello"}]}}

缺点

Crystal目前的生态还有问题,vscode插件甚至无法做到优秀的代码补全和类型检查。好在编译时Crystal会报告你的类型错误,呃()

速度测试

测试代码:欧拉筛素数,数量级1e8

def get_primes(n)
isnt_prime = Array.new(n + 1, false)
res = [] of Int32
isnt_prime.each_index do |val|
next if val < 2
next if isnt_prime[val] == true
(val * 2..n).step val do |id|
isnt_prime[id] = true
end
res << val
end
res
end

puts "Start calculating..."
t1 = Time.monotonic
resu = get_primes(100000000)
t2 = Time.monotonic
puts resu[..100]
puts t2 - t1
$ crystal build .\prime.cr --release
$ .\prime.exe
Start calculating...
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547]
00:00:01.156142200

相同框架改写的代码,Crystal 用时1.2秒,C++用时1.2秒,nodejs用时7.9秒,ruby用时16.5秒,python用时22秒

Crystal 在这个素数筛上还是非常接近 C++ 的速度的