Ruby

ラクスルはRubyKaigi2019にRuby Sponsorとして協賛します

ラクスルの松原です。

ラクスル株式会社は昨年に引き続き、本年も2019/04/18(水)〜2019/04/20(金)に福岡国際会議場で開催されるRubyKaigi2019にスポンサーとして協賛させていただくことになりました。

https://rubykaigi.org/2019/sponsors

ラクスル株式会社では「ラクスル」「ハコベル」両サービスにおいてRubyを中心技術として活用しています。

今回のRubyKaigiでは1日目(2019/04/18)にCTO泉よりスポンサーセッションとして、印刷・物流・広告といった、あまり技術的に攻められていない「ブルーオーシャン」(競合が少なく成長余地のある領域)をテーマにお話させて頂く予定です。

また、期間中は会場にてブースも出展させていただきます。「福岡」をテーマとしたラクスル特注の限定ノベルティーも用意しておりますので、参加される方は気軽にブースにお立ち寄り下さい!

 

当日会場でお会いできるのを楽しみにしております!

| by

ハコベルコネクトをリリースしました!

サーバサイドエンジニアの加藤です。
昨年8月に印刷から運送マッチングサービスのハコベルのチームに異動し、ハコベルコネクトの開発を進めてきました。

ハコベルコネクトとは

現在の物流業界における最大の課題はドライバー不足。ハコベルチームでは、現場の課題を調査し、その原因を探ってきました。見えてきたのは、複数の運送会社によって仕事が行われていることで、システム化が進まず紙・電話・FAXなどアナログで生産性を上げにくい現場環境でした。

ハコベルコネクトは、この情報断絶を解決すべく、大手物流荷主、一般貨物事業者など運送会社を自由度高くつなぐ物流プラットフォームを目指しています。

先月 1月24日に記者発表をし、日経新聞やTechCrunchにも取り上げられました。弊社CTOの泉がスマホのGoogle Chromeの新規タブで出てきたとのことで、社内でもちょっとした話題になりました。

今までも提供してきたサービスは、ハコベルカーゴ※に名称変更。これからもラストワンマイル向けのマッチングサービスを提供していきます。

仕事をする人に心地よい体験を

ハコベルコネクトは、PMやデザイナー・エンジニアが現場に足を運び、MVPを開発、実証実験を重ねて、徐々にその姿を明らかにしてできてきたプロダクトです。いくつもの運送会社の事業所に出向き、たくさんの方々にインタビューさせていただいたり、トラックに同乗させていただいたり。仕事をする人がどんな課題と向き合っているのか。様々なプロセスを経て、方向性が定まってきました。

ドライバーアプリイメージ

 

ハコベルコネクトという名前もその中で生まれてきました。SNSのように、会社と会社がつながりスムーズに情報のやりとりができることで、快適に仕事が出来るようにという想いが込められています。

ハコベルコネクトイメージ

技術負債の芽を最低限に ー今しかないでしょ!ー

さて、プロダクトとしてのリリースに向け、エンジニアとしてはどんなチャレンジがあったでしょうか。

  • 実証実験向けに急いで作られた側面があったため、既に技術負債気味なところがある
  • 業界独自の業務をモデリングしているため、キャッチアップしにくい
  • よくある機能でも複雑度が高く、要件の不整合が起きて開発が進みにくい

ハコベルチームには、私を含めラクスルの開発に関わってきたメンバーもいます。

「そうだ、ラクスルを作り直そう!」という投稿や「生まれ変わらNight -技術的負債からの一発逆転-」というイベントでご紹介させて頂いた通り、ラクスルは技術負債の課題に向き合ってきました。

(ちなみにまだまだ先はありますが、経営陣の理解もあり、ラクスルの技術負債の解消は着々と進んでいます。)

その教訓を活かして、ハコベルコネクトでは、リリース前に負債の芽の解消にも時間を使ってきました。リリース前ならば、ユーザー影響を気にせずにあらかじめアーキテクチャの整備を進めることが出来ます。「今しかないでしょ!」を合言葉に、気なっていたところはどんどん書き直しました。

ただし、特定業界向けのSaaSというサービスの成長特性を考えて、最初からドメイン分割やマイクロサービスを採用することはしていません。ラクスルで蓄積してきているベストプラクティスを取り入れ、まずはRails アプリケーションとしての Rail にきれいに乗るというところに集中しました。

フロントは、ラクスルの他のプロダクトでも採用しているVue.jsを使い、実証実験後にUIをフルリニューアル。Web APIが必要だったため、コントローラやサービスは約80%を書き換えました。

Web APIの開発中には、GraphQLが話題になったり、gRPC-Webが正式リリースになったりしたタイミングで、チーム内でGraphQLのPOCも行なったりしていたのですが、開発の状況を考慮して今回はオーソドックスにRESTful APIで開発していくことにしました。

フロントメンバーとのコミュニケーションやAPI エンドポイントの可視化のため、API仕様はSwaggerで記述することにしています。

チームでプロダクトを開発する ーペア&モブプロ、デザインスタジオー

先ほども書いた通り、ハコベルコネクトは、業界独自の業務をモデリングしているためキャッチアップしにくくチームの生産性がなかなか上がりにくい状態でした。

6ヶ月間でチームも倍増。新メンバーのキャッチアップスピードをあげたり、モチベーションを保ちつつ機能開発スピードを上げるのが重要課題になっていました。

そのため、複数人でコーディングするモブプログラミングやペアプログラミングを取り入れて、難しいドメイン知識を共有しつつ開発を進めたり、モチベーションが上がりにくい機能開発をわいわいと議論しながら進めました。

モブプログラミング用のスペースも導入され、今ではハコベルだけでなく他のチームで取り入れています。

モブプログラミングの様子

また、ラクスルの他のチームで行なっていた、コラボレーティブデザインの手法である「デザインスタジオ」も取り入れました。機能に対して色々なアイデアが出るのはもちろん、デザイナーも含めてこれから作る機能のデータ構造を共有したり、機能の背景を理解するのに役に立っています。

ハコベルチームには、運送業界出身のメンバーもいます。要件がわからない時はすぐに話を聞けるのも、チームで開発を進める上ですごく助けになりました。

新メンバー募集中です

さて、ハコベルコネクトはリリースしましたが、私たちの毎日を支えてくれている物流の現場には課題がたくさんあります。課題を解決すべく、ハコベルコネクトも成長させていきたいと思っています。

「仕組みを変えれば、世界はもっとよくなる」

エンジニアリングの力で世界を変えたいフロントエンドエンジニア、サーバサイドエンジニアを絶賛募集中です。興味を持たれた方は是非一度オフィスに遊びにきてください!

※ 2019/3/4 ハコベルマッチングからハコベルカーゴに再名称変更したためブランド画像更新しました。

RubyKaigi 2018にSticker Sponsorとして参加します

こんにちは。エンジニアの三瓶です。

ラクスルは、2018/05/31 ~ 2018/06/02に仙台国際センターで開催される RubyKaigi 2018 にSticker Sponsorとして参加します。

RubyKaigiは、プログラミング言語Rubyに関する国内最大級のイベントです。
Rubyはラクスルを支える中心技術であり、今後も積極的に活用していきたいと考えているため、このような機会を通じて少しでもRubyの発展に貢献していきます。

今回のRubyKaigiではSticker Sponsorとして、毎年配られているRubyKaigiのステッカーをラクスルにてスポンサリング・納品させていただきました。

また、ラクスル自身のステッカーや便利でかわいいノベルティもご用意していますので、是非ノベルティ置き場にお立ち寄りください。

【Ruby】Array から Hash を作る方法7選(a.k.a. やっぱり Array#zip はかわいいよ)

ラクスルでサーバサイドエンジニアをやっている小林です。

最近の業務では、主に Ruby を書いています。

さて、Ruby の組み込みライブラリにはいろいろな便利メソッドがありますが、
みなさん推しメソッドはありますか?

個人的推しメソッドは Array#zipHash#transform_values です。

Hash#transform_values について少し紹介すると、
もともと Rails4.2 の ActiveSupport で実装され、
Ruby2.4 で組み込みライブラリに移植されました。
移植の際は、名前をどうするかという議論でとても盛り上がったようです。
また、Ruby2.5 からは姉妹メソッドとも言える Hash#transform_keys が実装されました。

Hash#transform_values の話はこの辺にして、今回は Array#zip の推しポイントを紹介するため、
Array から Hash を作る方法について考えてみようと思います。

コーディングをしていると、下記のようなコードを書きたいことはないでしょうか?

  • ActiveRecord で取ってきて、id をkey、インスタンスを value にしたHash を作りたい
    • 例) [AR1, AR2, …] ⇒ { id1: AR1, id2: AR2, … }
  • 文字列のArrayに対し、元の文字列をkey、正規化後の文字列を value にしたHash を作りたい
    • 例) [Str1, Str2, …] ⇒ { Str1: NormalizeStr1, Str2: NormalizeStr2, … }

このようなときの実装方法をいくつかあげ、後半で性能比較をしてみようと思います。

 

以下、ActiveRecord で User 一覧を取得し、id を key、インスタンスを value とするHash を作成する場合を考えます。

空Hashに追加していく

他の言語でも実装できる、一番オーソドックスな方法かと思います。

array = User.all
hash = {}
array.each do |user|
  hash[user.id] = user
end
hash

 

Array#to_h を利用する

Ruby 2.1 から Array#to_h というメソッドが追加になっています。

レシーバを[key, value] のペアの配列として、Hash を返します。

これを利用すると、下記のように書くことができます。

array = User.all
array.map { |user| [user.id, user] }.to_h

 

Array#zip & Array#to_h を利用する

[key, value] のペアを作るのであれば、 Array#zip が便利です。

メソッドチェインですっきりと書けるところが、個人的気に入っています。

これだけでも、Array#zip がかわいいと思えます。

array = User.all
array.map(&:id).zip(array).to_h

 

Array#transpose & Array#to_h を利用する

レシーバを二次元配列として、転置配列を作成する Array#transpose を利用しても、
同じことができます。

array = User.all
keys = array.map(&:id)
[keys, array].transpose.to_h

 

Enumerable#each_with_object / Enumerable#inject を利用する

Array#to_h がない時代は Enumerable#each_with_objectEnumerable#inject を使うことが
多かった気がします。

array = User.all
array.each_with_object({}) do |user, hash|
  hash[user.id] = user
end

 

Enumerable#index_by を利用する(ActiveSupport)

よくあるパターンなので、ActiveSupport に Enumerable#index_by という、
まさになメソッドがあります。

ただ、こちらは Proc の返り値を key とする Hash を返すので、Array の要素を key、
Proc の返り値を value とする Hash を作りたい場合は、Hash#invert で一手間加える必要があります。

array = User.all
array.index_by(&:id)

 

Enumerable#reduce & Hash#merge を利用する

Lisp 的な発想で、 Enumerable#reduce と Hash#merge を使って畳み込みを行うことで 、
Hash を作ることもできます。

ちなみに、Ruby の Enumerable#inject とEnumerable#reduce は違う名前ですが、
同じ挙動をします。

少し話がそれますが、なぜ同じ挙動で名前が違うメソッドがあるのかについては、
るびまに書かれているので、読んでみると面白いかもしれません。

array = User.all
array.map {|user| {user.id => user} }.reduce(&:merge)

 

比較

みなさん、どの方法で実装することが多いでしょうか?

好みやコードの読みやすさなどで意見が分かれそうですが、一指標として、
各実装方法の性能評価をしてみたいと思います。

今回は、Array から Hash に変換する性能のみを評価するため、変換やメソッド呼び出しはせず、
Array から key と value が同じ Hash に変換する場合の性能を比較してみようと思います。

検証コード

ベンチマークの取得には、 Ruby on Rails Guides にも紹介されている
benchmark-ips gem を利用したいと思います。

検証コードは以下のとおりです。

#!/usr/bin/env ruby

require 'active_support/all'
require 'benchmark/ips'

array = (1..10_000).to_a

Benchmark.ips do |r|
  r.config(time: 20)

  r.report "Empty Hash" do
    hash = {}
    array.each do |num|
      hash[num] = num
    end
    hash
  end
  r.report "to_h" do
    array.map { |num| [num, num] }.to_h
  end
  r.report "zip & to_h" do
    array.zip(array).to_h
  end
  r.report "transpose & to_h" do
    [array, array].transpose.to_h
  end
  r.report "each_with_object" do
    array.each_with_object({}) do |num, hash|
      hash[num] = num
    end
  end
  r.report "index_by" do
    array.index_by { |num| num }
  end
  r.report "reduce & merge" do
    array.map { |num| {num => num} }.reduce(&:merge)
  end

  r.compare!
end

実行環境は以下のとおりです。

ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-darwin16]
activesupport (5.1.4)
benchmark-ips (2.7.2)

 

結果

Warming up --------------------------------------
          Empty Hash    80.000  i/100ms
                to_h    73.000  i/100ms
          zip & to_h   108.000  i/100ms
    transpose & to_h    98.000  i/100ms
    each_with_object    70.000  i/100ms
            index_by    63.000  i/100ms
      reduce & merge     1.000  i/100ms
Calculating -------------------------------------
          Empty Hash    773.943  (±10.3%) i/s -     15.280k in  20.013161s
                to_h    733.483  (± 8.9%) i/s -     14.600k in  20.090466s
          zip & to_h      1.065k (±10.3%) i/s -     21.060k in  20.027320s
    transpose & to_h      1.005k (± 8.5%) i/s -     19.992k in  20.067946s
    each_with_object    719.383  (± 7.1%) i/s -     14.350k in  20.063602s
            index_by    654.471  (± 8.6%) i/s -     12.978k in  19.999714s
      reduce & merge      0.962  (± 0.0%) i/s -     20.000  in  20.834702s

Comparison:
          zip & to_h:     1065.3 i/s
    transpose & to_h:     1004.8 i/s - same-ish: difference falls within error
          Empty Hash:      773.9 i/s - 1.38x  slower
                to_h:      733.5 i/s - 1.45x  slower
    each_with_object:      719.4 i/s - 1.48x  slower
            index_by:      654.5 i/s - 1.63x  slower
      reduce & merge:        1.0 i/s - 1107.19x  slower

Array#zip 早いですね!!メソッドチェーンですっきりかける上、処理も早いという、
これは推さざるをえない感じがしませんか?

Array#transpose も Array#zip とほぼ同じくらいの性能ですが、
やはり個人的にはメソッドチェーンで書ける Array#zip のほうが好きですね。

他の手法についても見てみると、Enumerable#index_by が思ったより遅いです。
実装を見たところ、空Hashに追加していく実装と同じなので、
yield の呼び出し分オーバーヘッドがかかっている感じでしょうか。

Enumerable#reduce と Hash#merge を利用する方法は、
配列長分 Hash#merge が実行されるため、かなり遅くなっています。

 

ただ、実際のコードでは、key と value が同じということはなく、
key や value に対して何かしらの処理を行うため、Hash の作成コストより、
他の処理のオーバーヘッドが大きくなります。

別途、key を Integer#to_s して Hash を作成する場合のベンチマークも取ってみましたが、
空Hash に追加する方法が一番早く、Enumerable#reduce & Hash#merge を除く
実装方法については、それほど変わらないという結果になりました。

#!/usr/bin/env ruby

require 'active_support/all'
require 'benchmark/ips'

array = (1..10_000).to_a

Benchmark.ips do |r|
  r.config(time: 20)

  r.report "Empty Hash" do
    hash = {}
    array.each do |num|
      hash[num.to_s] = num
    end
    hash
  end
  r.report "to_h" do
    array.map { |num| [num.to_s, num] }.to_h
  end
  r.report "zip & to_h" do
    array.map(&:to_s).zip(array).to_h
  end
  r.report "transpose & to_h" do
    [array.map(&:to_s), array].transpose.to_h
  end
  r.report "each_with_object" do
    array.each_with_object({}) do |num, hash|
      hash[num.to_s] = num
    end
  end
  r.report "index_by" do
    array.index_by(&:to_s)
  end
  r.report "reduce & merge" do
    array.map { |num| {num.to_s => num} }.reduce(&:merge)
  end

  r.compare!
end

=begin
Warming up --------------------------------------
          Empty Hash    17.000  i/100ms
                to_h    15.000  i/100ms
          zip & to_h    19.000  i/100ms
    transpose & to_h    18.000  i/100ms
    each_with_object    19.000  i/100ms
            index_by    18.000  i/100ms
      reduce & merge     1.000  i/100ms
Calculating -------------------------------------
          Empty Hash    192.959  (± 8.8%) i/s -      3.825k in  20.060582s
                to_h    178.314  (± 9.0%) i/s -      3.525k in  20.023982s
          zip & to_h    181.005  (±10.5%) i/s -      3.572k in  20.065550s
    transpose & to_h    176.782  (± 9.1%) i/s -      3.510k in  20.039329s
    each_with_object    192.932  (± 5.2%) i/s -      3.857k in  20.054975s
            index_by    182.739  (± 4.4%) i/s -      3.654k in  20.036235s
      reduce & merge      0.905  (± 0.0%) i/s -     19.000  in  21.030102s

Comparison:
          Empty Hash:      193.0 i/s
    each_with_object:      192.9 i/s - same-ish: difference falls within error
            index_by:      182.7 i/s - same-ish: difference falls within error
          zip & to_h:      181.0 i/s - same-ish: difference falls within error
                to_h:      178.3 i/s - same-ish: difference falls within error
    transpose & to_h:      176.8 i/s - same-ish: difference falls within error
      reduce & merge:        0.9 i/s - 213.18x  slower
=end

ちなみに、空Hash に追加するという Enumerable#index_by の実装も、
リファクタリングされて現在の形になっています。

まとめ

コーディングの際よくあるパターンとして、Array から Hash を作成する実装方法を7つあげ、
性能比較をしてみました。

個人的推しメソッドである Array#zip のかわいさが少しは伝わったでしょうか?