ESM アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

Rubyメソッドかるた裏話

RubyKaigi 2023 で限定配布した『Rubyメソッドかるた』好評のうちにソールドアウトしました。ありがとうございました。

ここでは『Rubyメソッドかるた』の制作にあたるふりかえりと裏話です。

制作のきっかけと配布方法の決定

アジャイル事業部では、月イチでホットな技術情報を持ち寄る『エンジニアお茶会』というイベントをしていて、そこで @ima1zumi が『Linux コマンドかるた』を元に Ruby 版のノベルティを作ると面白いのではということを言っていたのが始まりだったような。では実際に制作を進めようというときは、@ima1zumi は本編トーカー掛け持ちでになってしまうので @haruguchi との旗振りで、札のデザインや制作会社との調整が進められました。

今回はブースを出さないという方向にしていたため、配布方法をどうするかも議論したのですが、メンバーひとりひとりが参加者とのコミュニケーションのきっかけに配って行こうということになりました。結果的にメンバー個人と参加者との繋がりができた部分もあったかなと思います。

Ruby メソッドと選出メンバーの一覧

SNS 上でもたびたび話題にして頂いていたようですが、それぞれのメソッドを選出したメンバーは以下です。

  • Array#flatten @fugakkbn
  • Array#<< @haruguchi
  • Array#filter @wai-doi
  • Array#reject @wai-doi
  • Array#size @wai-doi
  • Array#join @wai-doi
  • Array#include? @koic
  • BasicObject#method_missing @koic
  • Comparable#clamp @koic
  • Data.define @koic
  • Enumerable#map @koic
  • Enumerable#lazy @koic
  • Enumerable#tally @kasumi8pon
  • Enumerable#each_with_object @koic
  • GC.disable @koic
  • Hash#compact @fugakkbn
  • Hash#merge @wai-doi
  • Hash#transform_keys @koic
  • Hash#transform_values @koic
  • Integer#even? @swamp09
  • Integer#succ @junk0612
  • Kernel.#binding @koic
  • Kernel.#puts @colorbox
  • Math.#sqrt @haruguchi
  • Method#source_location @swamp09
  • Method#curry @koic
  • Module#refine @amatsuda
  • Object#public_send @ima1zumi
  • Object#send @ima1zumi
  • Object#is_a? @fugakkbn
  • Object#inspect @fugakkbn
  • Object#== @koic
  • Object#equal? @koic
  • ObjectSpace.count_objects @koic
  • Proc#<< @haruguchi
  • RubyVM::AbstractSyntaxTree.parse @koic
  • RubyVM::InstructionSequence.compile @koic
  • RubyVM::YJIT.enabled? @koic
  • String#encode @ima1zumi
  • String#split @ima1zumi
  • String#chomp @wai-doi
  • String#concat @wai-doi
  • String#empty? @wai-doi
  • String#include? @wai-doi
  • String#gsub @wai-doi
  • String#size @wai-doi
  • String#match? @wai-doi
  • String#strip @wai-doi
  • String#chars @koic
  • Symbol.all_symbols @junk0612

String#encode を選んだメンバーが誰かは想像できたと思うのですが、他にも Array#tally などメンバーの推しメソッドが選ばれたりもしているので、そういった側面からも楽しんでもらえると嬉しいです。

今回の英語版

RubyKaigi は国際カンファレンスということもあって、札を日本語か英語いずれかあるいは両方で制作するか悩んだのですが、今回は読み札自体は日本語で制作して、英語は別途説明ページを設けようということになりました。英語ページのとりまとめは @kasumi8pon が行っています。

blog.agile.esm.co.jp

blog.agile.esm.co.jp

海外ゲストに向けて「かるた」がどういったものを伝えられるような動画は作っておけると良かったかもというのは、ちょっとした反省点です。

ともあれ、まだ日本に滞在している海外ゲストなどと遊んでみようというケースなどにご活用ください。

ruby.wasm の付録

英語ページを作る話の中で、札をランダムに選択するだけのボタンを付録として Web エントリの中に埋め込めると良さそうとなり、ruby.wasm を使った埋め込みをしています。

英語版ページでのブラウザのソースコードからも読めますが、ruby.wasm を使ったソースコードは以下です。RubyKaigi 2023 への移動前日の夕方に私 (@koic) の方でさっと書いてみたもので、ぜんぜんもっと DRY にしたりもできるはずですが、ひとまず Ruby 3.2 での推しメソッドである Data.define を使っているあたり、実際に配布する札に対してちょっとメタっぽい感じにできたのが気に入っている点です。

<script src="https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/browser.script.iife.js"></script>

<div id="wasm">
  <div id="content">
    <button id="cut_card" style='font-size: 30px;'>Next Card</button>
    <div id="card_table">
      <div id="next_card">Click the above button!</div>
    </div>
  </div>
</div>

NOTE: Loading WASM takes a little time.

<script type="text/ruby" charset="UTF-8">
  require 'js'

  Card = Data.define(:text, :link)

  deck = [
    Card.new(text: '001 : BasicObject#method_missing', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#001--BasicObjectmethod_missing'),
    Card.new(text: '002 : Object#public_send', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#002--Objectpublic_send'),
    Card.new(text: '003 : Object#send', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#003--Objectsend'),
    Card.new(text: '004 : Object#is_a?', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#004--Objectis_a'),
    Card.new(text: '005 : Object#inspect', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#005--Objectinspect'),
    Card.new(text: '006 : Object#==', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#006--Object'),
    Card.new(text: '007 : Object#equal?', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#007--Objectequal'),
    Card.new(text: '008 : Array#flatten', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#008--Arrayflatten'),
    Card.new(text: '009 : Array#<<', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#009--Array'),
    Card.new(text: '010 : Array#filter', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#010--Arrayfilter'),
    Card.new(text: '011 : Array#reject', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#011--Arrayreject'),
    Card.new(text: '012 : Array#size', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#012--Arraysize'),
    Card.new(text: '013 : Array#join', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#013--Arrayjoin'),
    Card.new(text: '014 : Array#include?', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#014--Arrayinclude'),
    Card.new(text: '015 : Data.define', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#015--Datadefine'),
    Card.new(text: '016 : Hash#compact', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#016--Hashcompact'),
    Card.new(text: '017 : Hash#merge', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#017--Hashmerge'),
    Card.new(text: '018 : Hash#transform_keys', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#018--Hashtransform_keys'),
    Card.new(text: '019 : Hash#transform_values', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#019--Hashtransform_values'),
    Card.new(text: '020 : Method#source_location', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#020--Methodsource_location'),
    Card.new(text: '021 : Method#curry', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#021--Methodcurry'),
    Card.new(text: '022 : Module#refine', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#022--Modulerefine'),
    Card.new(text: '023 : Integer#even?', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#023--Integereven'),
    Card.new(text: '024 : Integer#succ', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#024--Integersucc'),
    Card.new(text: '025 : Proc#<<', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#025--Proc'),
    Card.new(text: '026 : RubyVM::AbstractSyntaxTree#parse', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#026--RubyVMAbstractSyntaxTreeparse'),
    Card.new(text: '027 : RubyVM::InstructionSequence.compile', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#027--RubyVMInstructionSequencecompile'),
    Card.new(text: '028 : RubyVM::YJIT.enabled?', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#028--RubyVMYJITenabled'),
    Card.new(text: '029 : String#encode', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#029--Stringencode'),
    Card.new(text: '030 : String#split', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#030--Stringsplit'),
    Card.new(text: '031 : String#chomp', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#031--Stringchomp'),
    Card.new(text: '032 : String#concat', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#032--Stringconcat'),
    Card.new(text: '033 : String#empty?', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#033--Stringempty'),
    Card.new(text: '034 : String#include?', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#034--Stringinclude'),
    Card.new(text: '035 : String#gsub', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#035--Stringgsub'),
    Card.new(text: '036 : String#size', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#036--Stringsize'),
    Card.new(text: '037 : String#match?', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#037--Stringmatch'),
    Card.new(text: '038 : String#strip', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#038--Stringstrip'),
    Card.new(text: '039 : String#chars', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#039--Stringchars'),
    Card.new(text: '040 : Symbol.all_symbols', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#040--Symbolall_symbols'),
    Card.new(text: '041 : Comparable#clamp', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#041--Comparableclamp'),
    Card.new(text: '042 : Enumerable#map', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#042--Enumerablemap'),
    Card.new(text: '043 : Enumerable#lazy', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#043--Enumerablelazy'),
    Card.new(text: '044 : Enumerable#tally', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#044--Enumerabletally'),
    Card.new(text: '045 : Enumerable#each_with_object', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#045--Enumerableeach_with_object'),
    Card.new(text: '046 : GC.disable', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#046--GCdisable'),
    Card.new(text: '047 : Kernel#binding', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#047--Kernelbinding'),
    Card.new(text: '048 : Kernel#puts', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#048--Kernelputs'),
    Card.new(text: '049 : Math.#sqrt', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#049--Mathsqrt'),
    Card.new(text: '050 : ObjectSpace.count_objects', link: 'https://blog.agile.esm.co.jp/entry/ruby-method-karuta-reading-cards#050--ObjectSpacecount_objects')    
  ]

  document = JS.global[:document]
  button = document.querySelector('#cut_card')
  card_table = document.querySelector('#card_table')

  button.addEventListener 'click' do |e|
    document.querySelector('#next_card').remove

    next_card = deck.shuffle!.pop || Card.new(text: 'Game Over!', link: 'https://agile.esm.co.jp/en/')
    read_card = document.createElement('a')
    read_card[:id] = 'next_card'
    read_card[:href] = next_card.link
    read_card.append(document.createTextNode(next_card.text))

    card_table.append(read_card)
  end
</script>

このあたり JavaScript で行うという手もありますが、それでは全然ネタにならないのできちんとネタとして昇華させました。CDN から取ってきているのは Ruby 3.3.0-dev になり、そのあたりも RubyKaigi で新しい Ruby を runtime anywhere で動かしてもらうのを狙ってみたものです。

蛇足ながら、こういった一枚絵プログラミングこそ GPT が得意そうなので使ってみましたが、残念ながら ruby.wasm には詳しくなかったようです。2021年9月より新しい情報をベースにしているので、それはそうという感じでした。

そこで JavaScript でコードを書いては RubyKaigi に向けたネタとしては成立しないので、職人の温かなスクラッチ実装としたのが今回です。

ちなみに札選びの実装として、Array#sample を使うのでは 50 枚のかるたが重複しうるのと無限に選択できてしまうので、この類の処理は deck.shuffle!.pop のように Array#shuffle! して Array#pop すると、重複が起きず読み札がなくなったら終了します。ライトニングトークスの順番決めなどでも使えるパターンのため参考までに。

Ruby メソッドかるたの SemVer

今回の限定配布で入手された方は、箱をよく見ると 1.0.0 とバージョンが振ってあることに気がつくと思います。いちおう今後を見据えて以下のような SemVer (セマンティックバージョニング) を考えています。

  • メジャーバージョンアップ: 新しい Ruby バージョンのメソッド札が入る (例えば2日目のキーノートで紹介された Ruby 3.3 で追加予定の RubyVM::YJIT.resume が札に入るなど)
  • マイナーバージョンアップ: 札の入れ替えが起きる
  • パッチバージョンアップ: typo 修正のみ (今回でいうと Module#refine が対象)

もし年内に次回配布があるとすると、1.1.0 か 1.0.1 あたりになると思います。もし、次回こそ入手するぞという方がいらっしゃいましたら、弊社のこの開発者ブログや Twitter アカウントなどをウォッチしてください。

twitter.com

そして、むしろ一緒にかるたのメソッドを選びたいという方がいらっしゃれば採用応募を大歓迎しています。一緒に Ruby コミュニティを盛り上げていきましょう!

agile.esm.co.jp

Enjoy the Ruby Method Karuta!