RubyKaigi2017参加報告

サーバーサイドエンジニアの中野です。

大分期間が空いてしまいましたが、9/18〜9/20に広島で開催されたRubyKaigi2017のレポートです。3日目のBundler2のセッションの紹介をしたいと思います。

1系は1.16.xxが最後になる予定

1.16は現在開発中でまだstableではないですが、1系では1.16.xxが最後にバージョンになります。
もし1.15を使用している場合にbundle installコマンドを実行すると↓のようなワーニングが出力されます。

$ bundle install
The latest bundler is 1.16.0.pre.3, but you are currently running 1.15.4.
To update, run `gem install bundler --pre`

このワーニングの必要性に関しては個人的にも気になっていたのですが、開発中のバージョンを使ってもらうことでバグを洗い出したり仕様の問題などを見つけたいなどの理由があるようですが、意図せずバージョンを上げてしまう危険性もはらんでいる等の意見もあったりでこの問題を扱ったissueで活発に議論されました。
ちなみに、bundle config disable_version_check trueをすればでこのワーニングは表示されなくなります。

Bundler2について

興味深かったものをいくつか紹介していきます。
Bundler2のRFCの草稿はこちらで見ることができます。

コマンドを指定しないでbundleを実行するとhelpが出力される

1系では、コマンドを指定しないでbundleを実行するとbundle installが実行されていたのですが、2系からはhelpが出力されるように変更されます。個人的には便利で多用してたので少し残念な変更です。issueはこちら

Gemfile、Gemfile.lockがそれぞれgems.rb、gems.lockに変更される

この変更は話題にもなったためご存知の方も多いと思います。
2系でもGemfileとGemfile.lockはそのまま使用できますが、bundlerのバージョンが2系でrails newをした場合にはgems.rbとgems.lockが作成されるようになります。
名前変更の経緯を知りたかったのでissueをあさってみたところ、2010年にUNIXのLOCK拡張子があるのに異なる用途で同じ拡張子を使用するのはどうなの?というissueが立てられており、結果としてファイル名を変更することになったようです。

Gemfile内のgemをすべてアップデートする bundle update--all オプションの指定が必須になる

2系からGemfileのgemをすべてアップデートするために--allが必須になります。
理由としては、Gemfileに新たなgemを追加した時に bundle install すべきところにbundle updateしてしまう例が多かったためらしいです。自分もrailsを触り始めたばかりの頃にbundle updateしてしまって関係のないgemまですべてアップデートしてしまうことがあったので有り難い仕様変更だと思います。issueはこちら

githubやbitbucketなどに置いてあるgemをインストールするためのショートカットが使用できなくなる

gemにパッチをあてて使用してる場合にGemfileでそのgemを置いてあるサービス名をキーにしてuser/repoを指定するとインストールできてたのですが、これができなくなります。ただrailsの場合、5.0.1移行でrails newした際に作成されるGemfileには

git_source(:github) do |repo_name|
  repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
  "https://github.com/#{repo_name}.git"
end

が追加されておりgithubだけはショートカットが使えるようになってます。他のサービスも↑のように書くことでショートカットが使用できるようになります。

Bundle Plugin

Bundlerにはpluginという機能があり目的に応じた機能を追加することが可能となっています。
pluginの機能が用意されたのは、bundlerが大きくなるに連れ機能の要望が増え、開発、そしてメンテナンスしていくことが困難になってきたので、pluginという形で機能追加をする場を用意することで開発者各々が独自機能をbundlerのリリースを待たずに追加できるようにしたかったという経緯があります。バージョン1.13から用意されているのですが、まだドキュメント無くほとんど知られてないためドキュメント含め一緒にpluginを開発してくれる人大歓迎!とのことでした。

pluginの作り方

作り方はgemのそれとほとんど同じで、違いは一番上の階層にplugins.rbというファイルを用意しpluginのコードを書きます。
例としてbundle hogeと入力するとHogeと出力されるpluginの作り方を簡単に紹介します。hogeというgemを作成し↓のようなplugins.rbを用意します。

module Hoge
  class Plugin < Bundler::Plugin::API
    command "hoge"

    def exec(command, args)
      puts "Hoge"
    end
  end
end

これを通常と同じくrubygems.orgにアップすると、bundle plugin install hogeのようにして作成したpluginをインストールすることができるようになります。インストールが完了するとpluginが使用できるようになります。

$ bundle hoge
Hoge

おわりに

簡単ではありますがBundler2についての紹介をさせていただきました。
11月に開催されるRubyConfでは、Bundlerを初期から長くメンテをしているAndré Arkoがbundlerの歴史についてのセッションを行うので個人的にとても楽しみです。

なお、弊社では国内外のカンファレンス参加費用の補助制度があり、今回のRubyKaigiは会社の経費で参加させて頂きとても感謝しています。来年の仙台で開催されるRubyKaigiに参加したい方は是非ご応募下さい。

www.wantedly.com

RubyKaigi 2017に参加してきました

サーバーサイドエンジニアの芹沢です。

以前書いた私のエントリーにて、弊社のRubyKaigi参加者がそれぞれレポートを書くよという話をしましたが、その2人目のレポートです。

tech.toreta.in

私のレポートでは、2日目に行われたセッションから、Progress of Ruby/Numo: Numerical Computing for Rubyを紹介させていただきたいと思います。

スライドはこちら

speakerdeck.com

Ruby/Numo Projectとは

Rubyで機械学習や科学技術計算を行うためのライブラリを開発しているプロジェクトです。
一般に、機械学習界隈ではPythonで実装されたライブラリがよく使われていると言われており、実際私もnotebookやpandasをよく使うのですが、そういったライブラリが解決することをrubyでも解決することを目指しているプロジェクトのようです。*1

Rubyで機械学習を行うための別のアプローチとして、PythonのコードをRubyでも実行可能にするためのbinding機構であるPyCallがあります。今回のRubyKaigiでもセッションとワークショップが行われましたが、こちらはNumpy, pandasといったPythonのライブラリ資産をそのままrubyでも実行できるようにbindingすることを目指しています。

現在複数のgithub repositoryがこのプロジェクト配下に存在しますが、今回のセッションでも説明が多かったNumo::NArrayについて紹介します。

Numo::NArrayとは

多次元配列の計算を高速に行うためのライブラリ(という理解です。。)です。RubyにもArrayやMatricsの標準ライブラリがありますが、用意されているメソッドはPythonの行列計算ライブラリであるNumpyに近いインターフェースとなっています。
numpyとの対応表がwikiにありますが、かなりの割合のnumpyのメソッドを網羅していることが分かります。実際、Numpyの363個ある関数のうち217個をカバーしており、今後さらに91個が追加予定になっているとのことです。

Numo::NArrayを手元で試す

実際にNumo::NArrayを手元で動かしてみました。
Numo::NArrayのgithub repositoryのREADMEにはspecific_installを使う方法が記載されていますが、普通にGemfileを作ってbundle installしてもインストールできるのでこちらを採用しました。

こんな感じのGemfileを書いて bundle install します。

# frozen_string_literal: true
source "https://rubygems.org"

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

gem 'numo-narray', git: 'https://github.com/ruby-numo/narray.git'

irbで動かしてみます。

irb(main):001:0> require 'numo/narray'
=> true
irb(main):002:0> a = Numo::NArray[[1,2,3,4,5]]
=> Numo::Int32#shape=[1,5]
[[1, 2, 3, 4, 5]]
irb(main):003:0> a * 2
=> Numo::Int32#shape=[1,3]
[[2, 4, 6, 8, 10]]
irb(main):004:0> a.shape
=> [1, 5]
irb(main):005:0> b = Numo::NArray[[2],[3]]
=> Numo::Int32#shape=[2,1]
[[2],
 [3]]
irb(main):006:0> c = a * b
=> Numo::Int32#shape=[2,5]
[[2, 4, 6, 8, 10],
 [3, 6, 9, 12, 15]]

最後の行でやっている計算はbroadcastingという機能を使って実現されています。

これは、行列同士の四則演算を行う際に、両者の行列の形状が異なる場合であっても、次元が少ない方の配列を拡張して計算できるようにしてくれるものです。

これをrubyのArrayだけでやろうとすると結構複雑なmap処理をしないと実現できませんがNumo::NArrayを使うとシンプルに記述することができます。

また、今回のセッション中でも紹介されていたマスク処理という機能があります。これは配列内の各値に対して、条件にマッチしれいれば1,そうでなければ0の配列を返す機能です。

irb(main):001:0> a = Numo::NArray[1..10]
=> Numo::Int32#shape=[10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
irb(main):002:0> a < 6
=> Numo::Bit#shape=[10]
[1, 1, 1, 1, 1, 0, 0, 0, 0, 0]

これと同じことをRubyで書こうとすると以下のようになると思います。

 a = (1..10).map { |i| i < 6 ? 1 : 0 }

ステップ関数を配列の全要素に対して適用して二値ベクトルを得る時などに直感的に書けそうな感じがしてこのsyntaxは個人的にはすごく気に入りました。

ですが、パフォーマンスが悪ければ実用は厳しいので、実際にこのマスク処理のベンチマークを取ってArrayのみで実装した場合と比較してみました。

require 'numo/narray'
require 'benchmark'

base = (1...1000_000).to_a
compare = 1000

nbase = Numo::NArray[base]

Benchmark.bm do |r|
  r.report 'narray' do
    (nbase < compare).to_a
  end

  r.report 'map' do
    base.map do |v|
      v < compare ? 1 : 0
    end
  end
end
       user     system      total        real
narray  0.030000   0.010000   0.040000 (  0.029366)
map  0.060000   0.010000   0.070000 (  0.087115)

Numo::Int32のMask処理の方がArrayのmap処理のみで実装したサンプルよりも2倍ほど早い結果が出ました。

ただ、勘のいい人はお気づきのように、これはrubyのArrayオブジェクトをNumo::Int32のオブジェクトに変換する処理をBenchmark対象に入れていない場合の結果です。変換処理もベンチマーク対象に含めてみると

       user     system      total        real
narray  0.140000   0.000000   0.140000 (  0.156649)
map  0.070000   0.010000   0.080000 (  0.077548)

ベンチマーク結果が逆転します。Numo::Int32オブジェクトに変換する処理はかなりオーバーヘッドが大きい処理と考えられます。そのため、オーバーヘッドを無視できるほど同じ配列を何度も使用するようなケースであればともかく、基本的にはパフォーマンス面ではArrayを使った方が有利のようです。

まとめ

簡単ではありますがNumo::NArrayの紹介をさせていただきました。普段Pythonでscriptを書くこともある身としては、RubyでもNumpyのsyntaxで配列操作をできるようになるのはとてもありがたいことなので、projectの今後の発展に期待したいところです。
また、Arrayからのオブジェクト生成時のオーバーヘッドが大きいと本文中に書いたものの、それを無視できるユースケースであればNumo::NArrayの配列計算は相当パフォーマンスが良いと思われるため、うまくハマるユースケースがあれば使っていきたいと思います。

なお、今回のカンファレンスは会社の経費で参加させて頂きました。今年のRubyKaigiのclosingセッションでも「仕事としてRubyKaigiに参加している人はいますか?」というa_matsudaさんの質問に沢山の手が挙がる様子が見受けられましたが、このようなカンファレンスに業務として参加させていただけることはとてもありがたいことだと思います。

最後に、お約束になりますがトレタはサーバーサイドエンジニアを募集しております。来年のRubyKaigiは仙台なので、牛タンと萩の月と笹かまぼこが食べたい方は是非ご応募ください。*2(そうでない方もご応募ください) www.wantedly.com

*1:もし違ったらすみません…

*2:ちなみに今回はお好み焼きを食べ損ねました…

RubyKaigi2017とModule Builder Pattern #rubykaigi

サーバサイドエンジニアの中村です。先日開催されたRubyKaigi2017に参加しました。 その中で最も興味深かったセッションである The Ruby Module Builder Pattern について自分なりの理解をまとめてみようと思います。

Moduleの問題

module内でdefine_methodを使ってメソッド定義を行ったmethodに対して、そのmoduleをextendしたclass内で該当のメソッドに対して super を呼び出すことができません。

module AdderDefiner
  def define_adder(*keys)
    define_method :+ do |other|
      self.class.new(
        *(keys.map { |key| send(key) + other.send(key) })
      )
    end
  end
end

class LineItem < Struct.new(:amount, :tax)
  extend AdderDefiner

  define_adder(:amount, :tax)

  def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." } #=> No Method Error
  end
end

これは、+ methodが define_methodを使ってmodule内 で定義されているため、super で呼び出す祖先Object内で+ methodが定義されていないためだと認識しています。

これを回避するために、Anonymous Moduleを使ってこの祖先Object内で定義済みにするテクニックを今回のセッションで初めて知りました。

module AdderDefiner
  def define_adder(*keys)
    adder = Module.new do
      define_method :+ do |other|
        self.class.new(
          *(keys.map { |key| send(key) + other.send(key) })
        )
      end
    end
    include adder # この時点でAnonymous Moduleをincludeすることでsuperが利用可能
  end
end

このテクニックを応用したパターンがModule Builder Patternです。

The Ruby Module Builder Pattern

発表者の @shioyama さんに Module Builder Pattern を理解するための良いサンプルコードはありますかと質問したところ、 http://dry-rb.org/dry-equalizer というgemのコードがオススメだと教えていただきました。

そこで、本記事では dry-equalizer のコードを例にModule Builder Patternを理解していきたいと思います。

dry-equalizer

dry-equalizer gemはincludeしたclassの以下4つのmethodを再定義します。

Object#inspect
Object#hash
Object#eql?
Object#==

具体的な挙動は Examples のようになります。この挙動を踏まえて実際のコードを見ていきましょう。

Module Builder Patternのポイントは、Module.class #=> Class であることを利用して、動的にModuleを生成している部分です。

class Equalizer < Module

このように、Equalizer classを Module のsubclassとして定義することで、Anonymous Moduleのメリットが利用できます。

次に、included を見ていきます。

def included(descendant)
    super
    descendant.send(:include, Methods)
end

Equalizer が他classにincludeされた際に、

def eql?(other)
    instance_of?(other.class) && cmp?(__method__, other)
end

def ==(other)
   other.is_a?(self.class) && cmp?(__method__, other)
end

をincludeして再定義します。

そして最後に initializeです。

def initialize(*keys)
  @keys = keys
  define_methods
  freeze
end

初期化するkeyを受取り、define_methods を実行し、cmp?, hash, inspectを定義します。

このように、Module Builder Patternを使うと、非常にわかりやすくメソッドを動的に生成することができます。

おわりに

私はAPIのコードを書く上で常に設計についてどうするのがより良いか、チームの人と議論することが多く、今回のModule Builder Patternのような話は良い設計の1つのテーマとして非常に良いお話でした。

http://dry-rb.org というRubyの素晴らしい設計を集めたリポジトリも今回のRubyKaigiに参加するまでは知りませんでした。他のリポジトリのコードからも学びつつ、社内で良い設計とは何かについてまたさらに議論したいと思います。

またありがたいことに、弊社では国内・海外のカンファレンス参加は出張扱いになっており、カンファレンス参加費・交通費・宿泊費(一部)は会社負担になります。業務として色んなカンファレンスに参加したい!という方はぜひご応募お待ちしています。

© Toreta, Inc.

Powered by Hatena Blog