← melang

Ruby

melangの各noteで他言語と比較した、Rubyの設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。

型

ダックタイピングオブジェクトの型そのものではなく、そのオブジェクトが実際に持つメソッドやプロパティ(振る舞い)で扱いを決める考え方。「アヒルのように鳴き、アヒルのように歩くならアヒルとみなす」という比喩に由来し、動的型付け言語でよく採用される。が基本。

age = 20
age = 20
age = "Alice"  # 再代入で型が変わる
age + 10       # 実行して初めて TypeError になる

変数に型注釈を書く構文はなく、「そのメソッドが呼べるかどうか」で型を扱うダックタイピングオブジェクトの型そのものではなく、そのオブジェクトが実際に持つメソッドやプロパティ(振る舞い)で扱いを決める考え方。「アヒルのように鳴き、アヒルのように歩くならアヒルとみなす」という比喩に由来し、動的型付け言語でよく採用される。が基本。RBSやSorbetのような外部ツールで型を後付けすることもできるが、言語コア自体には型注釈がない。

型注釈の位置

言語コアに型注釈の構文がなく、型はRBSという別ファイルに分離して書く。

age = 20
age = 20
age = "Alice" # 再代入で型が変わる

def greet(name)  # 引数・戻り値に型注釈を書く構文がない
  "Hello, #{name}"
end

変数・引数・戻り値のいずれにも型注釈の構文がなく、型を付けたい場合はRBSという`.rbs`拡張子の別ファイルにシグネチャだけを書く。OCamlの`.mli`と似た「実装と型定義をファイルごと分離する」発想だが、Rubyでは型注釈が言語コアに一切存在しない点がOCamlとは異なる。

型変換

変換をメソッドで書き、厳密な変換は別の関数で分ける。

n = s.to_i
"42".to_i     # 42
"abc".to_i    # 0(失敗を隠す)
"12abc".to_i  # 12

Integer("abc")                    # ArgumentError
Integer("abc", exception: false)  # nil
42.to_s       # "42"

`to_i`, `to_s`, `to_f`のメソッド形式で変換するが、`to_i`は緩く、失敗しても`0`を返す。厳密に変換したい場合は`Integer()`を使い、失敗は`ArgumentError`になる。`exception: false`を渡すと`nil`を返す。

暗黙の型変換

数値型同士は暗黙変換されるが、文字列との混在はエラーになる。

x = 10
x = 10
y = 1.5
x + y   # 2.5(IntegerがFloatへ暗黙変換される)
x + "5" # TypeError(数値と文字列の暗黙変換はない)

Integer/Floatのような数値型同士の演算では暗黙に型を揃えるが、数値と文字列のような異なる種類の型を混ぜるとTypeErrorになる。ユーザー定義クラスでも`coerce`メソッドを実装すれば数値との暗黙変換に参加できる。

Enum / Sum Type

組み込みのenumがなく、symbolや定数で代替する。

STATUSES = %i[pending running completed]
status = :pending

case status
when :pending
  # ...
when :running
  # ...
end # :completedを書き忘れても実行は通る

組み込みのenumがなく、symbolや定数で列挙値を代替するのが慣用。網羅性を検査する仕組みはなく、書き忘れは実行時にしか気づけない。

値の不変性

オブジェクトは通常変更できる。`!`付きの破壊的メソッドも一般的。

obj.freeze / Data.define(:name)
User = Data.define(:name)
a = User.new(name: "alice")
b = a.with(name: "bob")     # 新しい値を生成
# a.instance_variable_set(:@name, "x")  # FrozenError

s = "abc"
s << "d"                    # OK: 文字列も変更できる
list = [3, 1, 2]
list.sort!                  # OK: 破壊的メソッド

NAME = "x"
NAME = "y"                  # 警告のみで、再代入できてしまう

文字列・配列・ハッシュなど、オブジェクトは通常変更でき、`sort!`や`<<`などの破壊的メソッドが一般的に使われる。`freeze`は浅く、定数への再代入も警告で済む。3.2の`Data.define`は、不変な値オブジェクトを作る手段である。

名前付き引数

キーワード引数として言語機能に組み込まれ、必須・デフォルト値ありを定義側で区別する。

def foo(width:, height: 0)
def foo(width:, height: 0)
end
foo(width: 10, height: 20) # widthは必須のキーワード引数
foo(width: 10)              # heightは省略可能(デフォルト値)

def bar(a, b, width:, height: 0)
end
bar(1, 2, width: 10) # 位置引数とキーワード引数を併用

Ruby 2.0でキーワード引数として導入。定義側で`width:`のように書くと必須、`height: 0`のようにデフォルト値を書くと省略可能になり、位置引数と明確に区別される。

関数の引数

オブジェクトへの参照が渡され、破壊的メソッドは呼び出し側に影響する。

def f(n, s)
def f(n, s)
  n += 1      # 再代入は呼び出し側に影響しない
  s << "a"    # 破壊的メソッドは呼び出し側から見える
end

str = +"x"
f(0, str)
puts str # "xa"

Pythonと同様に、オブジェクトへの参照が渡される。再代入は呼び出し側に影響しないが、破壊的メソッド(`<<`, `!`付きのメソッドなど)は影響する。`freeze`で不変にできるが、渡し方としては指定できない。

if 文/式

`if`は式。`else`を省略すると`nil`を返し、後置`if`も書ける。

x = if cond then a else b end
x = if cond
      1
    else
      2
    end

# 後置if
process if cond

`if`は式で、`else`を省略すると条件が偽のときに`nil`が返る。`unless`や後置`if`など、条件を書く方法が多い。

I/O の明示

動的型付けで、I/Oを示す仕組みもない。

def find_user(id)
def find_user(id)
  DB.find(id)
end

def get_user(id)
  find_user(id) # 呼べる
end

RBSやSorbetで型を後付けできるが、いずれもeffectの分類までは扱わない。

言語名

前身言語Perlの宝石名にちなんだ命名(誕生月の誕生石)。

開発者まつもとゆきひろ氏が、同僚の誕生石(ルビー)にちなみ、当時人気だったPerl(真珠)より価値のある宝石名を選んだという逸話が知られる。一般名詞(宝石名)だが、プログラミング言語としての用法とは文脈が分かれるため実務上の衝突は起きにくい。