社内Ruby会の紹介:Rubyの Kernel#` の気持ちを悪用して面白DSLをつくる

この記事は 『ドワンゴ Advent Calendar 2019』25日目 の記事です。



はじめに

こんにちは、 Ruたん( @ru_shalm )だよ!

ドワンゴでは、毎週火曜日に有志による社内Rubyの会「KBKZ.rb」 1 を開催しています。

この会は、決まった何かをしないといけないとか、特定のチームの人しか参加できないと言ったものではなく、最近のRubyの話や、業務のRubyで困ったときの相談など、話をしたい人が自由にRubyに関する技術交流ができる会として開催されています。

例えば以下のような話をしたりします。

  • こういう gem 使ってみようと思うんだけど何か知見ある?
  • こういうことを実現したいんだけど、いい感じの実装方法あるかなぁ?
  • パイプライン演算子、実際どうよ…?
  • Rustってどう?(Ruby…?)

ちなみに昨日はRuby 2.7のChangeLogをみんなで読みながらワイワイ気持ちを高めていました。

そこで今日は 「業務には1mmも役に立たないけど、個人的に面白かった話」 の1つとして Kernel#`で遊んでみた話を紹介します。


Kernel#` について

Ruby では以下のように記述することで、シェルでコマンドを実行できます。

`echo 1` # echo 1が実行される

一見、文法的にそういう機能があるのかな、と思う見た目をしていますが、実際は Kernel に実装されたメソッドが、人間にわかりやすい記述方法で呼び出せるようになっているものだったりします。

`command` -> String
command を外部コマンドとして実行し、その標準出力を文字列として返します。このメソッドは `command` の形式で呼ばれます。

https://docs.ruby-lang.org/ja/latest/class/Kernel.html#M_--60

つまり、こういうことです。

# コレは
`echo 1` 

# このように書いてもいい
Kernel.`('echo 1')

「だからどうした!?」となるところですが、この便利な記述方法を 悪用 活用し、他のクラスに ` メソッドを実装することで、面白いメソッド呼び出しを作ることができます。

class Hoge
  def `(arg)
    arg * 2
  end

  def my_eval(&block)
    instance_eval(&block)
  end
end

Hoge.new.my_eval do
  `abc` # => 'abcabc'
end

こういう感じの仕組みをうまいこと Opal とか利用しているらしい、というTwitter上での話題があり、その話から「じゃあこれを使って何か面白いことができないか?」という話が盛り上がりました。


お題:HTMLを組み立てるDSL

Webフロントの世界、主にReactなどの世界ではJSXというものが存在します。

function render(title: string, text: string) {
  return (
    <div>
      <h1>{title}</h1>
      <p>{text}</p>
    </div>
  );
}

コード内に直接XMLみたいな感じのものを書けて面白いですね。

実際のJSXはそのまま実行されるのではなく、トランスパイル(変換)の過程で普通の関数呼び出しに置換されるのですが、書き味としてこういう書き方がコード中でできるのは良さそうです。

Rubyの cgi で組み立てるとしたら、こんな感じですかね…?2

require 'cgi'

def render(title, text)
  cgi = CGI.new('html5')
  cgi.div do
    [
      cgi.h1 { title },
      cgi.p { text }
    ].join  # ← うーん…
  end
end

なんかあんまり嬉しくないですね……(◞‸◟)
これだったら無理にDSLで書かないで、erb みたいなテンプレート使えばよくない?という気持ちになってしまいそうです。

そこで先程の Kernel#` 、もとい ` メソッドの出番です!

もし、↓のような記述ができたら嬉しいような気がします!

# 注意:これは絶対に動きません
def render
  `div` do
    `p` = 'Hello, World'
  end
end

slim みたいな感じのDSLをRuby上で動くようにできれば、何かの役に立つような気はしますよね!?

上のイメージは `p` = 'Hello, World' の辺りが無理ゲーすぎるので諦めるとして、コレに近い形を考えてみたりしました。


実際に動く ` を使ったHTMLのDSLの例

SyntaxErrorから逃げられね~~とか言いながら、KBKZ.rbに来ていたみんなにアイデアをもらいながら書いてみたのが下のコードです。

class HTMLTemplate
  def initialize(name = 'div')
    @name = name
    @attributes = {}
    @children = []
  end
  attr_accessor :attributes

  def `(name)
    child = HTMLTemplate.new(name)
    @children.push(child)
    child
  end

  def render(attributes = {}, &block)
    self.attributes = attributes
    instance_eval(&block) if block_given?
    self
  end
  alias [] render

  def <<(text)
    @children.push text.to_s
  end

  def to_s
    attr = @attributes.map{|k, v| "#{k}=\"#{v}\""}.join(' ') # これHTMLエスケープしてないからヤバそう
    "<#{@name}#{" #{attr}" unless attr.empty?}>#{@children.map(&:to_s).join('')}</#{@name}>"
  end
end

これを使うと、以下のようにHTMLを組み立てることができます。

HTMLTemplate.new('div').render do
  `p`[class: 'text-center'] do
    `span` << 'Hello,'
    `strong` << 'World'
  end
end
# => <div>
#      <p class="text-center">
#       <span>Hello,</span>
#       <strong>World</strong>
#     </p>
#    </div>

意外とそれっぽく見えてムカつきますよね!

これを実現する上で、僕が一番の悩んだのはブロックの部分です。

# ↓みたいには絶対に書けない(SyntaxErrorになる)
`p` do
  ...
end

# ↓これもSyntaxError
`p`() do
  ...
end

# ↓これなら「 `p` の戻り値に対して `[]` メソッドの呼び出し」にできる!
`p`[] do
  ...
end

ブロックが渡せないと、子要素をうまく表現することができず悩んでいたのですが、[] メソッドを用いることでSyntaxErrorを回避できるよというアドバイスをもらい、それっぽい記述を実現することができました。

幸い、HTMLの場合は各要素に対して class などの属性を指定したくなることが多いため、p[class: 'hoge', 'data-hoge': '123'] みたいなことをするためにあるんだよ~と言い張ることもできます。

もちろんこんなDSLが業務の役に立つことは一生ないのですが、SyntaxError を避けるために parser.y を見てみたりなど、 なかなかに面白い時間を過ごすことができました。

やっぱRubyってたのしい!


まとめ

ドワンゴ社内で実施しているRuby交流会「KBKZ.rb」の紹介と、その中で出た今年の僕のイチオシ面白エピソードを紹介しました。

お外で他の会社のRubyな方とお話をすると「え、ドワンゴってScala以外あったんすかw」のようなことを言われてしまうことが多いのですが、 N予備校ニコニ立体 など、Rubyを使ったシステムも数多く存在しますし、そういったシステムに関わる人も、そうでない人も集まって、社内のRubystでワイワイ交流会を開催したりもしていたりします!


そんなわけでお約束の一文を。

ドワンゴは本物のRubyエンジニアを募集しています。
https://www.nnn.ed.nico/recruit/


それでは良いクリスマスを。 メリークリスマス!


おまけ

皆様へのクリスマスプレゼントとして、さらに遊んだコードをgem化してお手持ちのプロダクトにも採用できるようにしました!!!3

さらに思いつく変な記述方法を取り込んでいるのですが僕のイチオシは下の記述方法です。

require 'romantic_text'

RomanticText.markup do
  `table` > `tr` > `td`[class: 'red'] > 'item'
end
# => <table><tr><td class="red">item</td></tr></table>

なお、このコードを Rubocop 先生に見てもらうと凄く怒られます。知ってた。



  1. 「KBKZ」の由来はドワンゴ本社がある「歌舞伎座」から来ています

  2. ところで cgi にHTML作る機能あったの全然知らなかったです……><

  3. できる(できるとは言ってない)