Ruby 2.7.0-preview1 での変更内容を試す

 今年のクリスマスにリリースされる予定の Ruby 2.7 の preview1 が 5/30 にリリースされました。

www.ruby-lang.org

プレビュー版は、年末の正式リリースに向け、新たな機能を試し、フィードバックを集めるために提供されています。

ということで、正式リリースまでに仕様が変更になる可能性もありますが、現状の変更内容で試せるものをざっと試してみました。

Pattern Matching

 今回の主要な変更点であるパターンマッチについては以前書いたものがありますので、そちらを参照いただければと思います。

blog.akanumahiroaki.com

REPL improvement

 Ruby の REPL である irb が大幅に進化しています。入力内容に応じてインクリメンタルにシンタックスハイライトが行われたり、複数行編集がサポートされ、かなり使いやすくなっています。

f:id:akanuma-hiroaki:20190609164650g:plain

メソッド参照演算子

 メソッドオブジェクトの参照演算子として新たに .: が追加されました。今までも method というメソッドがありましたが、それと同じように使える演算子になります。例えば下記のようなクラスがあるとします。

class Sample
  def initialize(name)
    @name = name
  end

  def name(name)
    @name = name
  end

  def self.class_method(value)
    puts "Class method called with value '#{value}'."
  end

  def instance_method(value)
    puts "Instance method in sample '#{@name}' called with value '#{value}'."
  end
end

 今までの method メソッドを使ってメソッドオブジェクトを取り出すには下記のように書きます。

s = Sample.new('test')
cm = Sample.method(:class_method)
im = s.method(:instance_method)

cm.call('hoge')
# => Class method called with value 'hoge'.

im.call('fuga')
# => Instance method in sample 'test' called with value 'fuga'.

s.name('sam')
im.call('fuga')
# => Instance method in sample 'sample' called with value 'fuga'.

 2行目、3行目を .: を使って書き直すと下記のような形になります。

cm = Sample.:class_method
im = s.:instance_method

 メソッド参照演算子については下記サイトで詳しく解説されていましたので参考にさせていただきました。

qiita.com

デフォルトのブロックの仮引数

 これまではブロックに渡される値にアクセスするための仮引数を定義する必要がありましたが、デフォルトで @1 という指定でアクセスすることができるようになります(Numbered parameters)。下記の例では array という配列の要素についてこれまでのやり方で仮引数 v を定義してアクセスするパターン(3行目)と、デフォルトの仮引数を使用するパターン(5行目)を示しています。

array = %w(a b c d e)
p array
array.each {|v| puts v }
puts '---'
array.each { puts @1 }

 実行してみると Numberd parameters を使った書き方でもこれまでと同様の出力が得られていることが確認できます。

["a", "b", "c", "d", "e"]
a
b
c
d
e
---
a
b
c
d
e

 Numbered parameters については下記サイトでも詳しく解説されていました。

tech.smarthr.jp

開始値省略範囲式

 配列等で取り出す範囲を指定する場合、終了値を省略して最後まで全てとすることはできましたが、開始値を省略して最初の値からとすることはできませんでした。2.7では開始値も省略することができるようになります。

array = %w(a b c d e)

p array[0..2]
# => ["a", "b", "c"]

p array[..2]
# => ["a", "b", "c"]

各要素の出現回数のカウント

 新たに Enumerable#tally というメソッドが追加され、配列等に含まれている各要素の数をカウントすることができるようになります。

array = %w(apple banana apple chocolate apple banana)
p array.tally
# => {"apple"=>3, "banana"=>2, "chocolate"=>1}

ブロックを伴わない Proc.new と proc が deprecated

 これまではブロックを渡すメソッドの中での下記4つの書き方は同じ挙動でしたが、ブロックを伴わない Proc.newproc が deprecated となり、 warning が出るようになっています。代わりにブロック変数 &block を使いましょう、という内容です。

def block_method(&block)
  Proc.new.call
  # => warning: Capturing the given block using Proc.new is deprecated; use `&block` instead
  # => A block is given to the method.

  proc.call
  # => warning: Capturing the given block using Proc.new is deprecated; use `&block` instead
  # => A block is given to the method.

  block.call
  # => A block is given to the method.

  yield
  # => A block is given to the method.
end

block_method { puts 'A block is given to the method.' }

ブロックを伴わない lambda はエラー

 ブロックを渡すメソッドの中でブロックを伴わない lambda の利用はエラーになるようになりました。例えば下記のようなコードを書いてみます。

def block_method(&block)
  lambda
  yield
end

block_method { puts 'A block is given to the method.' }

 2.6.3 では warning は出るものの実行されていました。

lambda_sample.rb:2: warning: tried to create Proc object without a block
A block is given to the method.

 2.7 では下記のようにエラーになるようになっています。

Traceback (most recent call last):
        2: from lambda_sample.rb:6:in `<main>'
        1: from lambda_sample.rb:2:in `block_method'
lambda_sample.rb:2:in `lambda': tried to create Proc object without a block (ArgumentError)

新元号「令和」を表す合字 U+32FF サポート

 Unicode のバージョンが 12.1.0 になり、令和を表す合字が追加になっています。

irb(main):001:0> RUBY_VERSION
=> "2.7.0"
irb(main):002:0> "\u32FF"
=> ""

 この変更は 2.6 にもバックポートされているようで、 2.6.3 でも同じように使用できました。

bugs.ruby-lang.org

irb(main):001:0> RUBY_VERSION
=> "2.6.3"
irb(main):002:0> "\u32FF"
=> ""

 2.5.5 では下記のように使用できません。

irb(main):001:0> RUBY_VERSION
=> "2.5.5"
irb(main):002:0> "\u32FF"
=> "\u32FF"

Date.jisx0301, Date#jisx0301, および Date.parse で非公式に新元号に仮対応

  JIS X 0301 の新しい版で正式な仕様が決定されるまでの暫定的なものとのことですが、 Date.jisx0301, Date#jisx0301, および Date.parse で非公式に新元号に仮対応されました。下記のように 2019年5月1日は令和 R として表示されます。

irb(main):001:0> RUBY_VERSION
=> "2.7.0"
irb(main):002:0> Date.new(2019, 5, 1).jisx0301
=> "R01.05.01"
irb(main):003:0> Date.parse('2019-05-01').jisx0301
=> "R01.05.01"

 この変更も 2.6 にバックポートされているようで、同様に使用することができます。

irb(main):001:0> RUBY_VERSION
=> "2.6.3"
irb(main):002:0> require 'date'
=> true
irb(main):003:0> Date.new(2019, 5, 1).jisx0301
=> "R01.05.01"

 2.5.5 では対応されておらず、平成 H と表示されます。

irb(main):001:0> RUBY_VERSION
=> "2.5.5"
irb(main):002:0> require 'date'
=> true
irb(main):003:0> Date.new(2019, 5, 1).jisx0301
=> "H31.05.01"

そのほかの変更点

 今回は実際には確認しませんでしたが、他にも下記のような点が変更になっているということです。JIT については Rails との組み合わせだと逆に遅くなるということもあるようですが、今後のそれぞれの動きにも注目したいです。

  • 断片化したメモリをデフラグするCompaction GCが導入されました。

  • パフォーマンスの改善: JIT

  • Unicode および Emoji のバージョンが 11.0.0 から 12.0.0 になりました。

  • Ruby のビルドに C99 に対応したコンパイラが必要になりました。

まとめ

 2.7.0 の preview1 ということで、今後 preview2、 preview3、 Release Candidate を経て年末にリリースされることになります。今回は本当に簡単に試した程度で、今のところは 2.6 からバージョンアップするとしても大きな変更は必要なさそうに思っていますが、リリースまでの変更内容をキャッチアップして出来るだけスムーズに既存の環境をバージョンアップできるようにして、新たに導入された便利な機能は早く使っていけるようにしたいと思います。

Ruby 2.7 で導入される予定の Pattern Matching を触ってみる

 先日福岡で開催された RubyKaigi 2019 に参加してきました。下記の辻本さんのセッションの中で Ruby 2.7 で導入される予定のパターンマッチングについての紹介があったので、セッションの資料に沿って触ってみました。

Pattern matching - New feature in Ruby 2.7 - RubyKaigi 2019

 セッションの資料は SpeakerDeck にアップロードされているのでそちらを参照させていただきました。

speakerdeck.com

 今回使用しているサンプルコードは基本的には上記の資料内のサンプルをそのまま実行しているか、若干変更したものを使用しています。

パターンマッチングとは

 パターンマッチングについてのRubyist向けの説明としては、 case/when + multiple assignment という感じになり、正規表現とかではなくオブジェクトの構造のパターンのマッチングです。下記 issue にて開発が進められています。

bugs.ruby-lang.org

演算子

 パターンマッチング用の演算子としては case が拡張され、 case/when だけでなく case/in が追加されています。

case [0, [1, 2, 3]]
in [a, [b, *c]]
  p a #=> 0
  p b #=> 1
  p c #=> [2, 3]
end

 ちなみに上記のようにパターンマッチングを使用すると、現状は開発中の機能であり、将来的に仕様が変更になるかもしれないということで、下記のように warning が表示されます。

warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!

 multiple assignment との違いとして、オブジェクトの構造もチェックされ、マッチした部分の変数に値が代入されます。

case [0, [1, 2, 3]]
in [a]
  :unreachable # 配列構造が一致しない
in [0, [a, 2, b]]
  p a #=> 1
  p b #=> 3
end

 対象のオブジェクトとして Hash もサポートされています。

case {a: 0, b: 1}
in {a: 0, x: 1}
  :unreachable # b というキーにマッチしない
in {a: 0, b: var}
  p var #=> 1
end

 JSON形式のデータを扱う際に、期待した構造になっているかチェックし、なっていた場合には値を取り出すという使い方をする時に便利です。

require 'json'

json = '{
  "name": "Alice",
  "age": 30,
  "children": [
    {
      "name": "Bob",
      "age": 2
    }
  ]
}
'

case JSON.parse(json, symbolize_names: true)
in {name: "Alice", children: [{name: "Bob", age: age}]}
  p age #=> 2
end

 上記の case 文と同様のことをパターンマッチングなしでやろうとすると、下記のように書く必要があります。

person = JSON.parse(json, symbolize_names: true)
if person[:name] == "Alice"
  children = person[:children]
  if children.length == 1 && children[0][:name] == "Bob"
    p children[0][:age] #=> 2
  end
end

仕様 (2019.4.20時点)

 確認した 2019/4/20 時点での仕様としては下記のようになっています。

case obj
in pat [if|unless cond]
  ...
in pat [if|unless cond]
  ...
else
  ...
end
  • 条件にマッチするまでシーケンシャルに評価が行われる
  • マッチする条件がなければ else 句が実行される
  • マッチする条件がなく、 else 句も定義されていない場合、 NoMatchingPatternError が raise される

ガード条件も使用可能で、 in 句の条件にマッチした場合にガード条件が評価されます。

case [0, 1]
in [a, b] unless a == b
  :reachable # まず [a, b] のパターンにマッチするか評価され、そのあとで  a == b かどうかが評価される
end

パターン

 現状パターンとしては下記の6パターンがあります。

  • Value pattern
  • Variable pattern
  • Alternative pattern
  • As pattern
  • Array pattern
  • Hash pattern

 それぞれを簡単に見ていきます。

Value パターン

 pattern と object を比較し、 pattern === object であればマッチします。

pat: literal
   | Constant

 下記の in 句はいずれもマッチします。

case 0
in 0
in -1..1
in Integer
end

Variable パターン

 任意の値とマッチし、変数にその値を代入します。

pat: var
case 0
in a
  p a #=> 0
end

 変数に代入する必要がない場合はアンダースコアを使うことができます。

case [0, 1]
in [_, _]
  :reachable
end

 case/in の外側に同じ変数名が使われていても、上書きしてアサインします。

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

 既存の変数の値をパターンマッチに使いたいときは、 ^ を使用します。

a = 0
case 1
in ^a # 'in 0' という意味になる
  :unreachable
end #=> NoMatchingPatternError が raise される

Alternative パターン

 OR条件でのパターンマッチです。

pat: pat | pat | ...
case 1
in 0 | 1 | 2 # 0, 1, 2 のいずれかにマッチすれば真
  p :reachable #=> :reachable
end

As パターン

 値がパターンにマッチした時に、値を変数に格納します。

pat: pat => pat
case 0
in Integer => a # 値がIntegerだったら a に格納する
  p a #=> 0
end

 複雑なオブジェクトのパターンマッチで、そのオブジェクトの一部を取り出すのに便利です。

case [0, [1, 2]]
in [0, [1, _] => a]
  p a #=> [1, 2]
end

Array パターン

 配列オブジェクトに対してのマッチングです。アスタリスク * で複数の要素にマッチします。下記の in句はいずれもマッチします。

case [0, 1, 2, 3]
in Array(0, *a, 3)
in Object[0, *a, 3]
in [0, *a, 3]
in 0, *a, 3
end

p a #=> [1, 2]

 Struct に対してのマッチングも可能です。

class Struct
  alias deconstruct to_a
end

Color = Struct.new(:r, :g, :b)
p Color[0, 10, 20].deconstruct #=> [0, 10, 20]

color = Color.new(255, 0, 0)

case color
in Color[0, 0, 0]
  puts "Black"
in Color[255, 0, 0]
  puts "Red" #=> Red
in Color[r, g, b]
  puts "#{r}, #{g}, #{b}"
end

 下記は RubyVM::AbstractSyntaxTree::Node を使った Array パターンの例です。RubyVM::AbstractSyntaxTree を使った Power Assert の実装サンプルになっています。

class RubyVM::AbstractSyntaxTree::Node
  def deconstruct
    [type, *children, [first_lineno, first_column, last_lineno, last_column]]
  end
end

ast = RubyVM::AbstractSyntaxTree.parse('1 + 1')
p ast.type #=> :SCOPE
p ast.children #=> [[], nil, #<RubyVM::AbstractSyntaxTree::Node:OPCALL@1:0-1:5>]
p ast.deconstruct #=> [:SCOPE, [], nil, #<RubyVM::AbstractSyntaxTree::Node:OPCALL@1:0-1:5>, [1, 0, 1, 5]]

node = RubyVM::AbstractSyntaxTree.parse('assert { 3.times.to_a.include?(3) }')
pp node #=> (SCOPE@1:0-1:35
        #    tbl: []
        #    args: nil
        #    body:
        #      (ITER@1:0-1:35 (FCALL@1:0-1:6 :assert nil)
        #         (SCOPE@1:7-1:35
        #          tbl: []
        #          args: nil
        #          body:
        #            (CALL@1:9-1:33
        #               (CALL@1:9-1:21 (CALL@1:9-1:16 (LIT@1:9-1:10 3) :times nil) :to_a
        #                  nil) :include? (ARRAY@1:31-1:32 (LIT@1:31-1:32 3) nil)))))

case node
in :SCOPE, _, _, [:ITER, [:FCALL, :assert, _, _], body, _], _
  pp body #=> (SCOPE@1:7-1:35
          #    tbl: []
          #    args: nil
          #    body:
          #      (CALL@1:9-1:33
          #         (CALL@1:9-1:21 (CALL@1:9-1:16 (LIT@1:9-1:10 3) :times nil) :to_a nil)
          #         :include? (ARRAY@1:31-1:32 (LIT@1:31-1:32 3) nil)))
end

Hash パターン

 Hash パターンと言っていますが Hash オブジェクト以外にも使われるパターンです。下記条件を満たすとマッチします。

  • Constant === object が true を返す
  • object が Hash を返す #deconstruct_keys メソッドを持っている
  • object.deconstruct_keys(keys) への nested pattern の適用結果が true を返す

 Hash オブジェクトでのパターンマッチング例は下記の通りです。下記の in句は全てマッチします。

class Hash
  def deconstruct_keys(keys)
    self
  end
end

case {a: 0, b: 1}
in Hash(a: a, b: 1)
in Object[a: a]
in {a: a}
in {a: a, **rest}
  p rest #=> {:b=>1}
end

 中括弧は省略可能です。また、 a:a: a のシンタックスシュガーです。

case {a: 0, b: 1}
in a:, b:
  p a #=> 0
  p b #=> 1
end

 keys は効率的な実装のためのヒントになる情報を提供します。また #deconstruct_keys メソッドの見当違いな実装は非効率的な結果となることがあります。

class Time
  def deconstruct_keys(keys)
    {
      year: year, month: month,
      asctime: asctime, ctime: ctime,
      yday: yday, zone: zone
    }
  end
end

case Time.now
in year:
  p year #=> 2019
end

 keys はパターンの中で指定された key を含む配列を参照します。 keys に含まれない key は無視することができます。もしパターンの中で **rest が指定されている場合、代わりに nil が渡されます。その場合には全てのキーバリューペアを返さなければなりません。

class Time
  VALID_KEYS = %i(year month asctime, ctime, yday, zone)

  def deconstruct_keys(keys)
    if keys
      (VALID_KEYS & keys).each_with_object({}) do |k, h|
        h[k] = send(k)
      end
    else
      {
        year: year, month: month,
        asctime: asctime, ctime: ctime,
        yday: yday, zone: zone
      }
    end
  end
end

now = Time.now
case now
in year: # now.deconstruct_keys([:year]) が呼ばれ、{year: 2019} が返される
  p year #=> 2019
end

まとめ

 今回はとりあえず紹介されているサンプルで、 Ruby2.7 で実装される予定の Pattern Matching を触ってみました。仕様はまだ変わる可能性がありますが、 JSON オブジェクトに対してのマッチングは直感的に指定することができそうで、かつ値の取り出しも同時にできるということで、便利に使えそうです。 RubyKaigi をきっかけに、こうした新しい機能もできるだけ追いかけていきたいと思います。

rails server と rackup の違い

 前回の記事でミニマムな Rack アプリケーションを動かしてみました。 Rails も Rack ベースのアプリケーションですが、通常は rails server コマンドでアプリケーションを起動します。 Rails のアプリでも config.ru は持っているので、 rackup でも起動できるはずですが、 rails server コマンドで起動するのと何が違うのかを少し調べてみました。

 ちなみに前回の記事はこちらです。

blog.akanumahiroaki.com

まずはそれぞれで動かしてみる

 とりあえず実際に動かしてみてその違いを見てみます。まずは rails server コマンドで起動してみます。

$ rails s
=> Booting Puma
=> Rails 5.2.2 application starting in development 
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.0 (ruby 2.6.0-p0), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:8080
Use Ctrl-C to stop

 tcp://localhost:8080 でアプリケーションが起動しますので、こちらにブラウザ等でアクセスすると下記のようなログが出力されます。

Started GET "/" for 220.211.52.113 at 2019-04-14 13:32:54 +0000
Cannot render console from 220.211.52.113! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by Rails::WelcomeController#index as HTML
  Rendering /home/ec2-user/.rbenv/versions/2.6.0/lib/ruby/gems/2.6.0/gems/railties-5.2.2/lib/rails/templates/rails/welcome/index.html.erb
  Rendered /home/ec2-user/.rbenv/versions/2.6.0/lib/ruby/gems/2.6.0/gems/railties-5.2.2/lib/rails/templates/rails/welcome/index.html.erb (2.2ms)
Completed 200 OK in 18ms (Views: 6.2ms | ActiveRecord: 0.0ms)

 今度は rackup で起動してみます。

$ rackup
Puma starting in single mode...
* Version 3.12.0 (ruby 2.6.0-p0), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:9292
Use Ctrl-C to stop

 rails server で起動した時と比べると、最初の3行が出力されていません。また、ポートも 8080 ではなく 9292 で起動されています。

 tcp://localhost:9292 にアクセスするとログは下記のような出力になります。

127.0.0.1 - - [14/Apr/2019:13:36:22 +0000] "GET / HTTP/1.1" 200 - 0.1098

 シンプルな Web サーバのログが出力されるだけで、 rails server の時に出力されていた Rails に関するログは出力されていません。

処理内容の差分

 それでは起動時に実際にどのような処理が行われているのか、コードから確認してみたいと思います。下記 Rails ガイドのページで初期化プロセスが紹介されていますので、これを参考に確認してみます。

railsguides.jp

 rails server コマンドで起動した場合も最終的に config.ru から Rack ベースの Rails アプリケーションが起動されることになるのですが、 rackup と違って Rails の環境を色々整えた上でアプリケーションが起動されることになります。

 詳細なステップについては上記サイトで紹介されているので、ここで一つ一つの紹介は割愛しますが、主に違う点は下記の点かと思います。

  • Bundler の読み込みと設定が行われる

  • rails コマンドの別名の拡張が行われる

  • PORT番号などの設定が行われる

  • Logger の設定が行われる

 上記が行われた上で config.ru から Rails アプリケーションが起動します。それぞれについて簡単に見てみたいと思います。

Bundler の読み込みと設定

 起動シーケンスの最初の方で、 config/boot.rb が実行されます。内容は下記のようになっています。

# config/boot.rb

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)

require 'bundler/setup' # Set up gems listed in the Gemfile.

 これによって Gemfile のパスが取得され、 bundler/setup が require されています。

rails コマンドの別名の拡張

 続いて railties/lib/rails/commands.rb の内容が実行され、 rails コマンドの別名の拡張が行われます。これによって例えば rails srails server として解釈できるようになります。ここまではまだ rails コマンドの内容になります。

# railties/lib/rails/commands.rb

# frozen_string_literal: true

require "rails/command"

aliases = {
  "g"  => "generate",
  "d"  => "destroy",
  "c"  => "console",
  "s"  => "server",
  "db" => "dbconsole",
  "r"  => "runner",
  "t"  => "test"
}

command = ARGV.shift
command = aliases[command] || command

Rails::Command.invoke command, ARGV

PORT 番号などの設定

 ここからは rails server コマンドの内容になり、railties/lib/rails/commands/server/server_command.rb 内で Rack::Server を継承した Rails::Server が定義されています。 Rails::Server は初期化時にオプションを受け取るようになっていて、この中で PORT 番号などの設定が行われています。

module Rails
  class Server < ::Rack::Server
    class Options
      def parse!(args)
        Rails::Command::ServerCommand.new([], args).server_options
      end
    end

    def initialize(options = nil)
      @default_options = options || {}
      super(@default_options)
      set_environment
    end

 rails server コマンドでは Rails::Server を初期化する際にオプションを渡しています。 rackup で起動するとデフォルトでは 9292 番ですが、ここではデフォルトが 3000 番ポートとして設定され、さらに今回のケースでは PORT 環境変数で 8080 番ポートが指定されているので、起動時には 8080 番ポートが使用されます。

  module Command
    class ServerCommand < Base # :nodoc:
      include EnvironmentArgument

      # Hard-coding a bunch of handlers here as we don't have a public way of
      # querying them from the Rack::Handler registry.
      RACK_SERVERS = %w(cgi fastcgi webrick lsws scgi thin puma unicorn)

      DEFAULT_PORT = 3000
      DEFAULT_PID_PATH = "tmp/pids/server.pid"
      def perform
        extract_environment_option_from_argument
        set_application_directory!
        prepare_restart

        Rails::Server.new(server_options).tap do |server|
          # Require application after server sets environment to propagate
          # the --environment option.
          require APP_PATH
          Dir.chdir(Rails.application.root)

          if server.serveable?
            print_boot_information(server.server, server.served_url)
            after_stop_callback = -> { say "Exiting" unless options[:daemon] }
            server.start(after_stop_callback)
          else
            say rack_server_suggestion(using)
          end
        end
      end
$ echo $PORT
8080

Logger の設定

 続けて同じく Rails::Server クラスの log_to_stdout メソッドで ActiveSupport::Logger のインスタンスの作成とアサインが行われます。これによって Web サーバのログだけでなく Rails に関係するログが出力されるようになります。

      def log_to_stdout
        wrapped_app # touch the app so the logger is set up

        console = ActiveSupport::Logger.new(STDOUT)
        console.formatter = Rails.logger.formatter
        console.level = Rails.logger.level

        unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT)
          Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
        end
      end

まとめ

 rails serverrackup の違いは私も今までよくわかっていなかったので、今回整理できて参考になりました。今回は差分のところだけに絞ったので、 Rails アプリの起動シーケンスについてはざっくり割愛してしまいましたが、今後時間があったら Rack ミドルウェアの関連や、実際にリクエストがあった時の挙動なども見てみたいと思います。

Rack をシンプルに動かしてみる

 前回の記事で Roda というフレームワークのことを書きましたが、その時に久々に Rack について意識したので、今回は改めて Rack の基本的なことを再確認してみます。

 ちなみに前回の記事はこちらです。

blog.akanumahiroaki.com

Rack とは

 Rack の簡単な説明としては、 Ruby をサポートする Web サーバと、 Ruby の Web フレームワークの間のインターフェースを提供するためのものです。 Web フレームワークを開発する場合、開発者はそれぞれのサーバに対するハンドラを書かないといけませんが、 Rack ベースの構成にしておけば、 Rack をサポートするサーバのハンドラは自前で用意しなくてもよくなるので、効率的にフレームワークを開発することができるようになります(と言っても私自身はフレームワーク開発したことはまだありませんが。。)。

rack.github.io

 Rack の GitHub リポジトリはこちらです。

github.com

 前回の記事で書いた Roda や、有名どころでは Ruby on Rails や Sinatra も Rack ベースのフレームワークになります。

Rack のインストール

 まずはとにかく動かしてみたいということで、ごく簡単に Rack を動かしてみたいと思います。ベースになる環境としては Ruby と gem だけ使えるようにしてあって、 Rails や Rack はまだインストールしていない状態です。

$ ruby -v
ruby 2.6.2p47 (2019-03-13 revision 67232) [x86_64-linux]
$ 
$ gem -v
3.0.3
$ 
$ rails -v
bash: rails: command not found
$ 
$ rackup
bash: rackup: command not found

 Rack は gem でインストールできるようになっています。今回は Gemfile に記述して Bundler で管理してみます。下記のような Gemfile を用意します。

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "rack"

 そして bundle install します。

$ bundle install
Fetching gem metadata from https://rubygems.org/..............
Resolving dependencies...
Using bundler 1.17.2
Fetching rack 2.0.7
Installing rack 2.0.7
Bundle complete! 1 Gemfile dependency, 2 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

 Rack の gem がインストールされ、下記のように情報が確認できます。

$ bundle info rack
  * rack (2.0.7)
        Summary: a modular Ruby webserver interface
        Homepage: https://rack.github.io/
        Path: /home/ec2-user/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/rack-2.0.7

 rackup コマンドも使えるようになっています。

$ rackup --version
Rack 1.3 (Release: 2.0.7)

Rack を簡単に動かしてみる

 公式サイトでも紹介されている、ごくシンプルな例で Rack を動かしてみたいと思います。まずは下記のように my_rack_app.rb というファイルを用意します。

# my_rack_app.rb

require 'rack'

app = Proc.new do |env|
  ['200', { 'Content-Type' => 'text/html' }, ['A barebones rack app.']]
end

Rack::Handler::WEBrick.run app

 Rack で動くアプリケーションの条件としては、 call メソッドを持つ Ruby オブジェクトを用意することで、 call メソッドのレスポンスとして、 HTTP レスポンスコード、 HTTP レスポンスヘッダのハッシュ、レスポンスボディの3つを含む Array を返す必要があります。 Ruby の Proc は call が呼ばれることで中身が評価されるので、上記の例では Proc の内容としてレスポンスで返すべき Array を固定値で定義しています。

 Rack アプリケーションを動かすにはサーバのハンドラにアプリケーションのオプジェクトを渡します。 Rack には Ruby に標準で組み込まれている WEBrick のハンドラが含まれているので、その run メソッドに先ほどの Proc のオブジェクトを渡します。

 このファイルを Ruby スクリプトとして実行すると、下記のように WEBrick のサーバが起動します。デフォルトでは 8080 番ポートで起動します。

$ ruby my_rack_app.rb 
[2019-04-06 08:28:55] INFO  WEBrick 1.4.2
[2019-04-06 08:28:55] INFO  ruby 2.6.2 (2019-03-13) [x86_64-linux]
[2019-04-06 08:28:55] INFO  WEBrick::HTTPServer#start: pid=19549 port=8080

 curl でリクエストを投げると下記のように、 Proc で定義したレスポンスボディの内容がレスポンスとして返ってきます。

$ curl localhost:8080
A barebones rack app.

 サーバのログとしては下記のような出力があります。

127.0.0.1 - - [06/Apr/2019:08:30:57 UTC] "GET / HTTP/1.1" 200 21
- -> /

 この例ではどのパスにリクエストをしても同じ内容のレスポンスになります。

$ curl localhost:8080/
A barebones rack app. 
$ curl localhost:8080/hello
A barebones rack app.
$ curl localhost:8080/hello/world
A barebones rack app.
127.0.0.1 - - [06/Apr/2019:08:33:00 UTC] "GET / HTTP/1.1" 200 21
- -> /
127.0.0.1 - - [06/Apr/2019:08:33:03 UTC] "GET /hello HTTP/1.1" 200 21
- -> /hello
127.0.0.1 - - [06/Apr/2019:08:33:08 UTC] "GET /hello/world HTTP/1.1" 200 21
- -> /hello/world

rackup コマンドで動かす

 先ほどは Ruby スクリプトを動かす形で Rack アプリを実行しましたが、もう一つの方法として、 rackup コマンドでアプリケーションを動かす方法があります。そのためにはまず下記のような内容で config.ru ファイルを用意します。

# config.ru

run Proc.new { |env| ['200', { 'Content-Type' => 'text/html' }, ['get rack\'d'] ] }

 Ruby スクリプトで動かした場合と違い、 rackup コマンドで実行する場合はその環境を自動的に認識してくれます。設定ファイルの名前は config.ru ではなくても良いのですが、 rackup コマンドで引数として設定ファイルを指定しない場合はデフォルトで config.ru が使用されます。また、別名にする場合も拡張子は *.ru としておくと、 rackup の設定ファイルとして扱われるので、特別な理由がない場合はこのルールに乗っておくのが良いと思います。

 また、サーバとしては Thin や Puma がインストールされている場合は WEBrick よりも優先してそちらが使われます。インストールされていない場合は WEBrick が使われます。

 config.ru が置かれているディレクトリで rackup コマンドを実行すると、下記のようにサーバが起動します。 rackup の場合はデフォルトのポートとして 9292 番ポートが使用されます。

$ rackup
[2019-04-06 08:41:40] INFO  WEBrick 1.4.2
[2019-04-06 08:41:40] INFO  ruby 2.6.2 (2019-03-13) [x86_64-linux]
[2019-04-06 08:41:40] INFO  WEBrick::HTTPServer#start: pid=20113 port=9292

 curl でリクエストを投げると下記のようにレスポンスが返ります。

$ curl localhost:9292
get rack'd

 サーバのログとしては下記のような出力になります。

127.0.0.1 - - [06/Apr/2019:08:44:05 +0000] "GET / HTTP/1.1" 200 - 0.0003

 ちなみに Ruby on Rails でも config.ru が用意されていて、下記のような内容になっています。 Rails の挙動についてはまた別途追ってみたいと思います。

# This file is used by Rack-based servers to start the application.

require_relative 'config/environment'

run Rails.application

まとめ

 今まで Rack を直接触って動かすということはしてなかったのですが、 Rack ベースのフレームワークを使う上でも Rack がどういうものかわかっていた方がイメージがわきやすいと思うので、シンプルに動かしてみるというのは良いと思います。今回はとりあえず動かしてみた程度でしたが、もう少し中身の詳細も追ってみて、 Rails 等での挙動についても見て行きたいと思います。

Routing Tree Web Toolkit "Roda" を動かしてみる

 先日開催された Railsdm はチケットが即完売してしまって残念ながら行けませんでした。

railsdm.github.io

 何か試してブログに書けそうなネタはないかなーとセッションの資料を見ていたら Roda という Web Framework が紹介されていました。

docs.google.com

 セッションでは Rails と組み合わせて使ってみるという内容でしたが、シンプルに Roda を動かしてみるサンプルはあまり多くなかったようなので、試してみました。

Roda とは

 詳しくは本家サイトや上記 Railsdm のセッション資料等をご覧いただければと思いますが、 Rails のようなフルスタックのフレームワークとは違い、ルーティング機能を提供するシンプルなフレームワークで、 Sinatra に近いイメージです。様々なプラグインが用意されていますので、必要に応じてプラグインを追加して機能追加していく形になります。ルーティングをツリー構造で定義できる点が Sinatra と違う点になります。

roda.jeremyevans.net

Roda のインストール

 Roda の GitHub リポジトリはこちらで、 gem のインストール方法やシンプルなアプリケーションのサンプルが紹介されています。

github.com

 Roda のインストールは gem をインストールするだけです。今回は Bundler を使ってインストールしてみますので、下記のように Gemfile を用意します。

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "roda"

 そして bundle install します。

$ bundle install
Fetching gem metadata from https://rubygems.org/...............
Resolving dependencies...
Using bundler 1.17.2
Using rack 2.0.6
Fetching roda 3.18.0
Installing roda 3.18.0
Bundle complete! 1 Gemfile dependency, 3 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

サンプルアプリケーション

 とりあえず GitHub で紹介されているサンプルアプリケーションを実装してみます。下記の内容で config.ru ファイルを用意します。

require "roda"

class App < Roda
  route do |r|
    # GET / request
    r.root do
      r.redirect "/hello"
    end

    # /hello branch
    r.on "hello" do
      # Set variable for all routes in /hello branch
      @greeting = 'Hello'

      # GET /hello/world request
      r.get "world" do
        "#{@greeting} world!"
      end

      # /hello request
      r.is do
        # GET /hello request
        r.get do
          "#{@greeting}!"
        end

        # POST /hello request
        r.post do
          puts "Someone said #{@greeting}!"
          r.redirect
        end
      end
    end
  end
end

run App.freeze.app

動作確認

 Roda は Rack ベースなので、動かすには rackup するだけです。今回は環境としては Cloud9 で EC2 インスタンス上で動作させています。

$ rackup
Puma starting in single mode...
* Version 3.12.0 (ruby 2.6.0-p0), codename: Llamas in Pajamas
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://localhost:9292
Use Ctrl-C to stop

 9292ポートでアプリケーションが実行されるので、 curl でリクエストを投げてみると、下記のようにレスポンスが返ります。

$ curl localhost:9292/hello
Hello!

 ログには下記のように出力されます。

127.0.0.1 - - [30/Mar/2019:12:55:59 +0000] "GET /hello HTTP/1.1" 200 6 0.0005

サンプルアプリの内容

 Roda ではリクエストのルーティングはルーティングツリーと呼ばれる構成で管理されています。 config.ru で定義した内容がルーティングツリーになりますので、その設定内容を簡単に説明します。

 まず r.root はルートパス / への GET リクエストのみマッチします。

# GET / request
r.root do
  r.redirect '/hello'
end

 curl では下記のようにリクエストを投げます。

$ curl localhost:9292/
127.0.0.1 - - [30/Mar/2019:13:09:27 +0000] "GET / HTTP/1.1" 302 - 0.0011

 /hello というリクエストについては下記のブロックで定義しています。

# /hello branch
r.on 'hello' do
 ・・・
end

 その中でさらに下層のパスへの設定も行います。 /hello/world への GET リクエストについては下記のように定義します。

r.get 'world' do
  "#{@greeting} world!"
end

 リクエストを投げると下記のように動作します。

$ curl localhost:9292/hello/world
Hello world!
127.0.0.1 - - [30/Mar/2019:13:22:26 +0000] "GET /hello/world HTTP/1.1" 200 12 0.0013

 /hello の下層のパスがないリクエストの場合は下記の設定がマッチします。

# /hello request
r.is do
 ・・・
end

 /hello に GET リクエストが投げられた場合の設定は下記の内容になります。

r.get do
  "#{@greeting}!"
end
$ curl localhost:9292/hello
Hello!
127.0.0.1 - - [30/Mar/2019:13:28:37 +0000] "GET /hello HTTP/1.1" 200 6 0.0014

 また、 POST リクエストの場合は下記のブロックの設定になります。

# POST /hello request
r.post do
  puts "Someone said #{@greeting}!"
  r.redirect
end
$ curl -X POST localhost:9292/hello
Someone said Hello!
127.0.0.1 - - [30/Mar/2019:13:30:27 +0000] "POST /hello HTTP/1.1" 302 - 0.0013

まとめ

 今回はとりあえずサンプルをそのまま動かしただけでしたが、公式サイトには他にも様々な Matcher の書き方が紹介されています。 Rails を使っていると基本的な技術要素というよりは "Railsの使い方" に視点が行きがちだと思いますが、こういったコア機能のみ提供しているシンプルなフレームワークをベースにアプリケーションを作っていくとなると、 Rack や各個別の技術要素を改めて意識するきっかけになりそうで良いですね。

M5Stack用非接触温度センサユニットを試す

 最近は M5Stack のセンサーユニットをいろいろ試している感じになってますが、今回は非接触温度センサを試してみました。何かアイディアがあるわけではないのですが、いろいろと応用範囲が広そうかなと思っています。

www.switch-science.com

f:id:akanuma-hiroaki:20190323150151j:plain

M5Stack への接続

 ユニットの裏面を見ると、 PORT.A.I2C ということで、 M5Stack の Grove コネクタの A ポートに接続すれば良いということがわかります。

f:id:akanuma-hiroaki:20190323145931j:plain

 なので付属の Grove ケーブルを使って M5Stack 本体左側の Grove コネクタに接続します。接続はこれだけで OK です。

f:id:akanuma-hiroaki:20190323150021j:plain

UIFlow でのサンプル実装

 UIFlow でサンプルを実装してみます。まずはユニットを追加するために、エミュレータ下の ボタンをクリックします。

f:id:akanuma-hiroaki:20190323145344p:plain

 ユニットのリストから、 NCIR を選択し、ポート選択のプルダウンからは A を選択して OK をクリックします。

f:id:akanuma-hiroaki:20190323145453p:plain

 すると UIFlow に NCIR ユニットが追加されます。

f:id:akanuma-hiroaki:20190323145540p:plain

 Units メニューには NCIR メニューが追加され、センサーからの値を読み取るためのブロックが使用可能になります。

f:id:akanuma-hiroaki:20190323145630p:plain

 このブロックを使ってサンプルを実装してみます。やってることはシンプルで、1秒ごとにセンサーの値を画面に表示するだけです。

f:id:akanuma-hiroaki:20190323145718p:plain

動作確認

 これを実機で動作させると、下記写真のようにセンサーで取得した温度情報が画面に表示されます。

f:id:akanuma-hiroaki:20190323145818j:plain

 試しにコーヒーの温度を測ってみると下記のような感じになりました。

f:id:akanuma-hiroaki:20190323145846j:plain:w400

動体検知の動作確認

 先ほどのサンプルの待ち時間を 1秒間 から 0.1秒間 に変更すると、センサー前を人が横切った時などに検知できるようになります。動作確認の様子は下記の通りで、人の身体がセンサー前を横切った時に検知温度が高くなります。

まとめ

 温度情報を取得するところまでは、センサーの接続から含めて超お手軽にできてしまいました。温度情報をそのまま使うだけでなく、動体検知もできるので、使用用途を考えてみたいと思います。

M5Stack の Color Sensor を UIFlow で試す

 M5Stack の拡張ユニットには色々なものがありますが、なんとなく面白そうだったので物体の色情報を取得できる Color Sensor を試してみました。ただ、先に結論を言っておくと、センサーの値を読み出すまではすごく簡単にできたのですが、そのまま値を使っても対象の色を正しく再現するところまではできませんでした。ひとまずやったことを一通り描いてみたいと思います。

www.switch-science.com

 公式のドキュメントはこちらにあります。

docs.m5stack.com

ユニット接続

 Color Sensor も他の拡張ユニットと同様に Grove コネクタで接続できますので、 M5Stack 本体左側の Grove コネクタに接続します。

f:id:akanuma-hiroaki:20190316210620j:plain

 センサー裏面にはピンアウトが記載されています。また、 PORT. A. I2C という記載もあります。私が持っている M5Stack Gray は Grove コネクタは一つだけで、 A PORT のみなので、 PORT A(I2C)用の拡張ユニットが接続できます。

f:id:akanuma-hiroaki:20190316210700j:plain

センサーの値を読む

 Color Sensor は UIFlow も対応しているので、今回は UIFlow で手軽に試してみます。ユニットを追加するために、エミュレータ下の ボタンをクリックします。

f:id:akanuma-hiroaki:20190316204437p:plain

 ユニットのリストの中から COLOR を選択し、プルダウンからは A PORT して OK をクリックします。

f:id:akanuma-hiroaki:20190316204634p:plain

 すると Color Sensor ユニットが UI Flow に追加されます。

f:id:akanuma-hiroaki:20190316204822p:plain

 Units メニューには Color メニューが追加され、 Color Sensor 用のブロックが使用できるようになります。現状確認できたのは RGB それぞれの値を読み取るブロックです。

f:id:akanuma-hiroaki:20190316204930p:plain

 上記のブロックを使用して簡単なサンプルを作成してみます。1秒ごとに Color Sensor の RGB 情報を読み出し、画面に表示します。

f:id:akanuma-hiroaki:20190316205132p:plain

動作確認

 実際に動作させてみると、下記のように Color Sensor から読み取った RGB の値が画面に表示されます。

f:id:akanuma-hiroaki:20190316205429j:plain

 動作中は色判別のための白色 LED が点灯します。

f:id:akanuma-hiroaki:20190316210726j:plain

 そして実際に読んだ値が正しいのかの確認にためにも、読み取った RGB の値で図形の色を変えてみるサンプルを下記のように作ってみたのですが、結論から言うと実際の色は正しく反映されませんでした。

f:id:akanuma-hiroaki:20190316220940p:plain

 例えば下記写真の実行例だと、対象のものは濃い黄色なのですが、読み取った RGB をそのまま使用すると茶色の表示になってしまいました。下記数字をそのまま合成すると茶色になるというのは正しいようなので、読み取った値が間違っているのか、そのまま使うのではなく何か加工が必要なのかもしれません。

f:id:akanuma-hiroaki:20190316221017j:plain

 ただ、下記オフィシャル動画を見るとそのまま使えているように見えるので、違う原因があるのかもしれません。

docs.m5stack.com

まとめ

 今回はとりあえずセンサーの値を読み出すまではできましたが、正しい色を再現するところまではできなかったので、今後何かわかれば正しく再現できるように改善してみたいと思います。正しく再現できれば何か面白ことができそうな気はしているものの、特にまだアイディアは思いついていないので、何か役に立つもののアイディアも考えてみたいと思います。