2020-12-31

自分が思うRubyの「動的さ」が世間とずれているっぽいので書きとめておくメモ

何かに強く反論したいとかじゃなくて、自分で引用しやすいようにpermalinkを作っておく、くらいの意味。

Rubyは動的型であるという言い方

動的型というのは実行時にならないと型が決まらないという意味なので、それはそう。例えば Ruby には

int a;

のようなコードは存在しないので、

a = 1
a = '1'

も正しく動く。この時 a の型は実行時にしか決まらず、動的である。間違いない。

※ 1 や '1' は実行前に決まっているけど、こっちの話題は今回は取り上げない。

動的型言語に動的型変換の機能があるけど、Rubyは意外と厳格

1 + "2"を計算する

awk

$ awk 'BEGIN { print 1 + "2" }'
# -> 3

3 が返ってくる。これは数値の 1 に対して文字列の 2 が数値の 2 に動的に変換されて計算された結果である。

PHP では

$ php -B 'print 1 + "2";'
# -> 3

結果は同じ数値としての 3 になる。

JavaScript は

$ node -p '1 + "2"'
// -> 12

これは文字列として連結して 12 になっている。

Ruby

$ ruby -e 'p 1 + "2"'
Traceback (most recent call last):
        1: from -e:1:in `<main>'
-e:1:in `+': String can't be coerced into Integer (TypeError)

で実行できない。

これは Integer である 1 の + というメソッド(演算子のように見えるけど)が String である "2" を強制的に Integer にすることができず、Type が合わないという例外で死んでしまっている。

"1" + 2を計算する

awk では

$ awk 'BEGIN { print "1" + 2 }'
# -> 3

先ほどと同じ 3 になる。awk では + に文字列の連結としての機能は存在せず、常に数値で解釈される。

PHP では

$ php -B 'print "1" + 2;'
# -> 3

先ほどと同じ 3 になる。PHP も + は必ず数値の演算として解釈される。文字列の連結には . という専用の演算子がある。

JavaScript では

$ node -p '"1" + 2'
// -> 12

JavaScript では + は数値の演算にも文字列の演算にも利用できるが、混ざると文字列としての演算が優先される設計になっている。

Ruby では

$ ruby -e 'p "1" + 2'
Traceback (most recent call last):
        1: from -e:1:in `<main>'
-e:1:in `+': no implicit conversion of Integer into String (TypeError)

先ほどと同じように思いっきり TypeError となる。若干エラーメッセージは異なるが、String である "1" の + メソッドが 2 という Integer を強制的に String にできず Type が合わないという例外で死ぬ。

もちろん揃っていれば普通に動く。

$ ruby -e 'p "1" + "2"'
# -> "12"

実はRubyは型について厳しいのだが、静的に強制できないことが問題視されるようになった

Rubyの挙動を整理し直すと、

  • Ruby は文字列の連結も数値の加算も + を利用する
  • ただし + は演算子ではなくメソッドであり、メソッドは左辺に当たる値の class に定義されている
  • String#+ は Integer を受け取っても文字列の連結を行うことはできず、Integer#+ は String を受け取っても数値として加算することはできない

オブジェクト、メソッド、class で考えると至極自然な動作をしている。

このように Ruby は型については実は意外と厳しい。よく分からない挙動で悩まされることは少ない。少なくとも動的変換で変な踏み抜き方はしないので、そういう意味では「型に関する挙動ではだいぶ安全」である。もちろん明示的に変換を指示する際にヘマをすることはできる。そこはプログラマの自由だ。

ただし、標準の機能では変数への代入、関数の引数への割り当て、戻り値について型を強制することができないという問題はある。(動的に死ぬようなコードを作ることはできる。)

逆に、PHP や JavaScript はエラーが起きずに意図しない動作をすることが問題となり、開発規模の拡大とともに型を指定したいという要求が高まり、TypeScript や PHP 7 以降の型の扱いに結実した。結果、Ruby より静的に解釈しやすくなり「より安全に『開発できる』」と言われるようになった。

「動作としての安全性よりも人間の頑張りに期待する安全性の方がよりよいものとして評価されている」ような状況なので個人的にはあまり納得はいっていないのだけど、周辺のツールを含めて「結果としてどうなのか」だけに注目すると「実際に動作させる前に検知できる」のはバグの発見の早期化という意味でよいことだと思う。

これについての Ruby の 2020 年時点での回答は Ruby 3 なので、Ruby 3.0.0 Released など、関連情報を漁ってもらうとよいと思う。少なくとも TypeScript 的なアプローチで静的に問題を検知することはできるようになっている。

エコシステムとしては TypeScript ほどの充実はまだ実現できていないけれど、システム自体は Ruby 2.6 以降でも利用できるので、今すぐ実践投入することもできる。

Rubyの本当の動的さはそっちじゃない

上の例に挙げた String や Integer では不可能だが、PHP や JavaScript のようなふわふわした挙動の演算子のようなものを持った値も以下のように作ることができる。

class AmbigiousValue
  def initialize(val)
    ..
  end

  def +(other)
    ..
  end
end

さらに、上のように普通に class で定義したものについてはインスタンスの状態でメソッドを上書きできるので、あるオブジェクトだけ + の意味が違う、みたいなこともできる。

※ 残念ながら(?) Integer ( Numeric ) や String は継承ツリーに Class を持っておらず、Module に定義されている動的なメソッド定義を実現するメソッド群を持っていないので、Ruby の構文解析を維持したままいきなり 'a' + 1 が 'a1' になるようなコードを作ることはたぶんできない。

もちろんオブジェクト単位で挙動が異なるようなことは「できる」というだけで推奨してる人はたぶんいないけど、この点については自分は以下のようなことだと理解している。

午前4時。あと数時間でユーザが出勤してくる。それまでにシステムをまがりなりにも 動く状態にしておかねば… どうもサードパーティ製のあるライブラリ(ソース無し)の 挙動が怪しいのが問題の原因のようだが、APIにどういうパラメータを与えた時に バグが再現できるか絞りこめていない。もちろんサポートが開くのは明朝、それでは 間に合わないッ。だがッ! ライブラリの内部のみで使われる ある関数に、まれに異常な引数が渡っていることが分かったッ! この内部関数の呼び出しをフックして引数を修正すればとりあえず動かせるッ!—とか、

そういう状況において、「どんなに汚くても、打てる手段がある」というのは 何物にも替え難い救いなのです。というより、そういう予想外の事態に対して エスケープポッドが備えられていない処理系を使うなんて恐くて出来ません。 Lisperは臆病なんです。

Lisp:よくある正解

Ruby が安全なだけの言語でないのは間違いないと思う。

About

例によって個人のなんちゃらです