Tech Racho エンジニアの「?」を「!」に。
  • 開発

Goby: Rubyライクな言語(4)定数への再代入を禁止する仕様

こんにちは、hachi8833です。

本記事ではGoby 0.1.9を用いています。1.0になるまでは仕様変更の可能性もありますのでご了承ください。

インストール方法や動かし方については第1回目をご覧ください。

Goby: Rubyライクな言語(1)Gobyを動かしてみる

: GobyとRubyのどちらでも動くコードの冒頭には# Goby/Rubyと、Gobyでのみ動くコードには# Gobyと表記することにします。

Gobyの文法上の特徴(1)定数は=で再代入できない

Gobyの定数(大文字で始まる識別子)は、=による再代入を禁止する仕様を採用しています。文字列リテラルのほか、数値リテラルも定数に再代入できません。わずかな違いですが、より定数という言葉に沿った仕様になっています。

# Goby
Foo = "bar"
Foo = "baz"  #» ConstantAlreadyInitializedError: Constant Foo already been initialized. Can't assign value to a constant twice.

Port = 3000
Port = 4000  #» ConstantAlreadyInitializedError: Constant Bar already been initialized. Can't assign value to a constant twice.

Rubyと同様、定数はクラス名やモジュール名にも使われますので、一見クラスやモジュールの再定義やリロードができないように見えますが、そこは大丈夫です。

Gobyの定数で禁止されているのは再代入とメソッド名への使用(後述)であり、クラスやモジュールの再定義(モンキーパッチ)は許されています。言い換えれば、Gobyのクラスやモジュールは定数への代入によらない方法で定義されます。Rubyのクラス定義/モジュール定義は定数への代入という形で行っているので、その点が異なります。

# Goby/Ruby
class Foo
  def bar
    42
  end
end

Foo.new.bar  #» 42

class Foo # 再定義が可能
  def bar  
    1941
  end
end

Foo.new.bar  #» 1941

ここから想像がつくと思いますが、値を代入された定数をクラスやモジュールとして再定義することも、逆にクラス名やモジュール名に値を代入することもできません。

# Goby
Foo = 1

class Foo  # 値代入済みの定数名をクラスとして再定義するのはダメ
  def bar
    42
  end
end

Foo.new.bar
#» UnsupportedMethodError: Unsupported Method #new for 1
# Goby
class Foo
  def bar
    42
  end
end

Foo = 1  # 定義済みのクラス名/モジュール名に値を代入するのもダメ
#» ConstantAlreadyInitializedError: Constant Foo already been initialized. Can't assign value to a constant twice.

クラスやモジュールが再定義可能なので、定数が再代入禁止でもライブラリのリロードは原理的に可能なはずです。ただしRubyの#loadに相当するメソッドがまだありませんが💦。

そこで、Gobyの標準ライブラリであるDB#openを使って、定義済みのクラスやモジュールをわざと後からrequireで再定義してみました。

# Goby
class DB
  def self.open
    42
  end
end

DB.open     
#» 42

require 'db'

DB.open("postgres", "user=postgres sslmode=disable")
#» <Instance of: DB>

動いてますね😊。

人によって感想は違うかもしれませんが、Gobyは「驚き最小の原則」に沿って機能を絞り込むことを目指していると私は思っています。

Gobyの文法上の特徴(2)定数はメソッド名に使えない

(1)から想像できるように、英大文字で始まるメソッド名も禁止されています。

# Goby
class Foo
  def Bar     # この時点で「Invalid method name: Bar. Line: 1」が表示される
    42
  end
end

以前のGobyでは、メソッド呼び出しの時点で大文字メソッド名を禁止していたのですが、現在はメソッド定義の時点で禁止されています。

Gobyの文法上の特徴(3)定数のその他の特徴はRubyと同じ

Gobyの定数のその他の特徴はRubyと基本的に同じです(もしかすると他にも気づいていない違いがあるかもしれませんが)。

  • Rubyと同様、定数に含まれるArrayやHashなどの要素は=などでアクセス/変更可能です。
# Goby/Ruby
Ary = [1,2,3,4,5]
Ary[3] = 99              # 定数内のArray要素に代入できる
Ary                      
#» [1, 2, 3, 99, 5]

Hsh = { first: 1, second: 2, third: 3}
Hsh[:first] = 100        # 定数内のHash要素に代入できる
#» { first: 100, second: 2, third: 3 }
  • Rubyと同様、Gobyのモジュール定義やクラス定義内で(メソッドの外に)定数を定義するとそのモジュールやクラスのスコープで定数が有効になり、::による名前空間も使えます。
# Goby/Ruby
module Boo
  AZ = "az"
  def bez
    puts AZ
  end
end

class Boz
  include Boo
end

Boz.new.bez     #» az
Boo::AZ         #» az
Boz::Boo::AZ    #» az

定数をトップレベルで定義するとグローバルスコープになるのもRubyと同じです。


Gobyで定数の再代入を明示的に禁止しても、Rubyとの挙動の違いを最小限にとどめながら、このように一貫して機能できることがわかったのは私にとって収穫でした。
ついでに知ったのですが、Pythonには定数という概念自体がないそうです。

Gobyの作者st0012さんに「いっそPythonみたいに定数そのものをなしにする考えはありますか?」とSlackのDMで尋ねたところ、「それはしない: 定数はメソッド探索が発生しないで済むというメリットがあるので」とのことでした。

参考: Rubyの定数をこの際がっつり理解する

公式ドキュメント↓に記載されているように、Rubyでは定数にクラスオブジェクトを代入する形でクラスを定義するので、定数が再代入可能であることがそもそも前提になります。こちらの方が自由度が高いのは確かですね。

またクラス定義式はクラスオブジェクトの生成を行うと同時に、 名前がクラス名である定数にクラスオブジェクトを代入する動作をします。
https://docs.ruby-lang.org/ja/latest/doc/spec=2fvariables.html#constより

なお、Rubyの大昔のMLを見ると、Rubyの定数を「定数」と呼ばずに別の名前を付ける構想があったようですが、ご存知のように定数という呼び名のまま変わっていません。

  • Rubyの定数は、以下のように定数に再代入すると警告が表示されますが、再代入は可能です。
# Ruby
[1] pry(main)> Foo = "bar"
=> "bar"
[2] pry(main)> Foo = "baz"
(pry):2: warning: already initialized constant Foo
(pry):1: warning: previous definition of Foo was here
=> "baz"
[3] pry(main)>
  • Rubyでは、メソッド名に定数を使うことが一応可能です(推奨はされません)。

なお特殊な例として、RubyのKernelモジュールには、#Integerなどのように大文字で始まるものがあります。

# Ruby
[8] pry(main)> class Foo
[8] pry(main)*   def Bar
[8] pry(main)*     42
[8] pry(main)*   end
[8] pry(main)* end
=> :Bar
[9] pry(main)> Foo.new.Bar
=> 42

ただしトップレベルで定義された定数名メソッドを呼び出すときは、()を省略できません(以前はトップレベル以外でも必須だったようです)。

# Ruby
[1] pry(main)> def Foo
[1] pry(main)*   42
[1] pry(main)* end
=> :Foo
[2] pry(main)> Foo
NameError: uninitialized constant Foo
from (pry):4:in `__pry__'
[3] pry(main)> Foo()
=> 42
  • Rubyは、定数や変数に代入した文字列リテラルが#freezeされていても、後から別の文字列リテラルを代入できます。#freezeは文字列リテラルや変数内の文字列の破壊的変更を禁止するものであり、代入を禁止するものではありません。
# Ruby
Str = "bbbb".freeze
Str = "cccc"         # freeze後でも、freezeしていない文字列リテラルを代入できる
puts Str.upcase!     #=> "CCCC" (エラーにならない)
Str = "bbbb"
Str.freeze           # 変数をfreezeしても同じく代入可能
Str = "cccc"        

Str = "dddd".freeze
puts Str.upcase!     # can't modify frozen String (RuntimeError)
  • Ruby 2.3から導入された# frozen_string_literal: trueマジックコメントをファイルに書くと、そのファイルで文字列リテラルが#freezeされます(Ruby 3.0ではマジックコメントなしでデフォルトになる予定です)。

「Rubyの定数は定数ではない」とは

上述のMLにこんなことも書いてあります↓。

「定数は定数でない」ことに同意します。1.5系ではすでにwarningも出ません。

私のような古い人間は、定数というとC言語のマクロとしてコンパイル時に機械的にその値に置き換えられるもの、というふうに思い込みがちでしたので、「Rubyの定数が再代入可能」という従来の説明で割りと戸惑いました。

上の言葉はおそらくその辺が含意されているはずなので、略さずに書けば「Rubyの定数は、C言語などで言うところのマクロ的な定数とは異なるものである」となりますでしょうか。その意味ではGobyの定数もC言語などの定数とは異なりますが、少なくともconstantという語義からはかけ離れていないと思います。

Gobyを知ったことでようやっと、Rubyの定数がすべて腑に落ちました。そもそもRubyを知るためにGobyをやり始めたのでした。

最近のGoby

今年頭ぐらいにst0012さんから、いつの間にか私がコミット数No.2コントリビュータになっているとお知らせをいただきました。何というかびっくりです😲。

関連記事

Goby: Rubyライクな言語(3)Go言語の`defer`を減らしたら10%以上高速化した話など

Goby: Rubyライクな言語(2)Goby言語の全貌を一発で理解できる解説スライドを公開しました!

Goby: Rubyライクな言語(1)Gobyを動かしてみる


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。