Ruby3を試そう

Ruby3 が公開になりました。
今回は、Docker で Ruby3 の実行環境を用意し、新しい機能を中心に触ってみます。

目次

参考

Ruby3.0 の導入

今回は Ruby3.0 を docker で導入してみます。

シンプルにインタプリタ(irb)

適当なディレクトリで Dockerfile を作成する。

Dockerfile
1
2
3
FROM ruby:3.0.0

WORKDIR /usr/src/app

以下のコマンドで実行する。

1
2
3
4
5
6
7
8
9
# 作成したDockerfileの定義に基づいてイメージを作成
docker build -t ruby3 .

# イメージからコンテナの実態を作成
docker run -it ruby3

irb(main):001:0> RUBY_VERSION
=> "3.0.0"
# 実行中のバージョンが3.0.0だとわかります

Ruby3.0 を実行できました。
コンソールで動かしたいだけであればこれだけで OK です。

続けて用意したスクリプトを実行させてみます。

スクリプトを実行

使う Dockerfile は前述のものと同じです。
スクリプト自体をコンテナに作るとコンテナを破棄した時に、せっかく作成したファイルを消失させてしまうので、ドライブのマウントをします。

mac(bash)
1
2
3
4
docker run -it --name ruby3 -v `pwd`:/usr/src/app ruby3 bash

# 2回目以降はこっち
docker start -ai ruby3

Powershell は、pwdコマンドの返すものが単純な文字列では無いので、結果を少し加工して使います。

windows(PowerShell)
1
2
3
4
docker run -it --name ruby3 -v "$($(pwd).Path):/usr/src/app" ruby3 bash

# 2回目以降はこっち
docker start -ai ruby3

コンテナ内の/usr/src/appがコンテナを起動したディレクトリになっています。
以下のファイルを現在のディレクトリにエディタなどを使って作成します。

test.rb
1
2
puts RUBY_VERSION
puts "TEST"

コンソールで確認すると、作成したファイルが見つかります。

1
2
3
4
5
6
7
# ls
test.rb

# スクリプトの実行
# ruby b.rb
3.0.0
TEST

コンテナ内の Ruby3.0 でコンテナ外で作成したスクリプトを実行できました。

本題 Ruby3.0 を試そう

Ruby3.0 には新たにいくつかの機能が追加されていますが、それらの中からいくつか試します。

一行メソッドが定義できるようになった

ただただ便利。

test1.rb
1
2
3
def plus(x, y) = x + y
puts plus(1, 2)
# => 3

欠点としては、一行メソッドの記述にシンタックスハイライト側が追いついていいので、色付けはおかしくなりました。そのうち改善するでしょう。

パターンマッチが再設計された

再設計といわれても使ったことが無かったので、初見。
結構深い。

test2.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{ a: 1, b: 2 } in { a:, b: }
# =>true

{ a: 1, b: 2 } in { a: }
# => true

{ a: 1, b: 2 } in { a:, b:, c: }
# => false

# オブジェクトの形だけ見て判断してくれる
# 不足はfalseだが、一致していなくても満たしていればtrue

{ a: 1, b: 2 } in { a: 1, b: 2}
# => true
{ a: 1, b: 2 } in { a: 2, b: 2}
# => false
# 同じ形状でも値の一致までチェックすることもできる

# パターンマッチ結果を変数に入れるには少し工夫が必要

result = { a: 1, b: 2 } in { a:, b:, c: }
# => false
result
# => {:a=>1, :b=>2}
# resultにパターンマッチの結果は入ってこない!
result = ({ a: 1, b: 2 } in { a:, b: })
# => true
result
# => true
# ()でくくるとパターンマッチ結果を変数に入れることができる

# 結果がfalseの時は
#パターンマッチの結果としてa,bには代入されないが、マッチできた部分だけresultでアクセスできる
result = { a: 1, b: 2 } in { a:, b:, c: }
a
# =>nil
b
# =>nil
result[:a]
# =>1
result[:b]
# =>2

右代入の導入

test3.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{ a: 1, b: 2 } => { a:, b:}
a
# => 1
b
# => 2

# メソッドチェーンの最後の代入に便利らしい。おそらくこんな感じ
def to_name_object( first,last ) = { f: first,l: last }
to_name_object("hoge","huga") => {f:,l:}
f
# => foge
l
# => huga

# またこういうこともできる
def to_name_object( first,last ) = { f: first,l: last }
to_name_object("hoge","huga") => name
name[:f]
# => foge
name[:l]
# => huga

find パターンが追加された

追加といっても使ったことがなかったので、こちらも初見。
でも、神。正規表現で頑張るとかしなくても今後は良くなる可能性を感じます。(繰り返し関係の条件は表現できそうにないですが。)
オブジェクトの中から何か拾う操作はこれで良さそう。

test4.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# 要素のクラスで判定
case ["a","a",1,"b"]
in [ *pre, String => x, Numeric => y, *post ]
p pre
p x
p y
p post
end
#["a"]
#"a"
#1
#["b"]

# 特定の値で拘束することできる
# ちなみに前方一致のようで後から出てきた一致するものには反応しない

case "aabbcabcdd".split(//,0)
in [ *pre, "a"=>x, "b"=>y, *post ]
p pre
p x
p y
p post
end
#["a"]
#"a"
#"b"
#["b", "c", "a", "b", "c", "d", "d"]

# caseの結果を変数に返すことができる
result = case "aabbcabc".split(//,0)
in [ *pre, "a"=>x, "b"=>y, *post ]
post.join()
end

p result
# =>bcabcdd

# マッチしないと警告(NoMatchingPatternError)を出すので、対応しておくのがよさそう
# "1" | "2" | "3" => y みたいに使用して or の表現もできる
result =
begin
case "aabbcaa31bcdd".split(//,0)
in [ *pre, "a" => x, "1" | "2" | "3" => y, *post ]
{ pre: pre, x: x, y: y, post: post }
end
rescue
nil
end

p result
#=> {:pre=>["a", "a", "b", "b", "c", "a"], :x=>"a", :y=>"3", :post=>["1", "b", "c", "d", "d"]}

# 文字列や数字でなくてもOK
# 前後を使用しない場合は*だけでOK
result =
begin
case {type: "fish", orders:[ { name: "maguro", count: 3}, { name: "hamachi", count: 4}, { name: "iwashi", count: 5}] }
in {type: "fish", orders:[ *, {name: "hamachi", count: count }, *]}
count
end
rescue
nil
end

p result
# =>4

並列実行 Ractor の導入

外部のライブラリ無く単独で並列実行機能があるのすごいよね。

動作確認

test5.rb
1
2
3
4
5
6
7
8
9
5.times.each do |t|
Ractor.new t do |n|
puts "count#{n}"
end
end

# どうもRactorの処理が終わる前に、親プロセスが終わると何も表示されないのでsleepを書いておく
sleep(1)
puts "END"

こちらの実行結果が以下の通り。
非同期に実行できているらしいことがわかる表示になります。

1
2
3
4
5
6
count0
count2count3

count1
count4
END

並列実行し結果を受け取る

test6.rb
1
2
3
4
5
6
7
# takeを使うと返り値を受け取れる
r = Ractor.new do
100
end

puts r.take
# =>100

並列実行し「まとめて」結果を受け取る

test7.rb
1
2
3
4
5
6
7
8
result = 10.times.map do |t|
Ractor.new t do |n|
puts "count#{n}"
n*n
end
end.map(&:take)

puts result.inspect

実行すると以下の表示になる。

1
2
3
4
5
6
7
8
9
10
11
count0
count2count1

count3
count5
count4
count7
count8
count6count9

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

並行実行して、結果をまとめて配列で受け取ることができました。

速度比較してみる

リリースノートも紹介されている速度比較をしてみます。

test8.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
require 'benchmark'

Benchmark.bm do |bench|
bench.report 'seq' do
result = 6.times.map do |t|
tmp = 1
(1..1000000).each do |t1|
tmp += t1
end
tmp
end
end

bench.report 'par' do
result = 6.times.map do |t|
Ractor.new do
tmp = 1
(1..1000000).each do |t1|
tmp += t1
end
tmp
end
end.map(&:take)
end
end

計測結果はこちらの通り。

1
2
3
       user     system      total        real
seq 29.499186 0.010006 29.509192 ( 29.509665)
par 29.841942 0.000000 29.841942 ( 5.103909)

Core i5 9400F 6 コア での実行結果です。
大体 1/6 に収まっているので確かに高速化に寄与していると感じます。


今回は、Ruby3 を触ってみました。
1 行メソッド、find パターン、Ractor は、素晴らしいものだと感じます。
特に Ractor は、並列実行で何をさせるかという点で想像を掻き立てます。
クローラなどの複数 URL へのアクセスや、ダウンロードに使うと高速化が図れそうです。

ではでは。