《七周七语言》笔记——Ruby

不会有第二篇了,认真去学 Scala,将来若有需要可能回去碰碰 Rust 和 Scheme/Racket。

—— 2021-12-27

开始认真看《七周七语言》这书,主要目的是了解下各个编程范式在实践上的一些具体差别,同时了解一下各种语言的骚操作,如 Ruby 的模板元编程,scala 的 Actor,erlang 的……快速失败?Io 的基于原型的面向对象编程……总之按照书中的顺序挨个 peek 一下,首先是 Ruby。

考虑了很久,决定这书先放下,先学一门灵活又强大,既能用于实践也能用于各种形式抽象的语言,并对其进行深入学习,达到和 Java 一样的上手程度,能随手写出实例,也能拿来进行工程实践。

Haskell 先 pass,因为它难以用于实践;Kotlin 很有趣,let,apply 等方法精妙绝伦,但又略有工业气息(比如?语法糖,我还是希望它提供一个真正的 Optional),且进行函数式编程并不方便(写个柯里化的函数试试?);最终我还是选择 Scala,它的抽象能力足以模拟 Haskell 中各种骚操作,兼具面向对象和函数式编程特性让它在这两方面都可堪用,Actor 的并发模型也是我想关注的对象之一,而且在将来甚至还有实践意义,所以选择 Scala 是完全合理的。

第一天

我讨厌动态类型语言,讨厌鸭子类型(就如讨厌实用主义),因此对 Ruby 自然也没啥兴趣,但它的语法显然受 Perl 之类的影响很多(而函数命名则受到了 Scheme 的影响,从谓词后面带着的问号就能看出来),优美流畅如同自然语言,特别是单行的 if/unless,以及 while/until。其中 unless 等价于 if not。我始终想不到 unless 翻译成中文该怎么说,“若非”有点怪。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
answer = 42 # 变量无需声明
puts "correct!" if answer == 42 # Ruby 里,一切语句都有返回值,我喜欢这一点
# 等价于
puts "correct!" unless answer != 42
# 等价于
answer == 42 && puts "correct!" # 类似 js 的用法
# 一切操作符都是对象的方法,如 answer == 42 本质是 answer.== 42,这点同 scala(但是我不太喜欢这种形式,更喜欢 typeclass 那种,更像是定义了两个对象的“关系”)

i = 0
(puts i; i = i + 1) until i == 10
# 等价于
until i == 10
puts i
i = i + 1
end
# 等价于
(puts i; i = i + 1) while i < 10

必须承认,这单行的循环还是有点麻烦的……正经人谁写循环啊。

然后,这是基本 for 循环——

1
2
3
for i in 1..5  # (1..5).class == Range
puts i
end

这是 forEach 形式,语法非常像 Kotlin 的尾 lambda 形式——

1
2
3
4
5
(1..5).each do |elem|
puts elem
end

(1..5).each { |elem| puts elem } # 这就完全一样了

足够做第一天的题目了,开始吧。

  • 打印字符串”Hello, world.”。
1
puts "Hello, world."
  • 在字符串”Hello, Ruby.”中,找出”Ruby.”所在下标。

这题首先想到的就是使用 find 或 indexOf 之类的方法,借助 tab 真让我补全到了——

1
puts 'Hello, Ruby.'.index /Ruby\./ # 输出 7,返回 nil
  • 打印你的名字十遍。
1
10.times {puts '✯_G∀ZER'}
  • 打印字符串”This is sentence number 1.”,其中的数字 1 会一直变化到 10。

利用字符串格式化。

1
2
3
(1..10).each do |i|
puts "This is sentence number #{i}." # 需要格式化字符串时,使用双引号,使用#{}引用变量
end
  • 从文件运行 Ruby 程序。
1
2
3
4
5
6
# hello.rb
puts "hello, world"

# shell
$ ruby hello.rb
hello, world
  • 加分题:如果你感觉意犹未尽,还可以写一个选随机数的程序。该程序让玩家猜随机数是多少,并告诉玩家是猜大了还是猜小了。

这个语言的函数调用非常有趣,当把函数名写出来的时候,就是对函数的调用了。也就是说对于一个() -> a的函数,其是直接进行了函数的调用,而非对函数本身求值。这在其他语言里是见不到的,Haskell 的 IO Monad 倒是在这上面和它现象一致,但本质肯定是不同的,在赋值时就能看出来了。如果想要在下面的 Haskell 里达到和 Ruby 一样的效果,需要a = unsafePerformIO randomIO才行。下面的 a 是求值 randomIO 的结果,而非是 perform randomIO 的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Prelude> import System.Random
Prelude System.Random> randomIO -- randomIO :: IO Int
Prelude System.Random> randomIO
6879460589617617602
Prelude System.Random> randomIO
-2890844218392729664
Prelude System.Random> randomIO
-7445306786419155406
Prelude System.Random> a = randomIO
Prelude System.Random> a
-9077188816176212561
Prelude System.Random> a
8564549844481012240
Prelude System.Random> a
-6436459373760545118

咳咳,总之专注题目。Haskell 的环境搭建太烦人了,依赖总是找不到,Stack 也总是配置不好,下次干脆 tm 用 purescript 算了。

这个问题需要将输入转换为整数,测试发现字符串的 to_i 方法能够进行转换。经过上面的描述,可以得到一个非常有趣(以及离谱)的解决方案——gets.to_i。这种形式简直离谱😂。

1
2
3
4
5
6
7
8
9
r = rand 100
puts '猜一个从 0 到 100 的数'
while true do
puts '输入:'
input = gets.to_i # Magic!
puts '小了!' if input < r
puts '大了!' if input > r
(puts 'you got it!'; return) if input == r # return,就如其他 c 系语言
end

我佛啦。

第二天

Ruby 的数组和 Python 的数组极为相似。形如1..10这种形式的对象为Range。数组可以通过 Range 来获得子数组。

1
2
fruits = ["Apple", "Banana", "Peach", "Lemon"]
puts fruits[0..1] # 前闭后闭

有趣(但也挺 trival)的地方是,[][]=(设置特定索引的值)也是数组的方法。

1
2
puts fruits.[] 0
fruits.[]= 0, "What the Fuck?" # 多个参数通过逗号分隔,不能使用括号

有趣的地方是函数的多个参数是通过逗号分隔的……这么说我们如果要强调一个函数优先调用,得这么干——(puts 1, 2, 3)而非puts (1, 2, 3)。突然又像 Lisp 了,好家伙。

举一反三,我们定义数组的时候,其实是调用了这样的函数——

1
fruits = Array.[] "Apple", "Banana", "Peach", "Lemon"

我实在不太喜欢这样……

哈希表的语法是这样的,好像和 Python 的类似。冒号前缀的数据类型称为符号,类似 Lisp 的 quote,或者其他 C 系语言的枚举。其通过 to_s 和 to_sym 方法可以和字符串互相转换,

1
2
3
map = {"answer" => 42, "value" => "hello", :symbol => "like Lisp!"}
map["answer"] # 42
map[:symbol] # "like Lisp!"

前一天中{}包围的代码称为代码块。代码块是 Ruby 中的匿名函数。下面的代码顾名思义——

1
3.times { puts "三回啊三回" }

do-end 结构和代码块是否是同一种东西?

我们可以通过在运行时给 Integer 添加新的方法以构造我们自己的 time 方法,通过类似 Python 的 yield。yield 大概使函数变成了迭代器。

1
2
3
4
5
6
7
8
9
class Integer
def my_times
i = self
while i > 0
i = i - 1
yield
end
end
end

yield 本身不算难理解,可是它生成的是谁?self?i?如果是 self,对 i 的修改直接改变了 self?我们做一些测试——

1
2
3
4
5
6
7
8
9
class Integer
def tst
i = self
i = i + 100
puts i == self
end
end

3.tst # false

看上去是后者,那 yield 是怎样“选中”i 的?最近一条表达式的返回值?哈人啊朋友!

Ruby 的带参数的函数定义同其他 C 系语言一致——

1
2
3
4
5
6
7
def something(n) # 有趣的事情是 def 语句本身返回和函数名相同的 symbol
n + 100
end

something 2 # 102

:something.to_proc.call nil, 100 # 通过这种形式能够将符号转换成原函数(它称为过程,又是从 Lisp 来的),至于调用时传递的 nil,怀疑是某种上下文,就像是 js 的 funcall,apply 之类

而关于代码块如何传递的问题,Ruby 对其的处理类似…找不到类似,总之很丑陋——给相应参数前加上&符号,再次传递时也是。

1
2
3
4
5
6
7
8
9
def twice(&block)
block.call
end

def proxy(&block)
twice(&block)
end

proxy {puts "wtf"}

有趣的是,根据错误提示,代码块好像不属于函数参数的一部分,只能说十分奇怪了。


把 Ruby 后面的内容粗略翻了一遍……老实说没有什么我感兴趣的东西,而且在 kotlin 和 scala 里我已见过更漂亮的语法了,跳过!模版元编程什么的等 Haskell 再去学吧。下一个是 Io,一门简单的原型编程语言。