💎

Rubyでハローワールドする30の方法を全解説する

2023/02/25に公開

前説

この記事はRuby30イベントで私が発表した「Rubyでハローワールドする30の方法」の解説記事です。LTは5分しかなかったので説明がほとんどできなかったため、その補足として書かれました。お楽しみください。

蛇足

この記事はスクリプトによって生成されました。全貌はレポジトリからご覧になれます。

本編

スライドのURLは以下。
https://speakerdeck.com/okuramasafumi/30-ways-for-hello-world-in-ruby

RubyでHello worldする方法その1

# 一番単純
puts "Hello, world!"

解説不要。


RubyでHello worldする方法その2

# 文字列埋め込み
hello = "Hello"
world = 'world!'
puts "#{hello}, #{world}"

文字列の埋め込みは頻出。


RubyでHello worldする方法その3

# フォーマット文字列、`ord`で得られた数字を使う
printf "%s, %c%c%c%c%c%c%c", "Hello", 119, 111, 114, 108, 100, 33, 10

フォーマット文字列はあまり使ったことがない。%cの裏で動いているのはchrメソッドっぽい。10は改行コード、見た目からは全くわからない。


RubyでHello worldする方法その4

# 標準出力($>)に文字列を追加する
$><<"Hello, world!\n"

標準出力の$>表記はつい最近知った。それに<<で文字列を追加するとputsのようになる。ゴルファー御用達。


RubyでHello worldする方法その5

# 正規表現でキャプチャして連結
str = "Hello, ruby30 world!"
md = str.match /^(\w+).+(\w{5,}!)/
puts md.captures.join(', ')

正規表現の右側のキャプチャ部分(world部分)に注目すると、ここで文字数を明示的に5文字以上にしないとうまくいかなかった。


RubyでHello worldする方法その6

# シンボルのリテラルにもいくつか書き方がある
print %s|Hello, world|
puts :!

シンボルリテラルをこんな書き方したことない。:!が使えるのも知らなかった。


RubyでHello worldする方法その7

# Rubyには「文字」リテラルがあるのでそれらを結合する
puts ?H << ?e << ?l << ?l << ?o << ?, << ?\u0020 << ?w << ?o << ?r << ?l << ?d << ?!

パーセントリテラルで空白文字を表現するのは不可能だと思い込んでいたけど、ユニコード文字列っぽいやつが使えた、便利。


RubyでHello worldする方法その8

# ヒアドキュメントで縦書きを実現
puts <<"HELLOWORLD".chomp.gsub(/([a-zA-Z, ])?\n/) {|match| match[0]}
H
e
l
l
o
,
\x20
w
o
r
l
d
!
HELLOWORLD

日本人だから縦書きをしたい。ヒアドキュメントの文字列にメソッドを呼ぶ場合は最初の行に書く。gsubにブロックを渡すのは普通に便利。


RubyでHello worldする方法その9

# 文字列の生成方法は色々ある
puts %q(Hello) + String.new(', ') + String(:world!)

文字列リテラルの書き方はたくさんある。String.newはここで人生初の利用。Stringメソッドもめったに使わない。


RubyでHello worldする方法その10

# "Hello, world!".each_codepoint.map{|n| n.to_s(16) }
# で得られたコードポイントの配列を文字列に戻す
["48", "65", "6c", "6c", "6f", "2c", "20", "77", "6f", "72", "6c", "64", "21", "a"].each do |codepoint|
  print codepoint.to_i(16).chr
end

また別の文字の表現方法。最後の"a"は改行なのでprintで良い。


RubyでHello worldする方法その11

# evalするだけだが、よく見るとネストしている
eval("eval %q(puts 'Hello, world!')")

evalだけだとありがちなのでネストさせてみた。


RubyでHello worldする方法その12

# evalの仲間のinstance_evalを使う、selfはHello, world!
"Hello, world!".instance_eval do
  puts self
end

instance_evalを使うことでputs selfと書けるのはかっこいい。


RubyでHello worldする方法その13

# thenとmethodメソッドのProc化を組み合わせる
"Hello, world!".then(&method(:puts))

method(:puts)putsメソッドをオブジェクトとして取得し、それを&Procに変換してthenに渡す。tapでも可。


RubyでHello worldする方法その14

# pはinspectを内部で呼ぶので、inspectが文字列を返せばよい
str = ""
def str.inspect
  "Hello, world!"
end
p str

空文字列のオブジェクトに特異メソッドとしてinspectを定義し、pが内部でそれを呼ぶ。


RubyでHello worldする方法その15

# putsは内部でto_sを呼ぶので、to_sがHello, world!を返せばよい
class Okura
  def name = "OKURA Masafumi"
  def work_as = "Freelancer"
  def available_for_hiring? = true
  def organizer_of = "Kaigi on Rails"
  def to_s
    "Hello, world!"
  end
end

puts Okura.new

自己紹介のためのクラスだが、to_sをオーバーライドすることでputsの引数になったときの挙動を変更している。


RubyでHello worldする方法その16

# 全てのputsがHello, world!になる
module HelloWorld
  def puts(*args)
    super("Hello, world!")
  end
end
Kernel.prepend(HelloWorld)
puts

Kernel.prependするとグローバルな関数の挙動を変更できてしまう。putsの内部でsuperを呼ぶことで本物のputsを偽物から使うことができる。


RubyでHello worldする方法その17

# 素直に新しいメソッドを定義する
module HelloWorld
  def put_hello_world
    puts("Hello, world!")
  end
end
Kernel.include(HelloWorld)
put_hello_world

Kernel.includeでグローバルな関数を定義できる。素直。


RubyでHello worldする方法その18

# Stringをオープンクラスする
class String
  def print_self
    puts self
  end
end

"Hello, world!".print_self

Rubyのクラスは再オープン可能なので、どんなクラスにもメソッドを追加できる。


RubyでHello worldする方法その19

# オープンクラスはお行儀が悪いのでrefinementsを使う
using Module.new {
  refine String do
    def print_self
      puts self
    end
  end
}

"Hello, world!".print_self

組み込みクラスの再オープンはあまり良くないのでRefinementsを使うほうが良い…か?


RubyでHello worldする方法その20

# メソッドチェーンは便利
class String
  ("a".."z").each do |char|
    define_method(char) { self + char }
  end
  def comma = self + ?,
  def space = self + ' '
  def ! = self + ?!
  def puts = Kernel.puts(self)
end
"H".e.l.l.o.comma.space.w.o.r.l.d.!.puts

まずは"Hello, world!"の各文字に対応するメソッドをStringクラスに定義し、メソッド名を自身に連結する。カンマとスペースはそのままでは動かないので別途メソッドを定義する。あとは文字ごとのメソッドをチェインするだけ。


RubyでHello worldする方法その21

# 文字列は隣接させると連結される
# %リテラルの区切り文字には空白が使える
# Thanks @tompng
puts(% Hello, ' world'"!")

文字列が隣接していると合体することを利用し、空白で囲まれた%リテラルにシングルクオートとダブルクオートの文字を連結している。


RubyでHello worldする方法その22

# 無意味なオブジェクト指向
class Printer
  def initialize(object)
    @object = object
  end

  def print
    Kernel.print @object
    puts
  end
end

Printer.new("Hello, world!").print

オブジェクト指向設計。putsを空打ちして改行を補っている。print内で素朴にprintを呼ぶと無限ループするので、Kernel.printを呼んでいる。


RubyでHello worldする方法その23

# putsはKernelのインスタンスメソッドなので、適当なオブジェクトにbindできる
Kernel.instance_method(:puts).bind(Object.new).call("Hello, world!")

instance_methodメソッドでputsメソッドをオブジェクトとして取得する。そのままではcallできないので適当なオブジェクトにbindしてからcallする。mainの文脈でinstance_methodを呼ぶのにKernelが必要。


RubyでHello worldする方法その24

# どうしてこうなるのかわからない
# Dummyクラスは警告抑制に必要、sayメソッドはないとSyntaxError
class Dummy
  def method_missing(meth, *args, &blk)
    puts meth
  end

  def self.const_missing(name)
    print name.to_s + ', '
  end

  def say(*)
  end
end

Dummy.new.instance_eval("say Hello, world!")

mainmethod_missingをオーバーライドすると警告がでるのでDummyクラスを使う。sayの後ろに"Hello, world!"を配置すると引数として解釈され、先に"Hello"のconst_missingが呼ばれる。次に"world!"のmethod_missingが呼ばれる。これをDummy内で行うためにinstance_evalを使っている。


RubyでHello worldする方法その25

# クラス名は文字列の代わりになる
class Hello
end

class World
  def self.to_s = 'world!'
end

print "#{Hello}, #{World}\n"

文字列の埋め込みはなんでも使える。具体的にはto_sが呼ばれるだけなので、クラス名が"Hello"ならそのまま埋め込める。"!"は定数名には使えないので、to_sをオーバーライドしている。


RubyでHello worldする方法その26

# world!の返り値を、world!の中で定義したhelloメソッドで利用する
def world!
  def hello(str)
    puts "#{__method__.to_s.capitalize}, #{str}"
  end
  __method__.to_s
end

hello world!

hello world!は右から評価され、world!メソッドが実行されると、その中でhelloメソッドが定義される。helloメソッドは引数を受け取るがその引数は定義の真下にあるworld!メソッドの返り値がそのまま来る。__method__はそのメソッドの名前のシンボルを返すので、to_sして結合する。


RubyでHello worldする方法その27

# exitしてもハローワールドはできる
at_exit { puts "world!" }
print "Hello, "
exit

途中まで出力してexitで終了するが、at_exitのブロックが実行されて残りの文字列が出力される。


RubyでHello worldする方法その28

# 文字列をバラしてから復元する
ORDER = {
  "H": [1],
  "e": [2],
  "l": [3, 4, 11],
  "o": [5, 9],
  ",": [6],
  " ": [7],
  "w": [8],
  "r": [10],
  "d": [12],
  "!": [13]
}

puts "Hello, world!".chars.shuffle.each_with_object("a"*13) { |char, result|
  index = ORDER[char.to_sym]
  result[index.shift - 1] = char
}

文字列の位置をハッシュで管理する。"Hello, world!
"の文字列をshuffleしても復元できる。復元用の文字列として"a"だけの文字列を用意し、インデックスを指定して文字を置き換えている。


RubyでHello worldする方法その29

# 変数名を使う、大文字や空白、!は使えないので工夫が必要
hello = nil
world = nil
binding.local_variables.each_with_index do |var, i|
  i.zero? ? (print var.capitalize) : (puts ", #{var}!")
end

ローカル変数を定義し、local_variablesを使うことで変数名を値として使う。"Hello"の大文字部分などはcapitalizeを使ってなんとかしている。


RubyでHello worldする方法その30

# Fiberで記号に関して処理を中断して、記号を入れたら再開する
f = Fiber.new do
  "Helloworld".each_char.with_index do |c, idx|
    print c
    Fiber.yield if idx == 4 || idx == 10
  end
end

f.resume
print ", "
f.resume
puts "!"

"Helloworld"という記号なし文字列を順番に表示するが、記号が入るべき位置でFiber.yieldを呼んで元のFiberに復帰する。記号を出力するとFiber#resumeでまた文字列の出力を再開する。

Discussion