ハイサイ、ファインディでTeam+を開発しているEND(@aiandrox)です。
RubyKaigi 2024最高でしたね!!私は2度目の参加でしたが、去年よりもみんなが笑っているところで笑えるようになり、各種イベントなどでいろんな方と話すことができたのでさらに楽しめました。
今回特に印象に残ったのは初日のKeynoteの「Writing Weird Code」でした。
「なるほどわからん」という感じで、正直半分以上わからなかったです。ただ、描画されたコードが格好よくてめちゃくちゃ感動しました。何が起きているのかはわからなかったけど、なんか浪漫のようなものを感じました。
せっかくRubyKaigiでQuineというものを知ったのだから、自分にどこまでできるのかはわからないけどやってみたい!ということで、Quineに挑戦してみました。
Quine(クワイン)とは
Quineとは、自分自身のソースコードを出力するプログラムのことです。
すごい見た目でなければいけないのかと思いきや、Quine自体にデザインはなく、cat
と実行時の出力結果が同一になるものをQuineと呼びます。
eval$s=%w'o="eval$s=%w"<<39<<$s<<39<<".join";puts(o)'.join
しかし、私がやりたいのは見た目が格好いいやつなので、今回はデザインQuineを作るのを目標にしました。
いざ実践
まずは何からすればいいのか?ですが、最初に読むには「あなたの知らない超絶技巧プログラミングの世界」が一番わかりやすかったです。とりあえずこれを読んで手元で実行しながら、ざっくりとQuineの概要と考え方を頭に入れます。半分くらいは理解できていませんが気にせず進めました。
その後、他の実装記事なども読みつつ実際にコードを書いてみました。RubyでうどんげQuine(とAA型Quineの作り方講座)で、AAのQuineを作る方法が紹介されていたので、それを参考にしました。詳細な作り方はこちらの記事に書いてあります。
まず、使いたい画像をAAに変換します。今回はFindyのロゴを使用しました。
その後、形を整えつつAAを01
に置き換え、Quineを作成するためのbuild.rb
を作成しました。
細かいロジックは参考元*1の記事にあるので省略しますが、簡潔に書くと以下の通りです。
- 圧縮したAAデータ
bin
を元に、空白またはコード1文字分をo
に追加していく - 最初に
eval$s=%w'
、最後に'.join
があるので、その中のコードは空白を削除されたうえでRubyコードとして実行可能
# build.rb aa = <<~END 00000000000111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 00000001111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 00000111111111111111111100000000000001111111111111111100011111100000000000000000000000000000000000011111000000000000000000000 00011111110000000001111111000000000001111111111111111100011111100000000000000000000000000000000000011111000000000000000000000 00111111000000000000011111100000000001111110000000000000000000000001110000000000000000000000000000111110000111000000000111000 00111110011110000000000111110000000001111110000000000000011111100001111111111111110000000111111111111111000111111000001111110 01111111111100000000000111110000000001111110000000000000011111100001111111111111111100001111111111111111000011111000011111100 01111111111000000000000011110000000001111111111111110000011111100001111110000011111100011111110000111111000011111100111111000 01111111000000000000000111110000000001111111111111110000011111100001111100000011111100011111000000011111000001111111111110000 00111110000000000000000111110000000001111111111111110000011111100001111100000001111100011111000000011111000000111111111100000 00111111000000000000011111100000000001111110000000000000011111100001111100000001111100011111100000111111000000011111111000000 00011111111000000011111111000000000001111110000000000000011111100001111100000001111100001111111111111111000000001111110000000 00000111111111111111111111110000000001111110000000000000011111100001111100000001111100000111111111111111000000001111100000000 00000000111111111111100011111000000000111110000000000000001111100000111100000001111000000000111111001111000000011111100000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111000000000 END start_text = 'eval$s=%w' end_text = '.join' aa_data = aa.split("\n") x_length = aa_data.first.length y_length = aa_data.length last_point = aa_data.last.split('1').last.length # 最終行に1がないパターンは考慮しない bits = aa.gsub("\n", '').reverse.to_i(2) bin = [Marshal.dump(bits)].pack('m').gsub("\n", '') code = <<~CODE b="#{bin}" n=Marshal.load(b.unpack("m")[0]) e="#{start_text}"<<39<<($s*3) o="" j=-1 0.upto(#{y_length}*#{x_length}-1){|i| o<<((n[i]==1)?e[j+=1]:32) o<<((i%#{x_length}==(#{x_length - 1}))?10:"") } o[-#{last_point + end_text.length + 2},6]=""<<39<<"#{end_text}" puts(o) CODE code = code.split("\n").join(';') code << '#' file = File.new('quine_base.rb', 'w') file.puts "#{start_text}'#{code}'#{end_text}"
これによって作成されたコードを実行すると、以下の出力を得られます。出力結果をquine.rb
に記述して、ruby quine.rb
を実行すると、同じ結果になります。
これにてQuineの完成です🎉
eval$s= %w'b="BAhsK3oA+ AMAAAAAAAAAAAAAAAAA 8P8HAAAAAAAAAAAAA AAAgP/ /A4D/ //gBAAA A4AMAAP wB/AHw/x8/AAAAAHw AAMAPA H4Afg AAgAMA AMCHAz j4PAAf wA8 A/PD/ B/z /8Y Of/wP gA/g BgB/+ /8P/P3 z48T8A eAD/f/DDD3784Ye fH/4AgA/g/w9++M CPD/jg /4EPAP AB/P/BDx/w8 QEf+B /wA4Af gB8A+O EDPn7wA/4B/AP+AfA DAD98wIf/f4AfAP7 //wB+ AOCHD/ jg/w/wAQD+ Pz6A DwD44AEP4OcBPwA AAAAAA AAAAAA AAAAA8 AM=";n= Marsha l.load (b.unp ack("m" )[0]) ;e="eval$s=%w"< <39<<( $s*3) ;o=""; j=-1; 0.upt o(15*125-1){ |i|;o <<((n [i]==1)?e[j+=1] :32);o <<((i %125= =(124 ))?10 :"");};o[- 16,6]= ""<<39 <<".jo in";pu ts(o) #b="B AhsK3o A+AMAA AAAAAAAA AAAAAAA8 P8HAAAAA AAAAAA AAAAAg P//A4 D///g BAAAA4AMAAPwB/AH w/x8/A AAAAHwAAMAPAH4AfgAAgAMA AMCHAz j4PAAf wA8A/ PD/B/ z/8YOf/wPgA/gBg B/+/8 P/P3z48T8AeAD /f/DD D3784 YefH/ 4AgA /g/w 9++MCP D/jg /4EPAP '.join
ここまではわりとスムーズにできました。eval$s=%w'
から'.join'
まではRubyコードなので、#
の後はコメントアウトとして好きな文字を入れることができます。例えば、code
変数内のe
を変更すると、puts(0)
以降を#
で埋めることができます。
e="#{start_text}"<<39<<($s+"#"*500)
eval$s= %w'b="BAhsK3oA+ AMAAAAAAAAAAAAAAAAA 8P8HAAAAAAAAAAAAA AAAgP/ /A4D/ //gBAAA A4AMAAP wB/AHw/x8/AAAAAHw AAMAPA H4Afg AAgAMA AMCHAz j4PAAf wA8 A/PD/ B/z /8Y Of/wP gA/g BgB/+ /8P/P3 z48T8A eAD/f/DDD3784Ye fH/4AgA/g/w9++M CPD/jg /4EPAP AB/P/BDx/w8 QEf+B /wA4Af gB8A+O EDPn7wA/4B/AP+AfA DAD98wIf/f4AfAP7 //wB+ AOCHD/ jg/w/wAQD+ Pz6A DwD44AEP4OcBPwA AAAAAA AAAAAA AAAAA8 AM=";n= Marsha l.load (b.unp ack("m" )[0]) ;e="eval$s=%w"< <39<<( $s+"# "*500) ;o="" ;j=-1 ;0.upto(15*1 25-1) {|i|; o<<((n[i]==1)?e [j+=1] :32); o<<(( i%125 ==(12 4))?10:"") ;};o[- 16,6]= ""<<39 <<".jo in";p uts(o )##### ###### ######## ######## ######## ###### ###### ##### ##### ################ ###### ####################### ###### ###### ##### ##### ############### ##### ############# ##### ##### ##### #### #### ###### #### ###### '.join
色を付けてみる
とりあえず、ほぼコピペをすることでQuineが作成できたので、次は色を付けてみたいと思いました。eval
の中は自由にコードが書けるので、わりと簡単にできるのでは?と思いましたが、そんなことはなく、かなり苦労しました。
こちらが今回作成した色付きQuineです。
実際のロゴと重ねるとこんな感じ。
コードはこちら↓ github.com
以下に、実際に作ってみて自分が感じた点について記載します。
データを圧縮するのが難しい
このQuineでは、AAを成形するためのデザインコードと色付けロジックに関するコードを持っています。また、それらを描画するコードもあるため、すべてをQuine内に収める必要があります。つまり、AAの字数 - (AAを描画するロジックの字数 + 色付けロジックの字数 + 描画コードの字数) > 0
にしなければなりません。
Findyロゴをできるだけ大きくすることでコード文字数を確保し、色付けロジックをstringにして実行コード内でeval展開することで対応しました。
irb(main):018> colors_bin = [Marshal.dump(colors)].pack('m').gsub("\n", '') => "BAhbFG86ClJhbmdlCDoJZXhjbEY6CmJlZ2luaRI6CGVuZGkCZAFvOwAIOwZGOwdpAvQBOwhpAggCbzsACDsGRjsHaQKUAjsIaQKoAm87AAg7BkY7B2kCPgM7CGkCUgNvOwAIOwZGOwdpAuMDOwhpAvwDbzsACDsGRjsHaQKKBDsIaQKcBG87AAg7BkY7..." irb(main):019> colors_text = colors.to_s.gsub(' ', '').gsub("\n", '') => "[13..356,500..520,660..680,830..850,995..1020,1162..1180,1328..1345,1495..1510,1660..1675,1825..1840,1990..2005,2155..2170,2320..2336,2490..2506,2655..2669]" irb(main):020> colors_bin.length => 408 irb(main):021> colors_text.length => 156 # 252字の削減!
「Ruby Committers and the World」で`frozen string literalの話題のときにmametterさんが話していた「1byteを切り詰めている」とはこういうことかと思いました。
また、AAロジックをQuine内で持つことで、AA成形コードを削減することができるようですが、これについてはどうすればいいのかわからず断念しました。
文字コードの意識が難しい
出力する文字列を、実行コード・出力両方として扱う関係上、そのまま使うことができない文字もあります。そういったものは、ASCIIコードを使うことでエラーを回避します。
例えば、"\e[34m"
などのエスケープシーケンスを使うことで出力文字に色を付けることができます。
しかし、\e
をそのまま記述すると文字列として解釈されます。そのため、生成されたQuineを実行するとそのまま文字列が出力されてしまいます。

しかし、27.chr
で"\e"
と同値を出力することができます。*2ちなみに、#chr
で文字列に変換しているのは+
メソッドを使うためなので、<<
で文字連結する場合は文字コードの整数のままで問題ありません。
エスケープシーケンスを追加することにより、細かい位置の調整が難しくなる
$s
に代入する%w
で空白を許容した文字列を改めて実行可能なコードにするために、AAのコードの終わりの箇所に'.join
を挿入しています。しかし、エスケープシーケンスのそれぞれの文字列が1文字として扱われるため、最後の調整が難しかったです。
今回は以下のようにしましたが、AAが変わると若干崩れるので、最後は手作業で地道に調整することになりそうです。
# build.rb end_text = '.join' end_text_start_point = last_point + end_text.length output_text_length = "\e[0m#\e[m".length # エスケープシーケンスを使って白字を1字出力する必要な字数(`#`は仮の文字列) real_output_text_length = end_text.length * output_text_length # エスケープシーケンスを考慮した上で確保すべき文字数 # (一部省略) code = <<CODE l=#{"'".ord}.chr o[-#{end_text_start_point + real_output_text_length},#{(end_text.length + 1) * output_text_length}]=l+"#{end_text}" CODE
感想
eval
内ではRubyのコードを素直に書くことができるので、超絶技巧を使わなくてもQuineでAAを書くことはできました。
ある程度完成しないとコード自体が動かないのでデバッグしづらいし、理解できていない部分もたくさんあります(irbとはずっと付きっきりでした) 。でも、コードを書いていい感じの見た目ができるのがとても楽しかったです!!
おまけ
この記事をレビューしてもらったところ、早速カスタマイズLGTMをいただきました🙌
最後に
5/28(火)に、「After RubyKaigi 2024〜メドピア、ZOZO、Findy〜」として、メドピア株式会社、株式会社ZOZO、ファインディ株式会社の3社でRubyKaigi 2024の振り返りを行います。
オンライン・オフラインどちらもあり、LTやパネルディスカッションなどコンテンツ盛りだくさんなのでぜひご参加ください!!
また、ファインディでは、これからも新しいものを取り入れつつ、Rubyを積極的に活用して、Rubyとともに成長していければと考えております。
一緒に働くメンバーも絶賛募集中なので、興味がある方はぜひこちらから ↓