Wio Terminal でLチカしてみる

Seeed の Wio Terminal というマイコンをいただける機会があり、面白そうなので色々触ってみたいと思います。

jp.seeedstudio.com

Wio Terminal はマイコンにSAMD51を使用したボードで、下記のように盛り沢山な機能を積んでいます。

  • 2.4GHz/5GHz対応の無線LAN
  • BLE 5.0
  • 2.4インチLCD
  • IMU(LIS3DHTR)
  • マイク
  • スピーカー
  • microSDスロット
  • 光センサー
  • 赤外線送信機(IR 940nm)

さらに Grove コネクタを積んでいるので Grove モジュールを手軽に使える他、 Raspberry Pi 互換の GPIO だったり Raspberry Pi に接続できたりと夢が広がりますね。筐体にはマグネットが入っているので冷蔵庫とかに貼り付けることもできます。

今回はマイコン系を触るのがだいぶ久しぶりなのと、まずは Wio Terminal 最初の一歩ということで、下記のチュートリアルに沿って Lチカ を試してみます。

wiki.seeedstudio.com

開封

パッケージはこんな感じになっています。

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

中身は本体とUSB Type-C ケーブル、簡易マニュアルが入っています。

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

本体を USB Type-C ケーブルで PC と接続して電源を入れると、下記のような画面が表示されます。ここから本体上部のボタンを押すとプリインストールされているゲームを遊ぶことができます。

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

Arduino IDE 設定

Wio Terminal では Arduino 以外に MicroPython も使えます。今回はチュートリアルに沿って Arduino で試してみます。 Arduino IDE をインストールしていない場合はまず下記から Arduino IDE をダウンロードして、zip ファイルを解凍し、 Applications フォルダに移動します。

https://www.arduino.cc/en/Main/Software

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

Arduino IDE には様々なサンプルスケッチが用意されていて、Lチカのサンプルも用意されているので、 Arduino IDE を起動したら Lチカのサンプルスケッチを開きます。

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

次に、 Wio Terminal 用のライブラリを Arduino IDE に追加します。まずメニューから Preferences を開きます。

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

Additional Boards Manager URLs に下記URLを入力して、 OK をクリックします。

https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json

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

続けてメニューから Board Manager を開きます。

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

Board Manager で Wio Terminal を検索し、 Seeed SAMD Boards をインストールします。

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

ライブラリがダウンロードされた後インストールされていきますが、結構時間がかかります。私の環境では20分弱かかりました。

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

インストールが終わればライブラリの追加は完了です。

実機で動かしてみる

今回はプログラムはサンプルスケッチをそのまま使うので、あとは実機で動かしてみます。まずは対象のボードとして Wio Terminal を選択します。

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

次に PC と Wio Terminal を接続するポートを選択します。私の場合は Mac なので、下記のようなポートになります。

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

では実機にプログラムをアップロードします。 Arduino IDE のメニューバーから Upload ボタンをクリックします。

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

成功すれば Arduino IDE に下記のようなログが表示されます。

Sketch uses 31004 bytes (6%) of program storage space. Maximum is 507904 bytes.
Device       : ATSAMD51x19
Version      : v1.1 [Arduino:XYZ] Mar 12 2020 17:43:33
Address      : 0x0
Pages        : 1024
Page Size    : 512 bytes
Total Size   : 512KB
Planes       : 1
Lock Regions : 32
Locked       : none
Security     : false
BOD          : false
BOR          : true
Write 31628 bytes to flash (62 pages)
[==============================] 100% (62/62 pages)
Done in 0.416 seconds
Verify 31628 bytes of flash
[==============================] 100% (62/62 pages)
Verify successful
Done in 0.088 seconds

実機のLEDは筐体の中なので若干わかりづらいですが、下記写真の青色LEDの方が点滅します。

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

まとめ

Lチカまでは本体のセットだけで Micro SD カードも不要で簡単に実行できました。全てがパッケージングされているのはお手軽でとても良いですね。機能が豊富なのでアイディア次第で色々なことができそうですし、 MicroPython での実装や Raspberry Pi との連携もとても興味があるので、今後少しづつ試してみようと思います。

Ruby on Jets Quick Start on Cloud9

 最近になって Ruby on Jets というサーバレスフレームワークを知る機会があり、面白そうだったので触ってみました。Ruby on Jets は Rails ライクな開発ができるフレームワークで、それを AWS Lambda の Ruby Runtime の Function としてデプロイできるサーバレスなフレームワークです。

rubyonjets.com

 環境としてはローカルのPCで開発する場合は Lambda にデプロイする際に AWS アカウントの認証が必要になりますが、 Cloud9 環境であればシームレスに AWS サービスと連携できるので、今回は Cloud9 環境で試してみます。Ruby on Jets にはチュートリアル的に Quick Start が用意されているので、これを一通り試してみます。

rubyonjets.com

環境構築

 Cloud9 自体の環境構築については今回は割愛しますので、よろしければこちらをご参照ください。

blog.akanumahiroaki.com

 まず Ruby ですが、 Ruby on Jets は 2020/3/27 現在では Ruby 2.5系までの対応となっていますので、2.5系の最新版である 2.5.7 を使用します。

$ ruby -v
ruby 2.5.7p206 (2019-10-01 revision 67816) [x86_64-linux]

 ちなみに Ruby 2.6 以降を使っているとローカルで開発している段階では問題なくても、 Lambda への deploy 時に下記のように怒られてしまいます。

You are using Ruby version 2.7.0 which is not supported by Jets.
Jets uses Ruby 2.5.3.  You should use a variant of Ruby 2.5.x

 Jets では Rails 同様に bundler を使用しますので、インストールしておきます。

$ rbenv exec gem install bundler
Fetching: bundler-2.1.4.gem (100%)
Successfully installed bundler-2.1.4
1 gem installed
$ 
$ rbenv exec gem which bundler
/home/ec2-user/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/bundler-2.1.4/lib/bundler.rb

 そして Jets をインストールします。

$ rbenv exec gem install jets

 下記のようにインストールされたことが確認できます。

$ rbenv exec gem which jets
/home/ec2-user/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/jets-2.3.14/lib/jets.rb
$
$ rbenv exec jets -v
2.3.14

 また、 Jets では yarn を使用していますのでインストールします。

$ npm install -g yarn
/home/ec2-user/.nvm/versions/node/v10.19.0/bin/yarn -> /home/ec2-user/.nvm/versions/node/v10.19.0/lib/node_modules/yarn/bin/yarn.js
/home/ec2-user/.nvm/versions/node/v10.19.0/bin/yarnpkg -> /home/ec2-user/.nvm/versions/node/v10.19.0/lib/node_modules/yarn/bin/yarn.js
+ yarn@1.22.4
added 1 package in 0.445s

 今回 DB に MySQL互換の Aurora を使用しますので、下記ライブラリをインストールしておきます。

$ sudo yum install mysql-devel

プロジェクト作成

 Jets では Rails 同様に new コマンドでプロジェクトを作成できます。 Database Adopter はデフォルトで MySQL が選択されます。

$ rbenv exec jets new demo

 これで素のプロジェクトが作成されましたので、ひとまずサーバを起動して動作することを確認してみます。デフォルトでは 8888 ポートが使用されますが、 Cloud9 のプレビューで動作確認する場合、対応しているポートが 8080、8081、8082 なので、ポート番号を指定して起動します。

$ cd demo/
$ rbenv exec jets server --port 8080
〜〜〜中略〜〜〜
warning package.json: No license field                                                                                                                      
warning No license field                                                                                                                                    
=> bundle exec rackup --port 8080 --host 127.0.0.1
Jets booting up in development mode!
[DEPRECATED] `Bundler.with_clean_env` has been deprecated in favor of `Bundler.with_unbundled_env`. If you instead want the environment before bundler was originally loaded, use `Bundler.with_original_env` (called at /home/ec2-user/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/jets-2.3.14/lib/jets/commands/main.rb:46)
warning package.json: No license field                                                                                                                      
warning No license field                                                                                                                                    
Puma starting in single mode...
* Version 4.3.3 (ruby 2.5.7-p206), codename: Mysterious Traveller
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://127.0.0.1:8080
Use Ctrl-C to stop

 Cloud9 の IDE 画面から Preview を実行します。

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

 下記のように Jets の画面が表示されれば Jets が起動できています。

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

Scaffold での確認

 今回はとりあえず動作確認ができれば良いので、 Scaffold を試してみます。今回 DB は Aurora を使用しますので、 まずは RDS のコンソールから下記のような内容で DB を作成します。

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

 このままだと DB と同じ VPC の中からしか接続できないので、VPC セキュリティグループのインバウンドルールを変更します。最初は下記画像の上段のようになっていますので、とりあえず今回は試すだけということでどこからでも接続できるように下段のような内容に変更します。

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

 設定を変更したら Cloud9 のターミナルから接続できることを確認します。

$ mysql -h ruby-on-jets-demo.cluster-cnxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -u admin -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 37
Server version: 5.6.10 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> 

 ひとまずこれで DB の準備ができたので、下記コマンドで Scaffold を作成します。

$ rbenv exec jets generate scaffold post title:string

 DB の接続先情報については環境設定ファイル .env.development に下記のように設定します。

$ vi .env.development
$ cat .env.development 
# Example .env.development, meant to be updated.
ENV_DEVELOPMENT_KEY=example1
DATABASE_URL=mysql2://admin:xxxxxxxx@ruby-on-jets-demo-instance-1.cnxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com/demo_development?pool=5

 migrate を実行して Database とテーブルを作成します。

$ rbenv exec jets db:create db:migrate

 Aurora の方で確認すると下記のように posts テーブルが作成されています。

mysql> show tables;
+----------------------------+
| Tables_in_demo_development |
+----------------------------+
| ar_internal_metadata       |
| posts                      |
| schema_migrations          |
+----------------------------+
3 rows in set (0.01 sec)

 サーバを起動して動作を確認してみます。

$ rbenv exec jets server --port=8080

 先ほどと同様に Cloud9 の Preview でアクセスしてみます。 /posts にアクセスすると下記のように posts の CRUD が行えるようになっています。

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

 画面から posts の作成や変更が行えていればOKです。

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

 Aurora でも確認してみます。下記のように posts のレコードが作成されています。

mysql> select * from posts;
+----+-------+----------------------------+----------------------------+
| id | title | created_at                 | updated_at                 |
+----+-------+----------------------------+----------------------------+
|  1 | Test1 | 2020-03-26 00:13:46.882807 | 2020-03-26 00:13:46.882807 |
|  2 | Test2 | 2020-03-26 00:13:54.803820 | 2020-03-26 00:13:54.803820 |
+----+-------+----------------------------+----------------------------+
2 rows in set (0.00 sec)

Lambda にデプロイする

 ローカルでの確認がひとまず終わりましたので、 Lambda 環境へデプロイしてみます。Quick Start に記載されている手順通りにここまでやってくると、私が試した時点では nokogiri の 1.10.9 が使われるのですが、このまま Lambda 環境へデプロイしてアクセスすると下記のようなエラーが出てしまいました。

  "errorMessage": "Could not find nokogiri-1.10.9 in any of the sources",
  "errorType": "Init<Bundler::GemNotFound>",

 調べた結果これは nokogiri のバージョンによるもののようで、 1.10.8 を使えば回避できそうでしたので、 Gemfile に下記の設定を追加します。

gem 'nokogiri', '1.10.8'

 bundle update で gem を更新します。

$ rbenv exec bundle update nokogiri

 ローカルでの動作時は環境設定に .env.development というファイルを使用していましたが、 deploy 時は .env.development.remote が使用されますので、ファイルを作成します。DB のホストはローカルと同じ Aurora を使用し、 DB名だけ変更してみます。

$ cp .env.development .env.development.remote
$ vi .env.development.remote
$ diff .env.development .env.development.remote 
3c3
< DATABASE_URL=mysql2://admin:xxxxxxxx@ruby-on-jets-demo-instance-1.cnxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com/demo_development?pool=5
---
> DATABASE_URL=mysql2://admin:xxxxxxxx@ruby-on-jets-demo-instance-1.cnxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com/demo_development_remote?pool=5

 DB の migrate を実行します。環境変数として JETS_ENV=development JETS_ENV_REMOTE=1 を設定することによって接続先情報として .env.development.remote が参照されるようになります。

$ JETS_ENV=development JETS_ENV_REMOTE=1 bundle exec jets db:create db:migrate

 Aurora で Database とテーブルが作成されたことを確認します。

mysql> show tables;
+-----------------------------------+
| Tables_in_demo_development_remote |
+-----------------------------------+
| ar_internal_metadata              |
| posts                             |
| schema_migrations                 |
+-----------------------------------+
3 rows in set (0.00 sec)

 これで deploy する準備ができましたので、 deploy コマンドを実行します。

$ bundle exec jets deploy

 実行途中で下記のように gem を Lambdagems に送って良いか聞かれますので、Y と答えて処理を続行します。 nokogiri などのネイティブ拡張を使ってコンパイルされるような gem は、ローカルでコンパイルしたものをそのまま Lambda に配置しても使用できませんので、 Jets は Lambdagems からプリコンパイルされた gem をダウンロードして使用するようです。

Is it okay to send your gem data to Lambdagems? (Y/n)?
Y

 deploy に成功すると下記のようにアクセス先の URL が表示されます。

Stack success status: UPDATE_COMPLETE
Time took for stack deployment: 1m 32s.
Prewarming application.
API Gateway Endpoint: https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/

 ブラウザから上記のURLにアクセスしてローカルでプレビューした時と同様に jets の画面が表示され、 /posts で posts の CRUD が行えれば正しく deploy されています。

 Lambda のコンソールから確認すると、下記のように Lambda Function が作成されています。

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

 また、 API Gateway の下記のように自動的に作成されています。

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

まとめ

 今回はひとまず Quick Start の内容を一通り試してみただけですが、ローカルでは Rails ライクに開発することができ、 Lambda のことを意識しなくても deploy すれば自動的に Lambda Function を構成してくれますので、手軽に Serverless なサーバサイド開発が行えました。 Cloud9 であれば AWS サービス利用時の認証等の手間もありませんので、とても相性よく使えると思います。 Ruby on Jets はまだプロダクション環境での実績はあまりないようですが、 Serverless な構成ができると実運用でのメンテナンスやスケーリングのコストもかなり削減できますので、いろいろ試して将来的には実際のサービスに使えると良いなと思っています。

GPS マルチユニットSORACOM Edition でとりあえず位置情報を取得する

 ソラコムから GPS マルチユニットが発売になり、GPSによる位置情報、温度、湿度、加速度のデータが手軽に取得できるということで、興味本位で一つ買ってみました。 とりあえずデータを確認するところまでは何も難しいことはなくとっても簡単にできてしまったので、特に解説することもないかと思ったのですが、せっかくなのでまとめてみます。

soracom.jp

購入はソラコムユーザコンソールから

 購入は一個単位でユーザコンソールから発注できます。

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

 上記で発注すると、本体とマイクロUSBケーブルに加えて Sim もセットになっています。私が発注した時は朝のうちに発注して翌日には届きました。

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

デバイス登録からデータ確認まで

 下記の Getting Started のページの内容に従えば簡単にできてしまいます。私は Mac からやりましたが、 iPad とかからでもいけちゃいますね。本体への Sim カードのセットも下記ページで紹介されていますのでここでは割愛しますが、Simのカバーを外すのに少し力が入りますので、最初は折りそうで怖いかもしれません。

dev.soracom.io

Sim の受け取り処理

 まずは今回一緒に届いた Sim の受け取り処理を行います。発注 ボタンから発注履歴画面へ移動します。

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

 GPSマルチユニットの明細の右側に 受け取り確認 ボタンが表示されていますのでこれをクリックすればそれでOKです。

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

デバイス登録

 購入した GPS マルチユニットを登録します。メニュー内の ガジェット管理 > GPSマルチユニット から管理画面へ移動します。

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

 新規デバイス設定 ボタンをクリックします。

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

 GPSマルチユニットに使用する Sim を選択して、 次へ: グループを選択 ボタンをクリック。

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

 Sim を登録するグループを選択します。既存のグループを選択することもできますが、今回は新たに作ります。

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

 GPSマルチユニットから送信するデータの設定をします。今回はとりあえず試しということで、取得できる4種類の全データを送信してみます。送信先はとりあえず可視化ということで Harvest、送信モードも自動モードだといつ送信されるかのコントロールができないので、手動モードで2分おきに送信されるようにしてみます。

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

 設定はひとまずこれだけなので、あとは本体の電源を入れればデータの送信が開始されます。

データ確認

 データの確認はガジェット管理画面から データを確認 ボタンをクリックして Harvest の画面に移動します。

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

 下記のようにデータが取得できていることが確認できます。 Harvest は位置情報は地図上に表示してくれるので、お手軽に位置情報が可視化できてしまいます。

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

まとめ

 GPS マルチユニットSORACOM Edition であればハードウェア本体側の作業は Sim を入れる以外は何もなく、コンソールから少し設定するだけで簡単に位置情報の取得が開始できてしまうのがすごいですね。また、送信先は Unified Endpoint になっているので後から連携先を変える時もコンソールの操作だけでできますし、IMSI等で個体の識別もできるので、AWS 等と連携させれば色々なサービスが考えられそうです。今回はとりあえずデータ取得まででしたが、 Beam や Funnel を使って AWS や GCP との連携も試してみたいと思います。

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 等での挙動についても見て行きたいと思います。