RubyKaigi 2024をきっかけにQuineに入門してみた

ハイサイ、ファインディで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の記事にあるので省略しますが、簡潔に書くと以下の通りです。

  1. 圧縮したAAデータbinを元に、空白またはコード1文字分をoに追加していく
  2. 最初に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を実行するとそのまま文字列が出力されてしまいます。

(1敗)

しかし、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やパネルディスカッションなどコンテンツ盛りだくさんなのでぜひご参加ください!!

findy.connpass.com

また、ファインディでは、これからも新しいものを取り入れつつ、Rubyを積極的に活用して、Rubyとともに成長していければと考えております。

一緒に働くメンバーも絶賛募集中なので、興味がある方はぜひこちらから ↓

herp.careers