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

まとめ

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

AWS Cloud9 で既存の EC2 インスタンスに接続する

 以前の記事でEC2インスタンスも同時に自動的に作成するやり方で Cloud9 の環境を作成してみましたが、仕事等で使うケースを考えると、既存のインスタンスに接続するケースもありそうだったので、試しに EC2 インスタンスを事前に別途作成して、そこに接続するやり方で Cloud9 の環境を作成してみました。

 手順はこちらで紹介されているので、この手順に沿ってやってみました。 EC2 インスタンスを作成するリージョンは Cloud9 と同様にシンガポールリージョンにしています。

docs.aws.amazon.com

VPC作成

 まずは VPC を作成します。 VPC コンソールから VPC ウィザードの起動 をクリックします。

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

 VPC 内からインターネットに接続できる必要があるので、種別としては 1個のパブリックサブネットを持つ VPC を選択して 選択 ボタンをクリックします。

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

 VPC の設定は基本的にデフォルトで OK なので、 VPC 名だけ任意のものを設定し、 VPC の作成 をクリックします。

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

 VPC が作成されたら、 VPC コンソールからその VPC を選択し、 Description タブの Network ACL の ID のリンクをクリックします。

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

 インバウンドルールを設定するために、 Inbound Rules タブの Edit inbound rules をクリックします。

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

 インバウンドルールでは ssh の接続元を Cloud9 に限定するのですが、設定すべき CIDR は ip-ranges.json というファイルに記載されているので、下記のように確認することができます。今回はシンガポールリージョンなので、 region が ap-southeast-1 になっている2項目を使用します。

$ wget https://ip-ranges.amazonaws.com/ip-ranges.json
$ jq '.prefixes[] | select(.service=="CLOUD9")' < ip-ranges.json
{                              
  "ip_prefix": "13.250.186.128/27",
  "region": "ap-southeast-1",
  "service": "CLOUD9"
}                                 
{                               
  "ip_prefix": "13.250.186.160/27",
  "region": "ap-southeast-1",
  "service": "CLOUD9"
}            
〜〜〜以下略〜〜〜

 上記 CIDR をアクセス元として設定した ssh のルール2つと、Port Range で接続を許可する設定1つの合計3つのルールを設定します。最後に Save ボタンをクリックして設定を保存します。

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

EC2 インスタンス作成

 続いて実際に接続する EC2 インスタンスを作成します。 EC2 コンソールから インスタンスの作成 をクリックします。

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

 AMI は Amazon Linux 2 を選択します。

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

 インスタンスタイプは、今回はお試しということで t2.micro を選択します。

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

 インスタンスの詳細設定では、ネットワークとして先程作成した VPC を選択します。サブネットもその VPC に属する一つが選択されます。その他はデフォルト設定でOKなので、 次の手順: ストレージの追加 をクリックします。

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

 今回はストレージはデフォルトのままで良いので、そのまま 次の手順: タグの追加 をクリックします。

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

 今回は特にタグも追加しないので、そのまま 次の手順: セキュリティグループの設定 をクリックします。

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

 セキュリティグループの割り当てでは 既存のセキュリティグループを選択する を選択し、VPC 内に作成されているセキュリティグループを選択して 確認と作成 をクリックします。

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

 設定内容の確認画面が表示されますので、間違いがなければ 起動 ボタンをクリックします。

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

 インスタンスに接続する際のキーペアを新たに作るか、既存のものを使用するかの選択フォームが表示されます。今回は 新しいキーペアの作成 を選択し、キーペア名を設定して キーペアのダウンロード をクリックし、プライベートキーファイルをダウンロードします。ダウンロードすると インスタンスの作成 ボタンがアクティブになりますので、クリックしてインスタンスを作成します。

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

EC2 の環境設定

 冒頭で紹介したページでも説明されている通り、 Cloud9 で接続するにはいくつかの要件を満たす必要がありますので、インスタンスに接続して設定を行います。 VPC の作成中にインバウンドルールの設定で ssh の接続元を Cloud9 に限定しましたが、設定時にローカルからも接続できるように一時的に設定を追加します。設定が完了したら追加した設定は削除しておきます。

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

 また、詳細な説明は割愛しますが、パブリックIPアドレスが必要になりますので Elastic IP も割り当てておきます。

 接続できるようになったら ssh でインスタンスに接続します。

$ ssh -i "cloud9.pem" ec2-user@ec2-52-XXX-XXX-XXX.ap-southeast-1.compute.amazonaws.com

 Cloud9 で接続するには Python 2.7 が必要になります。これはデフォルトでインストールされています。

$ python -V
Python 2.7.14

 次に node.js も必要になります。これはデフォルトではインストールされていませんので、下記手順でインストールします。

$ sudo yum -y update 
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash
$ . ~/.bashrc
$ nvm install node
$ node --version
v11.11.0

 また、 Cloud9 で接続するディレクトリの権限を設定します。ホームディレクトリを対象とする場合には下記のように権限を設定します。

$ ls -ld ~
drwx------ 5 ec2-user ec2-user 122 Mar  9 11:47 /home/ec2-user
$ 
$ sudo chmod u=rwx,g=rx,o=rx ~
$ 
$ ls -ld ~
drwxr-xr-x 5 ec2-user ec2-user 122 Mar  9 11:47 /home/ec2-user

 最後に AWS Cloud9 インストーラを実行します。 gcc が必要なので yum で Development Tools をインストールしてから、 curl でインストーラをダウンロードして実行します。

$ sudo yum -y groupinstall "Development Tools"
$ curl -L https://raw.githubusercontent.com/c9/install/master/install.sh | bash

Cloud9 の環境作成

 それでは Cloud9 の環境を作成します。 Cloud9 のコンソールから Create environment をクリックします。

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

 環境名と説明を任意に入力し、 Next step をクリックします。

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

 Environment type で Connect and run in remote server (SSH) を選択します。User には ec2-user、Host には先程までに作成した EC2 インスタンスのパブリックIPを設定します。 Copy key to clipboard をクリックすると Cloud9 から EC2 インスタンスに接続するためのパブリックキーがコピーされますので、インスタンス上で authorized_keys に設定します。設定できたら Next step をクリックします。

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

 ちなみに authorized_keys への登録は vi などで下記のようにファイルを編集してペーストします。

$ vi ~/.ssh/authorized_keys

 設定内容の確認画面が表示されますので、問題なければ Create environment をクリックします。

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

 Cloud9 の環境が作成され、追加でインストールが必要なもののリストが表示されますので、全て選択された状態のまま Next をクリックします。

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

 コンポーネントのインストールが始まりますので完了するまで待ちます。ちなみに私は上記の画面から進まなくなってしまったので一度キャンセルしましたが、再度 Cloud9 環境に接続した際にインストールが行われ、無事に完了しました。

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

 インストールが完了すると Cloud9 の IDE が表示され、使用できるようになります。

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

まとめ

 Cloud9 で接続するためにはサーバ側がネットワーク設定やソフトウェアのインストールなどの要件を満たす必要があるのでちょっと煩雑ですが、一度設定を作ってしまって AMI として保存しておけば、誰かが同じ環境を使う場合にも AMI からインスタンスを作成すれば Cloud9 側の環境だけ新たに作れば良いので、導入時の手間が削減できそうですね。

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