Ruby

gRPC Client Interceptor入門 with Ruby

こんにちは。サーバサイドエンジニアの三瓶です。印刷ECサイトの ラクスル の開発保守を担当するチームに所属しています。

ラクスルでは Raksul Platform Project (RPP) と称して技術負債の返済活動を継続的に行っているのですが、その流れの一環として印刷ECチームでは巨大化したアプリケーションから商品仕様に関わる部分を別サービスとして切り出す、という作業を最近行いました。

この切り出したサービスでは通信方式として gRPC を、言語としてはRubyを採用しています。

実際に導入してみて、gRPCはまだドキュメントも多くはないと感じたので、本記事ではgRPCが備える機能の1つである Interceptor についてチュートリアル形式でご紹介したいと思います。言語はもちろんRubyを使います。

※ gRPC導入に至った背景や経緯について興味のある方は弊社エンジニアの二串のスライドをご参照ください

Interceptorとは何か

Interceptorとは、RPCコールの前後に任意の処理を差し込める機能・レイヤーのことです。Railsアプリケーションに馴染みがある方は Rack Middleware のようなものを想像してもらうと理解しやすいかと思います。

Interceptorを使えば、認証やロギング、データのフィルタリングなど各通信で共通の処理をひとまとめにできます。
実際に、我々のプロジェクトではリクエストを一意に特定するためのIDをmetadataに付与するInterceptorやリクエストしたメソッド・パラメータ・処理時間をログに残すInterceptor、例外をCatchしてSentryに通知するInterceptor等を自作して、本番環境で稼働しています。

以下はClient → Server間の通信に2つのInterceptorが介在したときのイメージ図です。

作成するClient Interceptorの仕様を決める

ClientのInterceptorと一口に言っても、以下のパターンが考えられます。

  • リクエスト処理の「前」に差し込む。リクエストデータに何か手を入れたい場合など
  • リクエスト処理の「後」に差し込む。レスポンスデータに何か手を入れたい場合など
  • リクエスト処理の「前後」に差し込む。両方に手を入れたい場合や処理全体に何かをしたい場合など

このうち、今回はリクエストの「前」に差し込んでリクエストデータを大文字化する UpcaseInterceptor と、リクエストの「前後」に差し込んで処理時間を計測する PerformanceLoggingInterceptor の2つを作ってみることにします。

以下、それぞれのインターセプターの簡単な仕様です。

  • UpcaseInterceptor
    • リクエストの前に差し込む
    • リクエストオブジェクトに name というキー名のデータあればその値を大文字化する
  • PerformanceLoggingInterceptor
    • リクエストの前後に差し込む
    • リクエスト全体にかかった時間を計測し、標準出力へ出力する
    • 時間は小数点第3位以下を四捨五入する

また、動作の流れは次の図のようになります。赤い線の箇所でインターセプターの処理を挟みこむイメージです。

Client Interceptorを実装する

※ 注意:gRPCのRuby実装では、IntercptorのAPIインターフェースは EXPERIMENTAL という扱いのようです。そのため、今後のバージョンアップで今回紹介するものとはAPIインターフェースが変わっている、ということがありえますのでお気をつけください。

Interceptorを実装するのはとても簡単で、今回の仕様であれば数行で作れてしまいます。

UpcaseInterceptor

UpcaseInterceptorの実装は次のようになりました。

# upcase_interceptor.rb
class UpcaseInterceptor < GRPC::ClientInterceptor
  def request_response(request: nil, call: nil, method: nil, metadata: nil)
    request.name = request.name.upcase
    yield
  end
end

いくつかポイントとなる部分を説明します。

(1) GRPC::ClientInterceptor を継承する

インターセプタークラスは GRPC::ClientInterceptor クラスを継承する必要があります。
継承しない場合、GRPC::InterceptorRegistry::DescendantError 例外が発生します。

(2) 通信方式に合わせてメソッドをオーバーライドする

メソッドは利用する通信方式に合わせてオーバーライドします。gRPCには以下4つの通信方式があり、それぞれに対応するメソッドがあります。

  • Unary RPC : #request_response
  • Server streaming RPC : #server_streamer
  • Client streaming RPC : #client_streamer
  • Bidirectional streaming RPC : #bidi_streamer

今回は、1リクエストに対して1レスポンスが返ってくる一番シンプルな通信方式であるUnary RPCを使うため、request_response メソッドをオーバーライドしています。

※ 各通信方式の詳細が気になる方は https://grpc.io/docs/guides/concepts/ をご参照ください

(3) yieldで次の処理を呼び出す

次にメソッドの中ですが、yield を呼び出すことで次のインターセプターまたはリクエスト処理の本体を呼び出すことになります。
UpcaseInterceptorはリクエスト実行の前に呼び出したいので、yield をコールする前に変換処理をしています。

PerformanceLoggingInterceptor

同様に、処理時間を計測して出力するPerformanceLoggingInterceptorは次のようになりました。

# performance_logging_interceptor.rb
class PerformanceLoggingInterceptor < GRPC::ClientInterceptor
  def request_response(request: nil, call: nil, method: nil, metadata: nil)
    start_time = Time.now.to_f
    resp = yield
    request_time = Time.now.to_f - start_time

    puts "duration: #{request_time.round(3)} sec"
    resp
  end
end

PerformanceLoggingInterceptorはリクエスト処理の全体を計測したいため、リクエストの前後に処理を差し込みます。
前述のとおり、リクエスト処理は yield を呼び出すことで伝播されていくため、その前後を囲う形で時間を取れば良いということになります。

返り値は yield で受け取ったレスポンスオブジェクトをそのまま返します。

Interceptorを使う

それでは、作成した2つのInterceptorを実際に使ってみましょう。
gRPC公式のチュートリアル Ruby Quick Start にあるHelloWorldアプリケーションに、今回作成したインターセプターを組み込んで動かしてみます。

Client Interceptorは、Stub と呼ばれるクライアントオブジェクトを初期化するときに引数として渡します。

# greeter_client.rb
require 'grpc'
require 'helloworld_services_pb'

# ★ 作成したインターセプターをrequireする
require_relative './performance_logging_interceptor'
require_relative './upcase_interceptor'

def main
  stub = Helloworld::Greeter::Stub.new(
    'localhost:50051',
    :this_channel_is_insecure,
    interceptors: [UpcaseInterceptor.new, PerformanceLoggingInterceptor.new] # ★ ここでインターセプターを設定する
  )

  # 省略..
end

main

interceptors 引数に渡す順序でインターセプターの実行順序が決まります。
配列の末尾から先頭へと順に実行されるため、上の書き方の場合、PerformanceLoggingInterceptorUpcaseInterceptor の順で実行されることになります。当初のイメージ図の通りの順序ですね。

それでは実行してみましょう。
まず、比較対象としてインターセプターを使わなかった場合の結果を見てみます。

$ bundle exec ruby greeter_client.rb
"Greeting: Hello world"

チュートリアルの通りであれば、上のように “Greeting: Hello world” という結果が出力されると思います。

次にインターセプターを有効化した状態で実行してみます。

$ bundle exec ruby greeter_client.rb
duration: 0.002 sec
"Greeting: Hello WORLD"

UpcaseInterceptor によって “world” が大文字化されて、PerfomanceLoggingInterceptor によって実行にかかった時間が出力されました!

まとめ

以上、簡単ではありますが、RubyでのgRPC Client Interceptorの仕組みや使い方についてのご紹介でした。

アプリケーションを本格的に運用するにつれて様々な要求が発生してくるかと思いますが、Interceptorが役に立つ・解決手段となるケースもあるのではないかと思います。そのようなときにこの記事が参考になれば幸いです。

RubyKaigi 2019に行ってきました!

こんにちは、ラクスルの村田です。

ラクスルはRubyKaigi 2019にRubyスポンサーとして参加してきました!

上の写真はCTOの泉がスポンサートークをしている様子です。

ラクスルの提供するサービスの多くにRubyが使われており、去年に引き続き会社としてRubyコミュニティに貢献するべく、今年も参加させていただきました。

こちらはブースで提供したノベルティです!

カップラーメンを模してあり、ふたには会社がある「目黒」の文字、
そして中央にはラクスルの「ラ」が書かれています。

社内のデザイナーが力を入れて作ってくれました。

頑張ってノベルティを作ったかいもあり、ブースは大盛況でした!

ノベルティの詳細は弊社のデザインブログの記事をご覧ください!

福岡県知事の小川さんもブースに来訪いただきました!

ラーメンの中に入っているノベルティはラクスルの自社サービスで作ったことや、
福岡にちなんでラーメンの形にしたことなどを話したそうです。

弊社のCMで女優ののんさんが親指と人差し指を広げるポーズ(ラクスルスイッチ)があるのですが、
みんなでそのラクスルスイッチをする様子です!

 

面白かったセッション

聞きに行ったどのセッションも勉強になりましたが、業務でAPIの開発を担当していることもあり、APIに関わるものが特に楽しめました。

 

 

業務でもcommitteeを使っていて、OpenAPI3への対応を待ち望んでいました。
これで念願のnullableができる…!

ota42yさんありがとうございます。

 

APIの設計に当たって、いくつかの設計方法で迷うことがよくあります。
その際にとても参考になるセッションでした。

 

そして、最終日の最後のkeynoteセッション。
普段は可読性を重視して実装することがほとんどなので、スピードを最優先にする書き方は
非常に新鮮で面白かったです。

反省点

人見知りな性格のため、一度も公式のアフターパーティーに参加できなかったことが反省点です。。。

社外でもRubyエンジニアの友達を作るため、来年は頑張って参加します…!

 

最後に

ラクスルではエンジニアを絶賛募集しています!

現在手がけている、印刷・物流・広告の事業はどれも競合がいないブルーオーシャンです。
そのような環境の中でテクノロジーを使って課題を解決していきませんか?

青い海を赤いRubyの力(CTOの泉が気に入っているフレーズ)を使って切り開いてみたい方は
是非一度オフィスに遊びに来てみてください。

また、RubyKaigi 2019をテーマとした勉強会が弊社オフィスで開催されるので興味のある方は是非ご参加ください。

https://raksul.connpass.com/event/125165/

ラクスルは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 のかわいさが少しは伝わったでしょうか?