SEAN_BLOG

プログラミング etc.

binding.pry がなぜ動くのか?今更ながら調べてみた

TL;DR

  • binding.pry は、Kernel#bindingObject#pry に分けられる。
  • Kernel#binding はコンテキストの環境情報を束縛したBinding オブジェクトを返す。
  • Object#pry はREPL プログラム。

そもそも、binding.pry とは?

Ruby を使っている方なら、一度はbinding.pry を利用したことがあるでしょう。

そう、debug するときにお世話になるあれです。

しかしながら、なぜあのような挙動になるのか知っている人は少ないと思います。 コード中にbinding.pry を記述し当該のコードを実行するだけで*1、 なぜかプロンプトが出現します。

     9: def name
 => 10:   binding.pry
    11:   puts @name
    12: end

[1] pry(#<Sample>)>

なぜでしょう。

wakaranai...

ということなので、今更ながら調べてみました。

そもそも、pry とは何なのか。

Pry is a runtime developer console and IRB alternative with powerful introspection capabilities. Pry aims to be more than an IRB replacement. It is an attempt to bring REPL driven programming to the Ruby language.

cf. https://github.com/pry/pry#introduction

要はruntime developer console だと。 そして、REPL driven programming をRuby に導入することを目指すものらしい。

でつ。oO(はて。REPL とは何だろう。。。難しそう。。。)*2

REPL (Read-Eval-Print Loop) とは、入力・評価・出力のループのこと。 主にインタプリタ言語において、ユーザーとインタプリタが対話的にコードを実行する。

cf. https://ja.wikipedia.org/wiki/REPL

とのことです。

今回はこちらのREPL の処理を中心に、pry を紐解いていこうと思います。

と、その前に今回はbinding.pry がなぜ動くのか?という事なので、 Kernel#Binding メソッドについても調査していきます。

Kernel#binding

まずは、binding.pry の前半分の、binding

これは、gem pry に依存するものではなく、Ruby 組み込みライブラリKernel モジュールのメソッドです。 変数やメソッドなどの環境の情報を束縛した、Binding クラスのオブジェクトを返します。

def sample
  name = 'John'
  binding
end

eval('puts name', sample)
#=> John

cf. Binding

Kernel#eval の第二引数に渡すと、そのコンテキストで第一引数の文字列をRuby プログラムとして評価してくれます。

つまり、binding.pry を実行したときに必要な情報が取得できるのは、この人のおかげという事ですね。

でつ。oO(すてき。。。)

Object#pry

さて、binding.pry の残りの部分pry の部分を深掘りしていきます。

肝心の pry メソッドですが、gem pry のなかで定義されています。

class Object
  ...
  def pry(object = nil, hash = {})
    if object.nil? || Hash === object # rubocop:disable Style/CaseEquality
      Pry.start(self, object || {})
    else
      Pry.start(object, hash)
    end
  end
  ...

Open class を用いてObject クラスに、pry メソッドを定義しています。 Object クラスは全てのクラスのスーパークラス*3なので、Object#pry を定義することで、 全てのオブジェクトでpry メソッドが利用できるようになります。

実際にModule#method_defined? を使って確かめてみると。

p Object.method_defined?(:pry)
#=> false

require 'pry'

p Object.method_defined?(:pry)
#=> true

require 'pry' の前では Object クラスに pry が定義されていなにのに対し、 require 'pry' の後では Object#pry が定義されていることが確認できます。

さて、ここからはbinding.pry がコード中で実際に呼ばれたとき、何が起こるのか見ていきます。

コード中でbinding.pry を呼ぶと、 Object#pry にデフォルト引数object = nil が渡されます。 そして、Object#pry の内部で、Pry.start(self, object || {}) が呼ばれます。

  def self.start(target = nil, options = {})
    ...
    options[:target] = Pry.binding_for(target || toplevel_binding)
    ...
    driver = options[:driver] || Pry::REPL

    # Enter the matrix
    driver.start(options)
    ...
   end

Pry.start は全体で30行程あります。 長くなるのでメインの処理の関係する箇所のみ抜粋しました。

Pry.binding_for が呼ばれています。target が存在すれば target を、 無ければトップレベルスコープの binding が引数として渡されます。

binding.pry のケースでは、self(=binding)Pry.binding_for の第一引数として渡されます。

  def self.binding_for(target)
    return target if Binding === target # rubocop:disable Style/CaseEquality
    return TOPLEVEL_BINDING if Pry.main == target

    target.__binding__
  end

こちらのメソッドは渡されたtargetbinding を返します。 binding.pry のケースでは、self(=binding) が戻り値となります。

    options[:target] = Pry.binding_for(target || toplevel_binding)

その結果、options[:target]self(=binding)アサインされます。

Pry.start では続いてdriver に Pry::REPLアサインされ、Pry::REPL.start が呼ばれます。 options が引数として渡されるので、先ほどoptions[:target] としてアサインした束縛情報ももれなく渡されています。

    def self.start(options)
      new(Pry.new(options)).start
    end

    ...

    def start
      prologue
      Pry::InputLock.for(:all).with_ownership { repl }
    ensure
      epilogue
    end

Pry::REPLインスタンスが作成され、Pry::REPL#start が呼ばれます。 Pryインスタンスが生成され引数として渡されています。 また、この際Pryインスタンス変数@binding_stackoptions[:target] の中身がアサインされています。

やっと、主要の処理にたどり着きました。Pry::REPL#repl です。

感の良い方はもうお気付きですね!(感が鈍くても気がつきますね。。。)

そう、Pry::REPL#repl は、先ほど説明したRead-Eval-Print Loop です!

    def repl
      loop do
        case val = read
        when :control_c
          output.puts ""
          pry.reset_eval_string
        when :no_more_input
          output.puts "" if output.tty?
          break
        else
          output.puts "" if val.nil? && output.tty?
          return pry.exit_value unless pry.eval(val)
        end
      end
    end

コードを見てみると、何だか色々やってそうですが、 重要なのはPry::REPL#readPry#evalKernel#loop です。

でつ。oO(あれ、print が無い。。。)

read

Pry::REPL#read > Pry::REPL#read_line > Pry::REPL#input_readline という順で見ていきます。

    def input_readline(*args)
      Pry::InputLock.for(:all).interruptible_region do
        input.readline(*args)
      end
    end

input@pry 経由でPry::Config からdelegate されています。

  class Config
    ...
    def initialize
      merge!(
        input: MemoizedValue.new { lazy_readline },
        ...
    def lazy_readline
      require 'readline'
      ::Readline
      ...
    end

長くなりそうなので中間の処理を省きます。

上記の通りinput には::Readlineアサインされるので、 Readline.readline が呼ばれ、ユーザからの入力を取得します。cf. Readline

これがコード実行中にプロンプトが表示されて、入力待機状態になる現象の実態ですね。 無事ユーザからの入力部分は理解できました。

続いて、評価部分のeval を見ていきます。

eval

前出のPry::REPL#repl を見るとpry.eval(val) となっており、先ほど見たread の戻り値をPry#eval に渡しています。

ちなみに、ここで呼ばれているeval は、Pry クラスでoverride されており、Kernel#eval とは異なります。

Pry#eval > Pry#handle_line > Pry#evaluate_ruby と読み進めていきます。

  def evaluate_ruby(code)
    inject_sticky_locals!
    exec_hook :before_eval, code, self

    result = current_binding.eval(code, Pry.eval_path, Pry.current_line)
    set_last_result(result, code)
  ensure
    update_input_history(code)
    exec_hook :after_eval, result, self
  end

こちらも途中の処理をだいぶ省きました。

Pry#evaluate_ruby の中で、current_binding.eval が呼ばれています。 Pry#current_binding は、binding_stack に格納されている現状のBinding オブジェクトを返します。 Pry#current_binding の戻り値のコンテキストで、文字列codeRuby のコードとして評価します。 ちなみに、多少処理がなされていますがcode の中身は、先ほどのPry::REPL#read の返り値です。

結局のところ、対象のbinding に対して、binding#eval を呼び出しているだけですね。

さて、ここまででreadeval までの処理が完了しましたね。

次は表示print の部分です。

print

実は先ほど見たPry#eval の中にprint の処理も含まれています。

Pry#eval > Pry#handle_line > Pry#show_result と掘り進めます。

  def handle_line(line, options)
    ...
      Pry.critical_section do
        show_result(result)
      end
    ...
  end
  def show_result(result)
    if last_result_is_exception?
      exception_handler.call(output, result, self)
    elsif should_print?
      print.call(output, result, self)
    end
    ...
  end

print の中身は、Pry::ColorPrinter.method(:default) の戻り値Method オブジェクトです。 そのMethod オブジェクトにcall メソッドを呼んでメソッドを実行しています。

require 'pp'
...
class Pry
  class ColorPrinter < ::PP
    ...
    def self.default(_output, value, pry_instance)
      pry_instance.pager.open do |pager|
        pager.print pry_instance.config.output_prefix
        pp(value, pager, pry_instance.output.width - 1)
      end
    end
    ...
  end
end

Pry::ColorPrinter.default の中で、pry_instance.pager.open によってfile を開きます。 ここで開くfile に関しては、設定によって変わります。

ブロックの中で、Pry::ColorPrinter.pp を呼び出します。

    ...
    def self.pp(obj, output = $DEFAULT_OUTPUT, max_width = 79)
      queue = ColorPrinter.new(output, max_width, "\n")
      queue.guard_inspect_key { queue.pp(obj) }
      queue.flush
      output << "\n"
    end

そして、ColorPrinter#pp を呼び出します。

    ...
    def pp(object)
      return super unless object.is_a?(String)

      text(object.inspect)
    rescue StandardError => exception
      raise if exception.is_a?(Pry::Pager::StopPaging)

      text(highlight_object_literal(inspect_object(object)))
    end
    ...

引数のobjectString 以外の時は、 キーワードsuper を呼び出し親クラスPPpp メソッドを呼び出しています。 それ以外のケースでは、ColorPrinter#text を呼び出します。

    ...
    def text(str, max_width = str.length)
      if str.include?("\e[")
        super("#{str}\e[0m", max_width)
      elsif str.start_with?('#<') || %w[= >].include?(str)
        super(highlight_object_literal(str), max_width)
      else
        super(SyntaxHighlighter.highlight(str), max_width)
      end
    end
    ...

表示する文字列str の内容に応じて処理を変更しています。 基本的にはこちらもキーワードsuper により、親クラスのPP#text を呼び出しています。

この一連の処理によって、結果が出力されます。

loop

あとはこれを外側のループで回すだけです。

でつ。oO(loop は特に書くことが無い。。。)

ここまでの一連の流れがbinding.pry ないしは、pry メソッドの正体です。 pry というgem 全体で見ると処理も多く、コード数もそれなりにあります(20,000 行強*4)。

ただ、REPL に関する処理は、意外とシンプルなようでした。

所感

今まで何となく使っていたbinding.pry ですが、実際に調べてみて、 ブラックボックス化されていたことを明らかにすることができました。

# 今まで
binding.pry == 便利な魔法の呪文

# 今
binding.pry == REPL プログラム

頭の中でbinding.pry の単語をアップデートすることができました*5

最後まで読んでいただきありがとうございます🙇‍♂️

参考

*1:gem のインストールが必要。

*2:でつ: 社のSlack でご活躍のスヌーピーさん。

*3:実際にはBasicObject クラス(Ruby 1.9 以降)という、Object クラスの親にあたるクラスが存在します。

*4:コード量計測には'AlDanial/cloc' を使用。

*5:pry は様々なコマンドが利用出来たり、ハイライトが効いたりと単純なREPL プログラム以上のものです。

Ruby 2.7 の新機能(Pipeline operator |> 編) - チケットとコミットログを読んでみて

さて、今回はRuby 2.7 の新機能、Pipeline operator |> についてのエントリーです。 RubyKaigi 2019 のRuby Committers vs the World で取り上げられていたあれです。

6月13日にtrunk にmerge されたようですが、それ以降Twitter を中心に議論が盛り上がっていますね。 他の機能に対しても言えることですが、今後も機能が大きく変わる可能性がありそうです。

Pipeline operator |>

TL;DR

  • dot の代わりに、|> を利用することが可能。
  • |> はdot に比べて優先順位が低く、Range の括弧を省略可能。(e.g. 1..10 |> each { p @1 })
  • |> の後に他のoperator を続けることは不可。(e.g. 10 |>+ 10)

使用例

$ ruby -v
ruby 2.7.0dev (2019-06-16T05:46:28Z trunk 2fb1564c02) [x86_64-linux]
# w/o pipeline operator
(1..).take(10).map { @1 * 2 }
#=>[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

# w/ pipeline operator
1..
  |> take 10
  |> map { @1 * 2 }
#=>[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

基本的にはmethod 呼び出しのdot と同じ動きをします。 Pipeline operator |> は、dot に比べ優先順位が低いです。 そのため、上述のコードのようにRange の括弧を省略することが可能です。

Numbered parameters @1 については、こちらをご覧ください。

Ruby 2.7 の新機能(Numbered parameters 編) - チケットとコミットログを読んでみて - SEAN_BLOG

チケット

bugs.ruby-lang.org

全体を通りしてPipeline operator の追加に否定的な意見が多かったように見受けられました。 主な理由としてあげられていたのが、以下の2つ。

  • 関数型のPipe operator をRuby に持ち込むのは如何なものか。
  • ElixirやF#、Elm のPipe operator とRuby 2.7 の"Pipeline operator"の挙動は異なる。
# w/o pipe operator
foo(bar(baz(new_function(other_function()))))

# w/ pipe operator
other_function() |> new_function() |> baz() |> bar() |> foo()

上記は、Elixir のPipe operator のコード例です。

記述法はRuby 2.7 のPipeline operator と変わりません。

ただ、Elixir のPipe operator では、前のfunction の結果を次のfunction の第一引数としてとります*1。 つまり、上記の例ではother_function() の戻り値が、new_function() の引数として渡されることになります。 (baz() 以下も同様。)

一方で、Ruby 2.7 のPipeline operator では、前のmethod の戻り値が次のmethod の引数としては渡されません。

コミットログ

github.com

今回もテストコードを中心に見ていきます。

dot の代わりに、|> を用いていますね。

assert_equal("121", eval('x = 12 |> pow(2) |> to_s 11'))

dot を用いるとこんな感じですかね。

eval('x = 12.pow(2).to_s 11')

ただ、dot の場合、変数x12.pow(2).to_s 11 の戻り値である121 が代入されます。 それに対して、Pipeline operator ではx = 12 になっているようですね🤔

優先順位がことなるからですね。

assert_syntax_error('a|>-b', /unexpected '-'/)

こちらは、6月14日に追加されていたのですが、|> の後に別のoperator を用いることができなくなっています。 この点はdot と異なるようですね。

所感

全体的にPipeline operator の導入には否定的な意見が多かったように思います。 確かに、実際のユースケースを考えて見ると、dot ではなくPipe operator を利用するケースが思い付かない😇 果たしてこれは、僕の経験が無いからなのか、そもそも使い道がないからなのか。。。

といったところで、今回は失礼します。

最後まで読んで頂き、ありがとうございます🙇

参考

*1:F#、Elm の|>では、function の結果を次のfunction の引数の最後にappend する仕様のようです。

Ruby 2.7 の新機能(Method reference operator .: 編) - チケットとコミットログを読んでみて

さて、今回はRuby 2.7 の新機能、Method reference operator .: についてまとめていきます。 Ruby 2.7 の新機能に関しては、前回のNumbered parameters に続き今回で3本目のエントリーです🎉

なんだか、連載のような感じになってきてしまいました。

最初は1つのエントリーに全てまとめるつもりだったのに。。。

Method reference operator .:

TL;DR

  • Object#method の代わりに、Method reference operator .: を利用可能
  • Dir["*/*.c"].map(&File.:basename) といった書き方が可能
  • Numbered parameters は便利

使用例

# w/ Object#method
m = 100.method(:to_s)
#=> #<Method: Integer#to_s>
m.call
#=> "100"

m = 100.method(:to_hash)
#=> NameError (undefined method `to_hash' for class `Integer')

引数で指定したメソッドの名前をオブジェクト化した、Method オブジェクトを返します。 もし、その名前のメソッドが存在しない場合は、NameError を返します。 メソッドをオブジェクト化したことにより、クロージャーとして利用することが可能になります。

これを、.: を用いて下記のように書くことができるようになりました。

# w/ .:
m = 100.:to_s
#=> #<Method: Integer#to_s>
m.call
#=> "100"

m = 100.:to_hash
#=> NameError (undefined method `to_hash' for class `Integer')

挙動は Object#method と同じです。

チケット

今回もチケットから導入の経緯、導入までの一連の流れを見ていきましょう。

bugs.ruby-lang.org

bugs.ruby-lang.org

今回の出発点は、下記のような処理をしたい時に、

  • ブロックパラメターを利用する方法
w/o Object#method
Dir["*/*.c"].map { |f| File.basename(f) }
  • Object#method を用いる方法
w/ Object#method
Dir["*/*.c"].map(&File.method(:basename))

どちらもあまりいけてないよね?というところのようです。

そこで、lambda で使われている-> を用いて、下記のような方法を用いるのはどうかと提案されています。

Dir["*/*.c"].map(&File->basename)

これを受けて、Matzさんは「アイディアは好きだけど、-> はちょっと。」と言った反応を示されておりました。

I like the idea of short hand notation for Object#method(), but I don't think -> is a good idea.

Matz.

チケットの中では.><..>&>->>=>>+>$>:>\.Object[.method]: や、 Elixir の|> を用いたらどうかという代替案が出されていました。

また、メッセージセンドの:: を地上げし、再利用するという提案もありました。 ちなみに、Java のMethod reference には :: が用いられているようです。

そんな中、Matzさん、

「候補の中で.: がベスト、ついで :::。...」とのこと。

..., .: looks best to me (followed by :::)....

Matz.

結果、.: の形に収まったようです。

その他、RubyPerl に近づいていくことを危惧する意見や、これ以上記号をRuby に導入しないで欲しいといった意見などの Method reference operator の導入しネガティブな意見も散見されました。

コミットログ

github.com

今回もテストを中心に、読み解いていこうと思います。

基本的には、Object#method と同様の挙動になるようテストが書かれています。

m = 1.:succ
assert_equal(1.method(:succ), m)
assert_equal(2, m.())

m = 1.:+
assert_equal(1.method(:+), m)
assert_equal(42, m.(41))

m = 1.:-@
assert_equal(1.method(:-@), m)
assert_equal(-1, m.())

o = Object.new
def o.foo; 42; end
m = o.method(:foo)
assert_equal(m, o.:foo)

def o.method(m); nil; end
assert_equal(m, o.:foo)
assert_nil(o.method(:foo))

通常のメソッド同様、改行にも対応してます。

assert_valid_syntax("a\n.:foo")

所感

Ruby 2.7 のMethod reference operator はなんとなく、Pattern matching とNumbered parameters の陰に身を潜めてしまっている感があります。 僕自身もPattern matching、Numbered parameters を先にまとめているので、何にも言えませんが。。。

w/o .:
Dir["*/*.c"].map { |f| File.basename(f) }

w/ Object#method
Dir["*/*.c"].map(&File.method(:basename))

w/ .:
Dir["*/*.c"].map(&File.:basename)

.: の導入により、上記のように書くことが可能になりました。 こう比べてみるとスッキリしましたね。

そういえば、Numbered parameters を用いてもかけますね。

w/ Numbered parameters
Dir["*/*.c"].map { File.basename(@1) }

少しだけ、Method reference operator .: の方が記述量少ないのか🤔 にしても、Numbered parameters も便利ですね。

これまでそもそも Object#method 知らなかったので、 これを機にガシガシ Object#method、Method reference operator .: 使っていきたいと思います。

では、今回はこの辺で。

最後まで読んで頂き、ありがとうございます🙇

参考

Ruby 2.7 の新機能(Numbered parameters 編) - チケットとコミットログを読んでみて

5月30日(木)、Ruby 2.7.0 preview1 がリリースされましたね。

パチパチパチ👏🎉👏

さて、前回のパターンマッチング編に引き続きRuby 2.7 の新機能、Numbered parameters についてまとめていきます。

Numbered Parameters

TL;DR

  • ブロックパラメターに @1, @2 という新しい記法を用いることが可能。
  • @n の記法はインスタンス変数を想起させ見慣れないが、時が解決してくれる(きっと)。
  • Hash でNumbered parameters を用いると著しく可読性が下がる(気がする)。

使用例

#w/o Numbered parameters
[1, 2, 3].map { |n| n + 2 }

#w/ Numbered parameters
[1, 2, 3].map { @1 + 2 }

#w/o Numbered parameters
{ key1: 'foo', key2: 'bar' }.map { |key, val| [key, val] }

#w/ Numbered parameters
{ key1: 'foo', key2: 'bar' }.map { [@1, @2] }

今まではこんな感じでブロックパラメターに名前をつける必要がありました。 しかし、Ruby 2.7 では @ を用いたデフォルト値を使うことができます。 便利そうですが、Ruby@ というとインスタンス変数を想起してしまいます。。。

チケット

今回もチケットの内容を読み解いてみることにします。

bugs.ruby-lang.org

@my_sons.each { |s| s.sell_to_someone }

Account.all.each { |a| my_account << a.money }

そもそもNumbered parameters という概念の始まりは、

ブロック中の変数の名前、そこまで重要でない時もありますよね。 もしかして、デフォルト値作ったら便利じゃないですか?

というところのようです。 ちなみに、チケットでは下記のようなサンプルコードも挙げられています。

Account.all.each { my_account << it.money }

お、it が。。。 これはRSpec の方々が黙ってなさそう。 と思ったらやはり、RSpec 等の既存のコードとの互換性の問題で it は却下されていました。

現状trunk にマージされたNumbered parameters は @n という形を取っています。

Matzさん曰く、「@ の見た目は、最初慣れないけどそのうち見慣れるでしょ!」とのことです。

I still feel weird when I see @ and @1 etc. Maybe I will get used to it after a while. I need time.

議論の中では先ほど挙げた it 、そして意味的にそれに近い this。 記号系では、\n$n&n. なんかも候補に挙げられてました。 あと、`(backtick)? この辺の記号を地上げして使えば良いみたいな少し過激な意見もありました。

そしてこのNumbered parameters ですが、非常に議論が盛り上がっているようで、 trunk にマージされた後も新規でチケットが起票されていました。

bugs.ruby-lang.org

その他個人的に印象に残った意見が、数文字のタイプを減らすために、ここまで可読性を犠牲にするのはどうかと言ったものです。 さらには、可読性が落ちることで初学者の学習コストが上がると言ったところ言及されており、 機能追加の段階でここまでの議論がなされているものなのかと驚きました。

また、Numbered parameters を利用した実例として、ネガティブな反応が多かったのがHash を用いたケース。

h = Hash.new { |hash, key| hash[key] = "Go Fish: #{key}" }

#vs

h = Hash.new { @1[@2] = "Go Fish: #{@2}" }

確かにこれは、Numbered parameters を使うとわかりづらいかも。。。

これを受けて、Matzさん

So use numbered parameters with care (just like other features in Ruby)

「(他の機能もそうだけど、)注意して使いましょうね。」とのこと。

時と場合によりけりといったところでしょうか。

コミットログ

github.com

さて、お待ちかねのコミットログです。 今回も前回同様、テストコード中心に読み進めていきます。

まずこちら、

assert_syntax_error('proc {|| @1}', /ordinary parameter is defined/)
assert_syntax_error('proc {|x| @1}', /ordinary parameter is defined/)

|x| といった現状のブロック変数との併用はできないようです。

assert_syntax_error('proc {@1 = nil}', /Can't assign to numbered parameter @1/)

ブロック中でNumbered parameter に値をアサインすることも出来ないようです。

assert_syntax_error('proc {@01}', /leading zero/)
assert_syntax_error('proc {@1_}', /unexpected/)

0 から始めるのもエラーのようですね。

アンダースコア用いることも出来ないようです。 と言っても、あんまり利用例も考えつきませんが。。。

assert_syntax_error('proc {@9999999999999999}', /too large/)

これは。。。too largeです。。。笑

assert_syntax_error('@1', /outside block/)

もちろん、ブロックの外でNumbered parameter を呼ぶのもエラーが出ます。

所感

RubyKaigi 2019 期間中初めて、Numbered parameters の存在を知ったのですが、その時は便利そうだなーと思いました。 ただ、今回チケット・コミットログを読んでみて、Hash のケース等は可読性が著しく下がってしまい、一概に便利ということではないようですね。 Matzさんもおっしゃる通り、節度を持って利用したいと思います。

Ruby 2.7 では Enumerable#filter_map というメソッドが新しく導入されています。 この辺とNumbered parameters 一緒に使ってみたいですね。 気が向いたら、Enumerable#filter_map についても記事まとめます!

では、今回はこの辺で。

最後まで読んで頂きありがとうございます🙇

参考

Ruby 2.7 の新機能(Pattern matching 編) - チケットとコミットログを読んでみて

今回はRubyKaigi 2019 に参加して俄然モチベーションが上がったので、 NEWS for Ruby 2.7.0 の中から気になった点をいくつかピックアップしてまとめていきたいと思います。*1

パターンマッチング

使用例

def pattern_match(arg)
  case arg
  in [a]
    p a
  in { key: a }
    p a
  end
rescue NoMatchingPatternError => e
  p e
end

pattern_match([1])
#=>1
pattern_match({key: 'value'})
#=>"value"
pattern_match('string')
#=>#<NoMatchingPatternError: string>

挙動としてはこのように、パターンが一致すれば値をキャプチャすることができます。 また、全てのパターンと一致しない場合は NoMatchingPatternError の例外を投げます。

ちなみに、現状Experimental なので、パターンマッチングを実行すると、 下記のようなwarning が出ます。

pattern_match/sample.rb:2: warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!

チケット

bugs.ruby-lang.org

元々k-tsj/pattern-match というgem で開発されていたようです。 gem のリリースは2012年ということなので、遡ること7年ほど。

お恥ずかしながらこちらのgem の存在を初めて知りました。 正直、詳しいことはわかりませんが、README を見る限りRuby trunk のパターンマッチングとはシンタックス等大きく変わっているようです。

チケット内で下記の2点がデザイン・ポリシーとして挙げられています。

  • Keep compatibility
  • Be Ruby-ish

"compatibility" に関しては、RubyKaigi 2019 のなかでMatzさんを始め、 他のRuby comitter の方々も言及していたように記憶しています。

k-tsj/pattern-match の方では Object#match という形で、パターンマッチングが表現されていました。 しかし、match を導入すると、String#match や、Regexp#match 等の既存のコードを壊してしまう恐れがある。 そういった理由でmatch の利用は見送られ、case 文を拡張する形で実装が進められたようです。

"Be Ruby-ish" という点に関しては、Ruby の動的型づけ言語としての強みを活かすよう意識されたようです。

"Keep compatibility" と"Be Ruby-ish"。 言葉で言うのは簡単ですけど、実現するのは本当に大変そうです。

こういったRuby committer の方々の苦労に支えられているのですね。いつも本当にありがとうございます。 自分も少しずつRuby community に恩返ししていきたいです。

コミットログ

github.com

さて、ここからは実際のコミットログを見て少しだけ深掘りしていきます。 C言語が全く分かりませんので、今回はテストコードをヒントに紐解いていきます。

assert_block do
  case 0
  in a if a != 0
  else
    true
  end
end

このようにguard exp を置くこともできます。

assert_block do
  case 0
  in 0 => a
    a == 0
  end
end

as pattern を用いることで、このように変数に代入することも可能です。

assert_block do
  [0, 1].all? do |i|
    case i
    in 0 | 1
      true
    end
  end
end

| を用いて、or を表現することも可能です。

assert_block do
  a = /a/
  case 'abc'
  in ^a
    true
  end
end

^ を使うと、既に定義されている変数と比較することも可能のようです。 下記の通り、^ を利用するとa = 0 がパターンマッチに利用されています。

a = 0
case 1
in a
  p a
end
#=>1

a = 0
case 1
in ^a
  p a
end
#=>NoMatchingPatternError

所感

パターンマッチングは、API とかでJSON をparse するときに用いたら便利そうだなと。 強い方々がRuby 2.6 のASTxPattern matching とかよく耳にするので、そちらもやってみたいです。 まずは、AST を調べるところから始めたいと思います。

今回RubyKaigi 2019 で感化されて、初めてRubyredmine のチケットやコミットログとか見てみましたが面白かったです。

普段Ruby committer の方々がどういった意識でRuby の開発に取り組まれているのかを垣間見ることができました。

正直なところコミットログに関しては、C言語ができないと言う理由で距離を置いていました。 しかし、実際に見てみるとテストコードから基本的な挙動を理解することができました。

自分みたいなC言語できない人も安心です。

「テストコードはドキュメントの代わり」という言葉の意味をやっと理解し、テストコードの重要性を再認識しました。

とはいえ、C言語を習得してコミットログちゃんと理解できるようにしたいなぁ、、、今後の課題です。

参考

*1:まとめ始めたら長くなってしまったので、いくつかの記事に分けたいと思います。(気が向いたベースで書きます。)

フロント初心者がReact + Redux でオセロを作ってみた話

普段はRuby をメインで使っているのですが、今年に入ってから少しReact + Redux を触っていました。 と言っても、GW に入るまではReact とRedux の公式Tutorial を写経していたくらいですが、、、

で、GW に入ってから一念発起しまして、完成したのがこの「オセロ」アプリです。 GW 最初の4日間はUdamy のReact + Redux コースをひたすら進めていたので、 実質の実装期間は1.5日です。

取り敢えず、形になりましたので忘れないうちにまとめていこうと思います。

目的

  • React + Redux の初心者を脱する。
    • Tutorial をただ写経するレベルを卒業する。
    • React + Redux の一連のflow を理解する。
  • ECMAScript6 等、新しいJS をキャッチアップする。

と、ここら辺のことを意識して、今回は実装をしました。 本当はAPI 使ってCRUD アプリを作ろうかと思っていたのですが、 色々な葛藤がありまして蓋を開けたら「オセロ」が完成していました。。。

使った技術・ライブラリー等

  • React
    • redux - state 管理をしてくれる人
    • classnames - 複数のclassNames をよしなにまとめてくれるいい奴
    • react-alert - いい感じのalert を表示してくれるいい奴
    • etc...
  • Heroku - 10分ほどでdeploy できるいい奴

versions

$ npm ls react redux classnames react-alert
@0.1.0 /path/to//react/othello
├── classnames@2.2.6
├── react@16.8.6
├── react-alert@5.4.0
└── redux@4.0.1

階層構造

src 以下はこんな感じに仕上がりました。 階層構造に関してはUdamy で写経したアプリの構造を参考にして分けてみました。 CSS は、CSS Module なるものが存在するようなので、見よう見まねで導入してみました。

$ tree othello/src
othello/src
├── actions
│   ├── index.js
│   └── types.js
├── components
│   ├── App.js
│   ├── Board.js
│   ├── Grid.js
│   ├── Header.js
│   └── Label.js
├── index.js
├── reducers
│   ├── boardReducer.js
│   ├── index.js
│   └── playerReducer.js
└── styles
    ├── board.module.css
    ├── grid.module.css
    └── header.module.css

4 directories, 15 files

成果物

DEMO

Image from Gyazo

  • 仕様
    • 新規で石を付与することが出来る。
    • 同色の石に挟まれた石が裏返る。
    • ルール上、石を付与することが出来ないマスをクリックすると、アラートを表示する。
    • Reset ボタンを押下すると、初期状態に戻ることが出来る。

URL

Heroku に上げてありますので、お時間ある方は遊んでみてください。

URL: https://aqueous-mesa-26312.herokuapp.com/

実装内容

オセロ盤の状態とプレーヤー(白 or 黒)の状態をstate として管理しました。 Components はオセロ盤のstate (二次元配列)board[10][10]map し、Grid マス目を表すComponent を表示し描画しました。

詳細は下記ご覧ください。( コード、グチャグチャです、、、m(_ _)m)

github.com

ちなみに、今回の実装の目的は、主にReact + Redux を理解することでした。 その為、オセロ自体のアルゴリズムは下記のサイトを参考に実装しました。 参考というか、コピペです。

大変助かりました、ありがとうございます m(_ _)m

www.cc.kyoto-su.ac.jp

ハマったところ

React + Redux の設計の問題

Udamy で作成したサンプルアプリの見よう見まねで設計してみたのですが、結局正解がわからず。。。 行ったり来たりしてしまいました。 設計に関しては経験が物を言うみたいなのもありそうですが、これくらいの規模のアプリならすんなりと設計できるようにしたいものです。。。

mutable vs. immutable

「state の中身変わってるのにrendering されないやん。」と思ったら、これが原因でした。 勝手にハマって、半日を溶かしました。。。 こちらのブログわかりやすかったです。

blog.logrocket.com

hooks はclass component 内では使えない

対象箇所を別のcomponent に切り出し、そこだけfunction component としました。 いまいち、class component とfunction component のメリット・デメリットがわかりません。 まだまだですね。。。ちゃんと復習します!

まとめ

初めて写経を脱っしReact + Redux でアプリを作成したことで、少しだけ自信がついた気がします。 React のシンタックス、Redux の流れに関しては、まだまだマスターしたとは程遠いものの、理解を深めることが出来ました。 アプリを作成する中で新たなLibrary を発見できたのも、良かったかなぁと。 総じて、当初の目的は達成することが出来たかな、と思います。

React + Redux の初心者を脱する。

「中級者」になれたのかと言われれば、まだまだなれてはおりませんが、、、m(_ _)m

少しだけ前進したと言うことで、お許しください。。。

参考

Note to self: ActiveSupport::TimeZone とString での時間の比較

先日TimeZone 周りでハマったので、備忘用にメモします。

結論

  • Time.zone.parse('2019-01-01')2019-01-01 は異なる。*1
  • ActiveSupport::TimeZoneString での時間の比較は注意が必要。
  • Time.zone.parse は比較対象もTime.zone.parse にするのがbetter。

内容

先日RoR で書かれたとあるコードのリファクタリングを行う機会がありました。 メインロジックの実装を完了し、Rspec でテストコードを実装していました。 その日は珍しく集中力が続き、スラスラとテストコードの実装が進みました。 が、それも束の間

$ bundle exec rspec spec/helpers/test_helper.rb
F

Failures:

  1) TestHelper#on_sale? when the released date is on/past 2019-01-01 should be true
     Failure/Error: expect(on_sale?(album)).to be true

       expected #<TrueClass:20> => true
            got #<FalseClass:0> => false

       Compared using equal?, which compares object identity,
       but expected and actual are not the same object. Use
       `expect(actual).to eq(expected)` if you don't care about
       object identity in this example.
     # ./spec/helpers/test_helper.rb:10:in `block (4 levels) in <top (required)>'

Finished in 0.22805 seconds
1 example, 1 failure

Failed examples:

rspec ./spec/helpers/test_helper.rb:9 # TestHelper#on_sale?  when the released date is on/past 2019-01-01 should be true

Rspec が落ちました。 ソースを確認してみると、

module TestHelper
  def on_sale?(album)
    album.released_at >= '2019-01-01'
  end
end
require 'spec_helper'

describe TestHelper do
  describe '#on_sale?' do
    let!(:album) { create(:album, released_at: Time.zone.parse('2019-01-01')) }

    context 'when the released date is on/past 2019-01-01' do
      it 'should be true' do
        expect(on_sale?(album)).to be true
      end
    end
  end
end

*2

Time.zone.parse('2019-01-01') >= 2019-01-01

false ( ´Д`)y━・~~

・・・・・

ペーペーの僕には全く理由が分かりませんでした。

そこで、困った時のbin/rails c です。

$ bin/rails c
pry(main)> t1 = Time.zone.parse('2019-01-01')
=> Mon, 01 Jan 2018 00:00:00 JST +09:00 >= Mon, 01 Jan 2018 00:09:00 JST +09:00
pry(main)> string_time = '2019-01-01'
=> "2019-01-01"
pry(main)> t1 >= string_time
=> false

t1ActiveSupport::TimeWithZone クラスで、t2String クラスな訳で、、、

この時の脳内は、

  • Ruby(Rails) でそもそもActiveSupport::TimeWithZoneString は比較できない!?
  • 比較できたとして、t2("2019-01-01") は何時なのか?String の場合00:00:00 じゃない!?

みたいな感じでした。

そこで、引き続きコンソール叩いてみました。

pry(main)> t2 = Time.zone.parse('2019-01-01 23:59:59')
pry(main)> t2 >= string_time
=> true
pry(main)> t3 = Time.zone.parse('2019-01-01 12:00:00')
pry(main)> t3 >= string_time
=> true
pry(main)> t4 = Time.zone.parse('2019-01-01 06:00:00')
pry(main)> t4 >= string_time
=> false
pry(main)> t5 = Time.zone.parse('2019-01-01 09:00:00')
pry(main)> t5 >= string_time
=> true 

この辺りで気がつきました。TimeZone の存在に、、、何やってんだよって感じですよね、本当に。

pry(main)> '2019-01-01'.to_time
=> 2019-01-01 00:00:00 +0000

やっぱり、 +0000 ってなってる。String の場合、UTC(協定世界時)で時間が設定されるようです。 一方、Time.zone.parse('2019-01-01') の方はJST で設定されています。 皆さんご存知のように、JSTUTC に比べて9時間早いので、今回の比較は実際には下記の通りです。

Mon, 01 Jan 2018 00:00:00 JST +09:00 >= Mon, 01 Jan 2018 00:09:00 JST +09:00

これでは、true になるはずがありませんね。

そこで、今回は下記の通り変更しました。

module TestHelper
  def on_sale?(album)
-    album.released_at >= '2019-01-01'
+    album.released_at >= Time.zone.parse('2019-01-01')
  end
end
require 'spec_helper'

describe TestHelper do
  describe '#on_sale?' do
    let!(:album) { create(:album, released_at: Time.zone.parse('2019-01-01')) }

    context 'when the released date is on/past 2019-01-01' do
      it 'should be true' do
        expect(on_sale?(album)).to be true
      end
    end
  end
end

この箇所に限らずJST を基本的に使っているので、今回のように急にUTC が出てきて予期せぬバグが出ないようにメインロジックをTime.zone.parse を用いて修正しました。

$ bundle exec rspec spec/helpers/test_helper.rb
.

Finished in 0.35198 seconds
1 example, 0 failures

無事テストもパスしました。( ´∀`)

ちなみに、このTimeZone ですがconfig/application.rb で設定できます。

class Application < Rails::Application
  ...
  config.time_zone = 'Tokyo' # ここ
  ...
end

参考:

api.rubyonrails.org

所感

  • テストを書くことって大事ですね。最近テストコードの重要性をひしひしと感じていますが、今回さらに強く思いました。プログラミング始めたての頃は動けばOK みたいな感じでやってましたが、今思えば恐ろしいことです。
  • Time.zone と何回もタイピングしていながら、TimeZone に気がつかなかったのはとても悔しいです。そして、このエントリーを書きながら自分へっぽこぶりにつくづく嫌になってきました。
  • とっても遠回りをしてしまったけれど、仮説検証のサイクルを回せたのはよかったかなぁと。Lv. 3 からLv. 4 くらいにはなったかなぁと。

*1:Timezone がJST の場合。

*2:大人の事情にてソースコードが実際のものとは異なります。