マイペースなRailsおじさん

Ruby、Ruby on Rails、オブジェクト指向設計を主なテーマとして扱います。だんだん大きくなっていくRuby on Rails製プロダクトのメンテナンス性を損なわない方法を考えたり考えなかったりしている人のブログです。

Rails6 + Webpackerな環境でyarnで入れたbulmaを使う

bulmaは美しいCSSフレームワークです。Railsで使おうとする場合、bulma-railsというgemを使ってもいいのですが、せっかくyarnが入っているので、yarnでインストールすることにしました。

bulma-railsは、本家よりすこし遅れてアップデートが反映されていくので、早く最新版を使いたい場合は、yarnで入れる方にメリットがあります。

環境

特にオプションは指定せずrails newした直後の状態から始めます。

手順

bulmaをインストール

yarnでbulmaをインストールします。

yarn add bulma

stylesheet.scssを設置

Railsからbulmaを読み込めるようにします。

まず、読み込むCSSを確認します。通常、node_modules/bulma/css/bulma.min.cssが配置されているので、これを読み込みます。

$ ls node_modules/bulma/css
bulma-rtl.css      bulma.css
bulma-rtl.css.map  bulma.css.map
bulma-rtl.min.css  bulma.min.css

app/javascript/packs/application.scssに下記を記述します。無い場合は作成します。| ~(チルダ)に続けてnode_modulesから見たパスを指定します。

@import '~bulma/css/bulma.min';

viewからapplication.cssを読み込む

application.html.erbに、こちらの記述があることを確認してください。なければheadタグ内に追加します。

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %

以上でRailsからbulmaを使う準備ができました。簡単ですね。

使ってみる

<div class="section">
  <article class="message">
    <div class="message-header">
      <p>こんにちは</p>
      <button class="delete" aria-label="delete"></button>
    </div>
    <div class="message-body">
    bulmaをインストールして使ってみました。
    </div>
  </article>
</div>

f:id:ytnk531:20201027000429p:plain 以上です!

Stimulas、いつつかうん?

Stimulas

Stimulasは、少ないコード量で動的な表現をすることに主眼を置いて開発された、軽量なJavaScriptフレームワークです。DHH率いるBaseCampが公開してます。

主に提供している機能はこの2つです。

  • DOM要素のイベントと、そのイベントにフックするJSのメソッドをHTMLへの簡単な記述で紐付けられる。
  • DOM要素とJSの変数を、HTMLへの簡単な記述で紐付けられる

解説はこちらがわかりやすいです。 JavaScript初心者にもオススメ!クリーンなコードでHTMLを拡張する「Stimulus」とは?|ferret

vs Vue.js, React.js

フロントエンドのフレームワークといえば、Vue.jsReactです。これらと比較するとどうでしょう?

結論から言うと、Stimulasはこれらのフレームワークほど協力ではありません。値を双方向バインディングして自動的にDOM要素を更新するような機能も無ければ、UIパーツをコンポーネント化して使い回すような機能もありません。

例として、Vue.jsではテンプレートの中に変数を入れておくと、その変数が更新されれば自動的にDOM要素の値も更新されます。これをバインディングといいますが、Stimulasにはそのような機能はありません。DOM要素を更新したければ、プレーンなJSを使うときと同様、HTMLのAPIを操作してDOM要素を更新する必要があります。

いつつかうん?

私が考えるのStimulasの使い所は、「フロントエンド専門のエンジニアがいないプロジェクトでJSを書く場合」です。

フロントエンドのエンジニアがいるような環境では、HTMLの共通化や自動生成はVue.jsやReact.jsを用いjてJS側で行われることがほとんどなはずです。 いくらStimulasが軽量のフレームワークで覚えることが少ない、といっても今どきの大抵のフロントエンドエンジニアは、よりリッチなVueやReactを使える人がほとんどです。わざわざ新しいことを覚えて、より非力なフレームワークを使うというのは非効率に感じます。

また、フロントエンドエンジニアが投入されているようなプロジェクトでは、やはりそれらリッチなフレームワークを使うべき複雑なフロントエンドの制御が求められるはずです。そのような場合、Stimulasを使ったからと言って、フロントエンド開発の効率化はあまり期待できません。

逆に、Railsのようなフレームワークを使って、モノリシックなアプリケーションを少人数で作っているような環境では、Stimulasが役に立つかもしれません。 JSにあまり詳しくないエンジニアからすれば、Stimulasは非常にちょうどいいフレームワークです。 きっと、そのような人たちはこのように考えると思います。

  • 生のJSでは、DOM取得と操作がコードの大半を埋め尽くしてしまい、記述が面倒である。
  • フロントエンドで複雑な制御(状態がたくさんあったり、UIパーツをガラッと入れ替えたり…etc.)を必要としない、控えめなフロントエンドの制御が必要な場合に、Vue.jsやReactのようなリッチなフレームワークを活用するのはやりすぎである。

そう思ったとき、Stimulasは、生のJSとリッチなJSフレームワークとのちょうど間をいく、いいかんじのフレームワークなんです。

サーバーサイドからHTML、CSSまで一人で触るような開発をしている場合、サーバーサイドでできること(HTMLの生成)を、わざわざJSに書くのは非効率です。そのため、クライアントサイドでの描画が必要な動的な部分だけを効率よく作りたい、と考えるのは自然なことに感じます。 Stimulasは、サーバーサイドでHTMLの共通化や自動生成を行っているプロジェクトで、JSのコードを記述する量を最小化するのに用いるのが適しているのではないかと考えています。

gemの概要を雑に把握するシリーズ② ruby-i18n/i18n

概要

Ruby用の国際化(i18n)機能を提供するgem。ymlで書かれた設定ファイルを、規定の構造で設定しておくと、I18.tメソッドで翻訳を取得できる。

github.com

仕組み

I18n.tの引数と、現在設定されているロケールを頼りにymlに書かれた対応関係に従って翻訳を返す。 I18n::BackEnd::Simpleに手を加えたい人もいるだろうから、Simpleを拡張できるようにした。オリジナルのTransformerをつくったら、こうやって読み込めば使える。

I18n::Backend::Simple.send(:include, I18n::Backend::Transformers)

gettextにも対応。.poにmsgidとmsgstrを記述する。_メソッドに元になるテキストを引数として渡す。

gettext メッセージIDとメッセージの対応ではなく、メッセージと別の言語のメッセージで対応を取る方式。ある特定の言語のメッセージをIDと捉えると言ってもいい。

気になる実装

ロケールの設定がマルチスレッド考慮している。グローバル変数として使いたくなる値はThread.current[]でスレッドローカルにする。

翻訳の機構の本体は、「backend」と呼ばれていて、configで付け替えられる。 tは配列にも対応。

transliterate(翻字)機能もある。

翻字(ほんじ、英: transliteration)とは、言語学において特定の言語を記した文字表記を別の文字による表記に移すことをいう。翻字は印刷物の発行する際などに技術的な問題や読者の便宜のために行われる。

翻字 - Wikipedia

localizeもある。 https://github.com/ruby-i18n/i18n/blob/master/lib/i18n/backend/simple.rb#L89 lookupに本体実装が有る。 ファイルの読み込みはload_fileからload_xxxを呼んだあとにstore_translations(バックエンド固有)を使う。拡張子ごとに読み込み方法を変えるというの、メタプロの使い所としてかなり美味しい。

Fiber Schedulerの使い方

この記事では、Ruby3で追加される予定のScheduled Fiberを紹介します。Ruby 3.0.0 preview1の情報であり、この機能はまだまだ議論と開発の真っ只中です。実際に正式にリリースされるのものとはインターフェースや実装が異なることをご理解ください。

Ruby本体の事情にかなり疎いので、いろいろと想像で補っている部分があります。間違ったこと言っていたら教えてくださると助かります。

Fiber Scheduler

とは

Fiber Schedulerは、Ruby3で追加される予定の、Fiberの新機能です。IOを扱うファイバーを複数作って、これらを適切にコンテキストスイッチしてあげると、Threadを使うよりも高い並行性を得られるという話があります。Fiber Schedulerは、このコンテキストスイッチを実装しやすくするための機能で、もともとはAuto Fiberと呼ばれていました。

経緯

これらのチケットで議論されました。もともとAuto Fiberは、Ruby Coreの方で適切なコンテキストスイッチを提供してあげようという提案でした。紆余曲折あって、Fiberのブロッキング操作に対してフックできるインターフェースを提供しようという事になったようです。

Ruby 3.0 preview1にマージされたのはこのissueで作られたPRです。

PR

何がうれしいか?

Fiber Schedulerが開発された目的は、既存のコードをあまり変更せずに並行性を向上させることです。

Fiberを使ってIO待ちの間、うまいことコンテキストスイッチをしてやると、Threadよりも効率よく非同期処理ができる、という話があります。

IO待ちのファイバーを適切に切り替えて行けるSchedulerを実装しておくと、IO#reade, IO#writeなどのブロッキング処理を、Fiberの提供する軽量スレッドを使って非同期で実行できるようになります。 うまく行けば、既存のロジックを変更せず、Fiber.scheduleで囲むだけで、同時接続に対してスケールできます。

この機能を作っているSamuel Williamsさんによる説明があるので、英語がわかる方はこちらをどうぞ。 Ruby Concurrency Final Report

使い方

Schedulerを作り、Fiber.schedulerに非同期で実行したい処理を書きます。

Schedulerのインターフェースはこちらで定義されています。 このインターフェースを実装したクラスをThread.current.schedulerに設定したうえで、Fiber.scheduleにブロックを渡すと、渡したブロックで実行されるブロッキング処理を行う際、Schedulerに実装されたメソッドをフックします。

class MyScheduler
  def io_wait(io, events, timeout)
  # 実装する
  end
  # 省略
end
Thread.current.scheduler = MyScheduler.new

Fiber. schedule do
  # この中身はMySchedulerによってコンテキストスイッチしながら非同期で実行される
end

つかってみる

以前紹介していた、nio4rを使ってselectすることで、ノンブロッキング処理ができるSchedulerを作って使っています。 Fiberとnio4rでサーバー - まいおじ

gist.github.com

これで一応動くんですが、acceptが動かなかったりするので、なにか間違えてるかもしれません。ソースコード読みながら何がおかしいか探っていく予定。

gemの概要を雑に把握するシリーズ① intridea/multi_json

github.com

概要

システムで利用可能なJSONパーサーのうち、最適なものを選んで使ってくれるJSONパーサーのラッパー。 できるだけ早いものを使うようにしていて、独自に優先順位を付けている。 候補がなにも使えない環境の場合はOkJSONを使う。

仕組み

Adapterクラスで、9種類のJSONパーサに同じインターフェイスをもたせている。偉い。 デザインパターンパターン的にはAdapterパターンかFacadeパターン。 どのAdapterを使うかは、defined?(::Adapter)みたいにして定数の存在を確認する docs.ruby-lang.org

Fiberとnio4rでサーバー

Fiberとnio4rを組み合わせてサーバーを作りました。あたまバグりそうになるのでよかったら見てってください。

Fiberとnio4rを組み合わせる

Fiberは、軽量なスレッドを作成できます。このスレッド上で、途中まで実行した処理を中断し、再開することができます。 この特徴をうまく使ってやると、nio4rのselectと組み合わせて効率的な通信処理を実現できます。

非常にややこしいので、注意してください。

クライアントとの通信

Fiber.newで、ブロックにとった処理を行うFiberを作成できます。作成した時点では処理は実行されません。Fiber#resumeを呼ぶと、Fiber.yieldまで処理を実行したあと、呼び出し元に戻ります。この時点で、Fiberは処理が「中断」された状態になります。このあと、再びFiber#resumeを呼ぶと、中断されたところから処理を再開します。

f = Fiber.new do
  puts 'Hello1'
  Fiber.yield
  puts 'Hello2'
end

f.resume # => Hello 1
f.resume # => Hello 2

ここで、nio4rのselectと組み合わせて効率的な通信を行うことを考えます。selectは複数のIOを待って、利用可能なものを選択できます。ブロッキングが起こりそうなときに処理を中断してselectし、またなくても良くなったら処理を再開するようにFiberを制御すると、Fiberはノンブロッキングに処理を実行できます。

コードにするとこういう感じです。 ioは、ノンブロッキング通信が可能な状態で渡されるソケットです。@selectorNIO::Selectorです。

Fiber.new do |io|
  message = io.read_nonblock 5000
  @selector.register io, :w
  Fiber.yield

  io.write_nonblock "response"
  io.close
end

ノンブロッキングでreadしたら、次は書き込みを行いたので、書き込みを待つようにselectorに登録します。こうなったらいったん処理を抜けます。 処理が再開されたら、それはすなわち書き込みができる状態ということなので、ソケットに書き込みます。

処理の振り分け

振り分ける側は、適切な(=ノンブロッキングで処理が可能な)fiberをresumeできるように、@fibersにioとfiberの組み合わせを持つようにします。 selectで適切なioがわかるので、ioをキーに、そのioを処理するfiberを値にもつハッシュを作ってやれば、どのfiberをresumeすべきかは簡単にわかります。

@srv = TCPServer.new 13_000
@selector.register @srv, :r

loop do
  @selector.select do |m|
    case m.io
    when @srv
      socket = m.io.accept_nonblock
      @fibers[socket] = communication_fiber
      @selector.register socket, :r
    else
      f = @fibers[m.io]
      @selector.deregister m.io
      f.resume m.io
    end
  end
end

これが非常にややこしいんだなぁ…

Fiberなサーバー

ソースコード全量がこちら。前回に引き続き、最低限のHTTPレスポンスを返すサーバーを作ってます

gistb6da8b93f525ccfdb150f160717fa786

ブラウザで表示したようす。受け取ったメッセージをそのまま返すので、WebブラウザのHTTPリクエストが書いてあります。

f:id:ytnk531:20201022010753p:plain

nio4rでselect

TCPコネクションのようなIOを複数扱うとき、書き込みや読み込みが可能になっているものを一つ選びたい、ということがあります。RubyにはIO.selectというメソッドがあり、これを使えばそのようなことができます。

IO.selectは、Rubyが動く環境ならどこでも動くというメリットがあるのですが、selectの機構はカーネルによってはepollなどのより効率のいい方法が使えます。nio4rは、これらの効率のいいシステムコールないしCライブラリでを使ったselectを実現するライブラリです。

これ1年位前にちょっと触ったんですが、なんだかよくわかりませんでした。復習兼ねて使い方を説明します。

nio4rの使い方の大まかな流れ

1. NIO::Selectorオブジェクトを作る

2. 監視したいIOオブジェクトを登録する

3. NIO::Selectorで処理をブロックして待ち受ける

という感じです。

 

で、これの何が嬉しいの?という点なんですが、複数のIOを待ち受けて、処理が可能になったときだけ処理を行う、というようなことができます。普通にやったら無限ループを書いて、ステータスを確認して…というコードになるところを、無駄な処理を行わず、かつ無駄な待ち時間を発生させずに処理できます。

 

下記のサンプルコードでは、TCPで接続を待ち受けつつ、標準出力が書き込み可能になるのを待ちます。接続されない間は標準出力にドットを出力し続け、接続されたら、TCPソケットに送られてきた文字を出力します。

gist9cf98cd662a2d55c37d88c89278d5cc2

 

 動画です。左側でドットを吐き出し、データが送信されてきたタイミングで"!"が出力されるのがわかるかと思います。

そしてFiberへ

asyncというライブラリでは、Fiberとnio4rを組み合わせて、低コストな非同期処理を実現しています。

github.com

大まかな流れは下記のとおりです。

1. タスクをFiberで実行する

2. IO待ちが起こりそうになったら、IOをNIO::Selectorに登録する

3. IOとFiberの組み合わせを配列に保存する

4. Fiberの処理を中断する

5. selectする。このとき、他に処理可能なFiberがあればそっちを処理する

6. 中断していたIOがSelectされたら、3.の配列からFiberを取り出して再開する

 

こちらのほうがThreadを使った並列処理よりもパフォーマンスが良いそうです。Fiberは、Threadよりも低コストにスレッドが生成できます。Threadは、GVLの制約を受けてIOブロッキングが起きている間しか並列に処理が実行できません。ということは、より生成コストの低いスレッドをSelectを使って半自動で制御してあげたほうがパフォーマンスいいよね、という話のようです。