Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Crystal言語作者がRubyを愛する理由(2)文字列の表現(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

crystal-lang/crystal - GitHub

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_sinspectが両方あるのがよいのです。

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を探すことになるでしょう。

次回予告

次回のお題は、「独自の言語を作る」です↓。

Crystal言語作者がRubyを愛する理由(3)ほぼすべてがメソッド呼び出し(翻訳)

関連記事

Crystal言語作者がRubyを愛する理由(1)「等しさ」の扱い(翻訳)


CONTACT

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