Crystal言語作者がRubyを愛する理由(2)文字列の表現(翻訳)
文字列の表現
Rubyのあらゆるオブジェクトはto_s
に応答します。これはデフォルトの振る舞いとしてよくできています。inspect
を使えば、たいていのオブジェクトの内部構造も調べられます。
例:
class Point
def initialize(x, y)
@x = x
@y = y
end
end
point = Point.new(1, 2)
point.to_s # => #<Point:0x00007fab148d55d8>
point.inspect # => #<Point:0x00007fab148d55d8 @x=1, @y=2>
これならto_s
よりinspect
の方がずっと便利そうですが、to_s
とinspect
が両方あるのがよいのです。
inspect
のよい点は、参照が循環する可能性にも配慮されていることです。
class Person
def initialize(name)
@name = name
@sibilings = []
end
def add_sibiling(person)
@sibilings << person
end
end
ary = Person.new("Ary")
gabriel = Person.new("Gabriel")
ary.add_sibiling(gabriel)
gabriel.add_sibiling(ary)
ary.inspect # => #<Person:0x00007fe670851160 @name="Ary", @sibilings=[#<Person:0x00007fe6708510c0 @name="Gabriel", @sibilings=[#<Person:0x00007fe670851160 ...>]>]>
gabriel # => #<Person:0x00007fe6708510c0 @name="Gabriel", @sibilings=[#<Person:0x00007fe670851160 @name="Ary", @sibilings=[#<Person:0x00007fe6708510c0 ...>]>]>
inspect
は単にクラッシュしないというだけではなく、オブジェクトidが表示されるので、どのオブジェクトがいつ循環するかを調べられます。
Crystalでも同様のコードが動きます。
class Person
def initialize(@name : String)
@sibilings = [] of Person
end
def add_sibiling(person)
@sibilings << person
end
end
ary = Person.new("Ary")
gabriel = Person.new("Gabriel")
ary.add_sibiling(gabriel)
gabriel.add_sibiling(ary)
ary.inspect # => #<Person:0x107d3aea0 @name="Ary", @sibilings=[#<Person:0x107d3ae60 @name="Gabriel", @sibilings=[#<Person:0x107d3aea0 ...>]>]>
gabriel.inspect # => #<Person:0x107d3ae60 @name="Gabriel", @sibilings=[#<Person:0x107d3aea0 @name="Ary", @sibilings=[#<Person:0x107d3ae60 ...>]>]>
この出力はちょっと読みにくいですね。RubyにもCrystalにも、あらゆる型のオブジェクトを整形して手軽に出力できるpretty print機能があります。Rubyの場合は以下のようにできます。
require "pp"
pp ary
出力:
#<Person:0x00007ffad4076fd0
@name="Ary",
@sibilings=
[#<Person:0x00007ffad404b8a8
@name="Gabriel",
@sibilings=[#<Person:0x00007ffad4076fd0 ...>]>]>
Crystalの場合は以下です。
pp ary
出力:
#<Person:0x101418ea0
@name="Ary",
@sibilings=
[#<Person:0x101418e60
@name="Gabriel",
@sibilings=[#<Person:0x101418ea0 ...>]>]>
他の言語では、このようにオブジェクトをその場で手軽に調べる機能が存在しなかったり、あっても使いにくかったりします。あらゆるオブジェクトを文字列に変換できる言語もありますが(出力の整形機能の使いやすさはともかく)、そうした機能がない言語もあります。ここでは(おそらく自分が業務で使っていることもあって)Haskellを例に挙げますが、Rustもそうだと思います。
Haskellは、あらゆる値をデフォルトで文字列に変換できるわけではありません。そのためにはShow
型クラスを実装する必要があります。実際には、上述のPoint
型は以下のようになります。
❯ ghci
GHCi, version 8.10.7: https://www.haskell.org/ghc/ :? for help
Prelude> data Point = Point { x :: Int, y :: Int }
Prelude> p1 = Point { x = 1, y = 2 }
Prelude> p1
<interactive>:8:1: error:
- No instance for (Show Point) arising from a use of 'print'
- In a stmt of an interactive GHCi command: print it
pi
の内容を表示できないことがわかります。ここでも、宣言の末尾にderiving (Show)
を追加すればShow
型のshow
関数をカスタム定義できます。
この程度なら大した手間ではなさそうにも思えます。しかしコードのデバッグ中にオブジェクトをその場で調べられないと、作業を中断してあちこちにderiving (Show)
を書き足さないと何が起こっているのかがわからないということです。これは楽しくありません。さらに、その型が自分の手出ししにくい場所にあれば、Show
を追加するだけでも大変になり、つらさが増してしまいます。そして作業が終わったら終わったで、追加したderiving (Show)
を今後のために残すべきか、それとも削除すべきかをいちいち考えなければなりません。そして私は「これらをデフォルトでderiving
しないのはなぜなのか」と疑問に思うわけです。
私はこんなとき、Rubyが「開発者の幸せ」を目標に置いていることを思い出します。上のような作業はどれひとつ取っても楽しくありません。Rubyではそんな苦労は不要です。そうしたことはRubyが面倒を見てくれるので、私たちは問題を解くという楽しみにもっと集中できるようになります。
しかしHaskellでもderiving (Show)
をいったん追加すれば同じことができますし、標準ライブラリの型についてはshow
の実装は一般に非常によくできています。
❯ ghci
GHCi, version 8.10.7: https://www.haskell.org/ghc/ :? for help
Prelude> data Point = Point { x :: Int, y :: Int } deriving (Show)
Prelude> p1 = Point { x = 1, y = 2 }
Prelude> p1
Point {x = 1, y = 2}
Prelude> [p1]
[Point {x = 1, y = 2}]
Prelude> ["hello", "world"]
["hello","world"]
別の言語の例: Go
別の言語も見てみましょう。ここではランダムにGoを選びました。理由は、比較的モダンかつ人気も高く、すぐ試せるプレイグラウンドもあるからです。
以下は、Go's tour: Arraysから引用したコード例です。
package main
import "fmt"
func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)
primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)
}
出力:
Hello World
[Hello World]
[2 3 5 7 11 13]
それではRubyやCrystalと比較してみましょう。以下で使っているp
は、オブジェクトでinspect
を呼び出します。
a = ["Hello", "World"]
p a[0], a[1]
p a
primes = [2, 3, 5, 7, 11, 13]
p primes
出力:
"Hello"
"World"
["Hello", "World"]
[2, 3, 5, 7, 11, 13]
最初に気づくのは、RubyやCrystalは文字列の周りを引用符で囲んでいることです(ただしこれはinspect
の場合だけです: 文字列でto_s
を呼び出した場合はそうなりません)。次に気づくのは、配列の内容にinspect
が使われることです。これにより、最初の配列に文字列が2つあることがわかります。Goの場合は、"Hello"と"World"という2つの文字列があるのか、"Hello World"という1つの文字列があるのかがはっきりしません。
RubyやCrystalでもうひとつ嬉しい点は、配列の出力をコードに貼り付けるとそのまま動くことです。いつもそうできるとは限りませんが、配列やハッシュのように多用されるプリミティブ型では非常にうまく行きます。Goの場合はそうではありません。理由はわかりませんが、カンマすら出力されません。
Goで配列や文字列をもっとよい感じに出力する方法はきっとあるはずです。たとえばJavaならArrays.toString
が使えますが、この関数はデフォルトでは追加されないのですぐには使えず、直感に反しています。
次は一貫性と統一性です。GoにはStringerというインターフェイスがあり、オブジェクトを文字列に変換するString()
関数が定義されています。では配列でString()
を呼び出すとどうなるでしょうか?
package main
import "fmt"
func main() {
a := [2]string{"Hello", "World"}
fmt.Println(a.String())
}
コンパイルエラーになりました。
./prog.go:7:15: a.String undefined (type [2]string has no field or method String)
つまり、配列を文字列に変換する方法を知っているのはfmt
だけであり、配列についてはfmt
で、それ以外の型についてはString()
に頼ることになるようです。
こういったことはRubyではまず起きないので、心から感謝しています。Rubyで何かがある形で動作したときに「同じことが他の型やコンテキストでもできるだろう」と類推すると、ほぼ常にそのとおりになります。こういう類推が利かない言語では、こういうときに早速頭の中で例外のコレクションを組み立てつつ、Stack Overflowを探すことになるでしょう。
次回予告
次回のお題は、「独自の言語を作る」です↓。
概要
原著者の許諾を得て翻訳・公開いたします。