TRICK2018応募作(解説編)
以下の記事で「解説はまた後ほど」と書いたので、簡単な解説を書く。
TRICK2018とは
RubyKaigi 2018 仙台に合わせて行われた、「普通でないRubyプログラム」のコンテストである。
結果発表の様子は以下のtogetterにまとめてある。(なお余談だがタイムラインが盛り上がりすぎた結果、トレンドにTRICKの文字が入ることとなり、ドラマの方のTRICKクラスタがざわざわしたようである。次回があるとしたら別のコンテスト名になるかもしれない)
その1:SVGで無向グラフを出力するDSL (選外)
コード
$n={};$e=[];class Array;def -@;self;end;def -(o);$n[self]=1
$n[o]=1;$e<<[self,o];end;end;at_exit{a=['<?xml version="1'+
'.0" standalone="no"?>','<!DOCTYPE svg PUBLIC "-//W3C//DT'+
'D SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/s'+
'vg11.dtd">','<svg xmlns="http://www.w3.org/2000/svg" xml'+
'ns:xlink="http://www.w3.org/1999/xlink">'];$n.each_key{|x,
y|a<<"<circle cx='#{x}' cy='#{y}' r='5' fill='red'/>"}; $e.
each{|(x,y),(z,w)|a<<"<line x1='#{x}' y1='#{y}' x2='#{z}'"+
" y2='#{w}' stroke='red'/>"};a<<"</svg>";puts a.join("\n")}
a = [10, 60]
b = [60, 10]
c = [150, 10]
d = [200, 60]
e = [105, 180]
a ---- b
b ---- c
c ---- d
d ---- a
a ---- e
b ---- e
c ---- e
d ---- e
実行結果
着想
superatorsというgemがあって、このテクニックを使って何かできないかと考えた。
感想
.svgファイルは実はテキストフォーマットなので、gemとか使わずに簡単に出すことができる。
拡張として有向グラフ(a <--- b
)とかラベル対応(a <--1-- b
)が考えられるが、実装する時間がなかった。
その2:真に自己言及的なQuine (入賞)
コード
def method_missing(n);$*<<n.to_s.bytesize
n[-1]=="!" and eval$*.map(&:chr).join;end
*自己言及的なプログラム.
これは「自己に言及」したQuineプログラムです.
動かすには普通に無引数で実行してください.
MRIの最新の安定版で動作確認を行っています.
*冒頭の2行が日本語プログラミングのDSLを提供します.
Rubyはピリオドまでの部分がメソッド名であると解釈します.
そのままではNoMethodErrorになります.
それをmethod_missingを使って検知しメソッド名のbytesizeをASCIIコードとして文字にします.
メソッド名が半角の!で終わる場合、記憶した文字たちをjoinしてevalします.
これにより任意のRubyプログラムを日本語により記述することができます.
このプログラムの場合はこのファイルをreadし出力するようになっています.
*Note:文の長さは注意が必要.
UTF_8の日本語は1文字が3bytes.
端数が丁度になるよう英語を入れる.
以上、自己言及的programでした!
実行結果
上記がそのまま出力される。
着想
日本語を使って何かできないかと考えたもの。
その後Quineにするというアイデアを経て、プログラム自体がremarks.markdownを兼ねている、というアイデアに行き着いた。
プログラム自体が自分に言及している、というコンセプト。
実装
識別子に多バイト文字が許されるのを利用し、日本語部分をmethod_missingで拾う。
各行のバイト数がASCIIコードになっていて、上記をデコードすると$><<IO.read($0)
というプログラムになる(print File.read($0)
と等価)。
感想
バイト数がちょうどになるように日本語と半角英数を混ぜるのが楽しかった。数時間しかかけてないわりにはわりと自然な文章にまとめられたのではないかと思う。
その3:単項演算子によるテキストのエンコード (選外)
コード
class O;def ~;@a<<[];self
end;def+@;@a||=[[]];@a[-1
]<<1;self;end;def-o;puts(
o.s)end;def-@;@a[-1]<<000
self;end;def s;@a.map{|a|
a.join.to_i(2).chr}.join;
end;end||TRICK;(_=O.new)-
-+-+~-++++++~-----+~---+-
++~--+-+++~+-+-++~-+--++~
-----+~+--++++~-+---++~+-
+-+++~-+--+-+~-----+~++--
+++~--+-+++~+----++~-+--+
++~+++--++~-+++-++~++++-+
+~++----+~-----+~-++++++_
出力
~ Congrats Ruby 25th ~
ちなみにMRI以外にJRuby、mruby、Opalでも動かすことができる。
* ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-darwin16]
* jruby 9.1.16.0 (2.3.3) 2018-02-21 8f3f95a Java HotSpot(TM) 64-Bit Server VM 9.0.1+11 on 9.0.1+11 +jit [darwin-x86_64]
* mruby 1.3.0 (2017-7-4)
* Opal v0.11.0
着想
これもsuperators gemのテクニックを使えないか、というところから始まったもの。
もとはJUST ANOTHER RUBY HACKERと出していたのだけど、その4を作ったあとに連作にするというアイデアを思いつき、サイズを25x7にし、メッセージもRuby 25周年にちなんだものにした。
実装
ある式に任意個の単項演算子を付けられることを利用している(-1
, --1
, ---1
, ...)。プログラムの後半部分は大きな一つの式になっている。末尾の_に、たくさんの単項演算子が付いた形。
エンコードは0/1が-/+で、~は文字間のデリミタ。
感想
こういうシンプルで綺麗な見た目の作品が好きなので、わりと満足している。
心残りといえば、メッセージが+から始まることに依存したコードになってしまったこと。最初は+/-のどちらでも大丈夫なコードだったのだが、Opalとmrubyにも対応させたことで改行できる箇所が減って、データ部と同じ四角におさまるようにゴルフできなかった。
その4:コードとデータを左右に併置する (佳作)
コード
I=Integer;O=[ObjectSpace, 1042168250852542240864495
].first;s=10**24;at_exit{ 5348222601280368651569634
a,=O.each_object(I).sort, 4780579399978966787453143
e=s*10;b=a.select{|n|0<=> 7807076270124060079384842
s<n&&n<e}.map{|n| n.to_s{ 1001849981903442212561686
}.slice 1,25}.join.to_i|| 4544119307892328259442112
$><<["%x"%b].pack('H*')}; 3990956024951047161425313
出力
~ Congratulations Ruby 25th ~
TRICK FINAL in RubyKaigi2018 Sendai
備考1:数値の最初の桁の合計(1 + 5 + 4 + 7 + 1 + 4 + 3)と最後の桁の合計(5 + 4 + 3 + 2 + 6 + 2 + 3)がいずれも25になっている。またすべての数値の合計が35514971836982755392701225で、25の倍数になっている。
備考2:左のコード部分は、単独でvalidなRubyプログラムになっている。以下のように数値を削除して実行しても、例外にならず実行することができる。
I=Integer;O=[ObjectSpace,
].first;s=10**24;at_exit{
a,=O.each_object(I).sort,
e=s*10;b=a.select{|n|0<=>
s<n&&n<e}.map{|n| n.to_s{
}.slice 1,25}.join.to_i||
$><<["%x"%b].pack('H*')};
着想
その3の初期バージョンを書き上げたあと、このブロックを左右に配置できないか?というアイデアを思いついた。データ部分を何にするかは少し考えたけど(配列とか)、とりあえず数値が簡単そうだったのでそれで進めた。
実装
Bignum(Integer)のインスタンスをObjectSpaceで拾う。このテクニックはmameさんの本で解説されているもの。メッセージの数値へのエンコードも同書のものをそのまま使っている。
ただし今回は数値が複数なので、順序を保証するために最上位桁にprefixを付けている。これは合計が25になるように選んだ。最下位桁の合計が25なのと合計が25の倍数なのは実は単なる偶然である。
左を独立したRubyプログラムにするというアイデアは最後に思いついた。このために25>= 1001849981903442212561686
のようにして捨てていた箇所を0<=> 1001849981903442212561686
に直すなどした(次の行がs<n
なので、boolを返す演算子だとfalse >= s<n
になってエラーになってしまう)。
また初期段階では「各行で全て違うテクニックを使っている」というコンセプトがあって、
n.to_s[ 1001849981903442212561686
.imag+1..-1]
みたいなコードになっていたのだが、これも左が独立にならないため「to_sに実行されないブロックを渡す」というテクニックで置き換えた。.slice
みたいに空間非効率なコードが混じっているのはこれにより空間が空いたためである。
感想
エンコード手法が借り物なのは残念だけど、データを上下でなく左右に配置すること、左右がそれぞれ独立していること、あと上下に並んだものとの連作になっていること、という点で満足行く出来になったと思う。まあ結果発表では「あるブロックを上下左右に好きに並べられる」という超絶プログラムが出てきたんだけど。
その他
- 3日くらいずっとコードゴルフしてた気がする。その3とその4は矩形にする他に、「selfやendが縦に並ばないようにする」という自分ルールがあり(その方が綺麗だから)、メソッド定義の順序を延々入れ替えてうまくはまるまで待つみたいなことをしていた。
- 締切は3/31だったけど、mameさんが「3/32まで待ちます」と言うのでもう一日粘ることになった。結果的に完成度が倍くらいになったので良かった。
- 着想というか「この方向で何かできないかな?」というのは10〜20個くらいあるんだけど、どうやったら作品という形になるかを思いつくのが難しい。
- 作品にならかったアイデアは、例えば「Rubocopの全ての警告に引っかかるRubyプログラム」とか、「&.演算子をテーマにした何か」とか。
- 逆にいったん作品としての形ができると「もっとこうできないか?」という課題が次々に思いつくので、作業としては大変だけど思考としては簡単になる。
- remarksは作品の一部であるということ。
- 今回はremarksをわりと頑張って書いた。コンセプトがどれだけ格好良くても、審査員に気づいてもらえなかったら意味がない。
- 作品のことは自分ではよく分かっていても、他人が初見でどう考えるかを正確に予想するのは難しい。無粋すぎるくらい徹底的に書いたほうがよい。
- 作品の一部なので、締め切り後に気づきがあってもremarksを更新することはできない。その4は左だけじゃなく、右も完結したRubyプログラムなんだよなあ。 「左右がそれぞれ完結したRubyプログラムになっています」って書けば良かった。(こういうこともあるので制作作業は早めに始めたほうが良いと思う)