2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

RubyでJavaScriptのTemplate literalsの真似をする

Last updated at Posted at 2018-12-13

この記事はOkinawa.rb Advent Calendar 2018の13日目の記事です。
昨日は @hanachin_ さんのTurnipで書いたfeatureにパスに応じて異なるtypeをつける方法でした。
明日は @hanachin_ さんの配列にためる手間なしenum_forです。

javascript_template_literals gem

Rubyの`の動作の変更をしてJavaScriptのTemplate literalsの真似をするgemです。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals

ソースコードです。
https://github.com/hanachin/javascript_template_literals

つかいかた

インストール方法です

% gem i javascript_template_literals

requireしてusingすると使えます。べんりですね。

require "javascript_template_literals"

using JavaScriptTemplateLiterals

a = 5
b = 10
puts `Fifteen is ${a + b} and
not ${2 * a + b}.`
# Fifteen is 15 and
# not 20.

def my_tag((strings, person_exp, age_exp))
  str0 = strings[0]
  str1 = strings[1]

  if age_exp > 99
    age_str = "centenarian"
  else
    age_str = "youngster"
  end

  `${str0}${person_exp}${str1}${age_str}`
end

person = "Mike"
age = 28
puts my_tag`That ${ person } is a ${ age }`
# That Mike is a youngster

実装方法

Rubyの`メソッド

Rubyでは`は、ただのメソッドです。Kernelに生えています。
https://docs.ruby-lang.org/ja/latest/class/Kernel.html#M_--60

通常は外部コマンドを実行します。
Refinementsで上書きすると好き勝手な動作ができるので、ここに実装を書いていきます。

module JavaScriptTemplateLiterals
  refine(Object) do
    def `(str)
      # ここでJavaScriptのTemplate literals風の動作をさせる
      "bar"
    end
  end
end

using JavaScriptTemplateLiterals

puts `foo`
# bar

呼び出し元の変数を参照する

JavaScriptのTemplate literalsでは${}の中から変数を参照できます。
Rubyで`メソッドの中から呼び出し元の変数を参照するにはbinding_of_callerを使い呼び出し元のBinding下で変数をevalします。

require "binding_of_caller"

module JavaScriptTemplateLiterals
  refine(Object) do
    def `(str)
      binding.of_caller(1).eval('x')
    end
  end
end

using JavaScriptTemplateLiterals

x = "foo"
puts `bar`
# foo

式展開で呼び出し元の変数を参照する

正規表現でいい感じに式をとってきて、gsubで置き換える際にbinding_of_callerを使って呼び出し元のBindingでevalします。

require "binding_of_caller"

module JavaScriptTemplateLiterals
  refine(Object) do
    def `(str)
      b = binding.of_caller(1)
      str.gsub(/\$\{([^\}]*)\}/) { b.eval($1) }
    end
  end
end

using JavaScriptTemplateLiterals

foo = "bar"
puts `${foo}`
# bar

Tagged templatesのような振る舞いの実装方

最初は`メソッドがtagに渡されるのであれば配列を返して、tagメソッドに渡す前に展開しようと考えていました。

module JavaScriptTemplateLiterals
  refine(Object) do
    def `(str)
      # もしここで返した値がtagっぽいメソッドに渡るなら配列を返す
      [["foo"], 1, 2]
    end
  end
end

using JavaScriptTemplateLiterals

def bar(strings, exp1, exp2)
  "bar"
end

puts bar(*`${foo}`)
# bar

しかし、tagメソッドにで返した文字列が渡るのか渡らないのかメソッドの中からは判断できませんでした(うまいやり方をしっている人がいたら教えてください)。
あと*が必要なのがJavaScriptっぽくなくてイケてない。

module JavaScriptTemplateLiterals
  refine(Object) do
    def `(str)
      # ここで返した値がtagっぽいメソッドに渡るかどうか、判断がつかない...
      [["foo"], 1, 2]
    end
  end
end

using JavaScriptTemplateLiterals

def bar(strings, exp1, exp2)
  "bar"
end

#    これ↓がいけてない
puts bar(*`${foo}`)
# bar

ところで、Rubyでは引数で()を使うと配列を分解できます。

def foo((a, b, c))
  a + b + c
end

puts foo([1, 2, 3])
# 6

"配列"と書きましたが、配列に変換できれば分解できます。
配列に変換する際はto_aryメソッドが呼ばれます。
to_aryで配列に変換できるのであれば配列じゃなくても渡せます。

def foo((a, b, c))
  a + b + c
end

obj = Object.new

def obj.to_ary
  [1, 2, 3]
end

puts foo(obj)
# 6

これを利用して、tagメソッドの引数は()で受けとるということにします...!

def my_tag((strings), exp1, exp2)
  # ...
end

a = 44
b = 23
my_tag`a + b = ${a + b}, a * b = ${a * b}`

`メソッドではdefine_singleton_methodを使い返す文字列にto_aryメソッドを定義します。
これで通常の文字列として利用する場合は通常の文字列として利用でき、tagメソッドに渡すとtagメソッドの引数にいい感じに展開される値を返すことができました! イェイ!

module JavaScriptTemplateLiterals
  refine(Object) do
    def `(str)
      # 普通は文字列として振る舞う
      ret = "foo2000bar"
      # to_aryメソッドがあるため
      # tagメソッドに渡されたときはタグっぽく振る舞う
      ret.define_singleton_method(:to_ary) do
        [["foo", "bar"], 2000]
      end
      ret
    end
  end
end

using JavaScriptTemplateLiterals

def my_tag((strings, exp))
  "#{strings.join}#{exp}"
end

puts `foo${1000 * 2}`
# foo2000bar

puts my_tag `foo${1000 * 2}`
# foobar2000

まとめ

  • Rubyの`はただのメソッド、Refinementsで上書きできる
  • メソッドの引数は()で囲うと分解できる
  • メソッドの引数が()で囲われているとき、わたした引数のto_aryメソッドが呼ばれ配列に変換してから分解される

RubyではなんでもメソッドなのでRefinementsで上書きし放題でべんりですね :v:

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?