Edited at

前に書いたMatzLispスクリプトについて説明する


はじめに

個人的にはおもしろコードが書けた気がしてるのだけど、誰も評価してくれないので、自分のギャグについて自分でことこまかに解説する。

choise.jlc.rb

尚、以前書いたラムダ式で学ぶクロージャとDSLは「ふざけた文法を例にとった、まじめな入門記事」を意図して書いたが、この記事は「純度100%のおふざけ」であることを注意されたい。

仕事でこんなコードを書かれたら、どんな事情があったとしても僕は殴る。


Lispについて


(defun factorial (n)

(if (<= n 1)
1
(* n (factorial (- n 1)))))

ja.Wp - LISP より引用(2014年7月18日 (金) 13:16; Zilog80xによる版)


括弧で構文を表現する。Cライクな言語では foo("arg1", "arg2") とするところをLispでは (foo "arg1" "arg2") と書く。あとは察してほしい。フィーリング重点だ。


解説


ファイル名

.cljはプログラミング言語Clojureの代表的な拡張子である。だが、Matz Lispは特にClojureに似せたわけではない(どのLispにもまったく似てないし、言語的なハックは何もしてないので、所詮RubyはRubyである)。

ところで、なぜファイル名に br. が含まれるのか、あなたは想像を巡らしてみるべきだ。


無名クラス

通常のクラスはclass構文を使って定義されるが、Class.newで無名クラスを作成することもできる。無名クラスが必要な理由もとくにないのだが、後述するように特異メソッドを定義するために使ってゐる。

(Class.new do

# ...
end)


define_singleton_method

(define_singleton_method :let, ->(*){})

define_singleton_method はオブジェクトに特異メソッドを定義する。第一引数はメソッド名だ。第二引数は任意個の引数をとり、何もしない(nilを返す)Procオブジェクトだ。

letメソッドの存在は特に何も意味しないのだが、Lisp系言語(およびOCamlなどのML系言語では局所的な変数の定義に let を使った構文を利用することがある。

letについて語りたいことはいくつかあるが、さしあたって(Lispではないが)過去にF#のletはかっこいいのだ - DT戦記(zonu_exeの日記)なる記事を書いたことがあるので、興味のある型は一読いただけると良いかもしれない。


letメソッド

 (let

[(display = ),
(guys = ),
(atnd = )])

前述する通りLispのletは局所的な代入式なのだが、これを見かけ上だけ摸倣してる。ここで注意したいのが、Lispのletとは はたらきがまったく異なる ことに注意されたい。メソッド呼び出しの中で書いた代入文はどのように作用するのか? 気をつけてみよう。


display手続きオブジェクト

(display = ->(ary){

(puts \
%(\nattendee: ) +
%([) +
(ary.send(:join, ?, + ' ') + %(])))})

順を追って説明する。displayには手続きオブジェクト(lambda)が代入される。「手続き」とはなんとも珍妙な日本語なのだが、これは英語の “procedure” の翻訳で、処理をとりまとめたものだ。プログラミング言語の文脈では「関数/函数」と呼ばれることもある。

->(arg){ … } は、手続きオブジェクト(lambda)を生成する特殊な構文で、いはゆる無名函数を生成する。

puts \について。\の次には改行がある。これは「行継続」を意味する。Rubyではメソッド名だけを書いて行を終ると無引数でメソッドを呼び出したことになるので、メソッド呼び出しに括弧をつけず、引数を同じ行に書きたくない場合は、このように書くことができる。

ここで実用におもねった説明をするのも業腹ではあるのだが、安易にまねをされても迷惑だから一言注釈をしておくと、そのような場合は行継続ではなく、あらかじめ一時変数に文字列を作っておいて puts str のようにメソッド呼び出しの行を短くするか、puts(のように書いてから改行した方が見やすいことだらう。今回はコードの見ためを調節するために利用したに過ぎない。

%()はあまり見ない記法だが、""で文字列を作るのまったくと同じだ。説明はRuby 2.1.0 リファレンスマニュアル - リテラル#%記法にある。その文章中にある %!STRING! : ダブルクォート文字列 がそれなのだが、%!!系の構文はお好みの文字を利用できる(%/a/%{b}と書いてもよい)ので、まだお馴染みのない型には注意されたい。

ary.send(:join, …)ary.join(…) と同様だ。しかし、呼び出すメソッド名を明示することができる点が異なる。ただし今回はそんなことは一切関係なく、すこし複雑に見せかけたかっただけだ。


quote

 (guys =

'(tadsan
tadakiti
tadsong
tadstudy
be_tadsan
__hoge__
zonu_exe)
'
),

ここで一度Lispの話に戻りたい。LISPは歴史的に LISt Processing に由来するとのだされることからわかるように、「リスト」を操作することを基本とするプログラミング言語だ。そのプログラムもリストとして表現される。

リストを人間にわかりやすい記述として表記する方法が S-expression、日本語で「S式」だ。S式ではリストを (りんご みかん バナナ) のように、両端を () で囲って、空白文字で区切る。また、 ((りんご 10) (バナナ 20) (みかん 15)) のように、リストのリストを作ることもできる。

そうしたとき、冒頭で紹介したLispのサンプルコードのように (* n m) のように書いたとき、「*nmのリスト」なのか、「n * m を意味するプログラム」なのか、わからなくなる。

そこで、「プログラムとして解釈されたくないリスト」を作りたいときには (quote (りんご みかん バナナ)) と書くことにする。しかし、いちいち (quote ) で囲むのもめんどくさいので、縮めて '(りんご みかん バナナ) と書いてもいいことにする。これで、データとしてのリストを表現しやすくなった。

……と、上の全部をぶっこわすようで悪いのだが、ここはRubyの世界だ。よって 'quote の省略記法ではない。ただの文字列リテラルだ。

もしこれがLispだったら、Ruby流で書くと [:tadsan, :tadakiti, :tadsong, …] のようなリストが返ってきてほしいところだ。

しかし、

"(tadsan\n tadakiti\n tadsong\n tadstudy\n be_tadsan\n __hoge__\n zonu_exe)\n "

スペースと改行がいっぱい入った変な文字列だこれー。現実はきびしい。


処理の核心

 (atnd = ->(‌){

(loop do
(let (‌‌= (‌.sample 5))) &&
((‌‌.include? :tadsan)?
(break (‌‌)):
(p (‌‌)))
(next) end)}.
(eval \
(?%+?i+ (guys)) ||
(atnd)))

自分で言ってみるのもなんだが、奇妙なコードだ。

Lispでは (= 1 2) は概ね 1 == 2 の意味だ。しかしRubyではそんなことはない。ご存じの通り代入文、つまりオペランド(被演算子)には左辺(代入される変数名)と右辺(代入する式)が必要だ。

それに、. も同様だ。self.name を省略して name と書くことができるが、 .name などと書くことはできない。

……などと自分で言ってて白々しさにも程があるので、Syntax highlightingしてみよう。

カラーリングしたソースコード

(Qiitaはプレビュー時と本文投稿時で強調表示のスタイルが異なるようだ。この画像はプレビュー画面のハードコピーだ)

種明かしをすると、なんだつまらんと感じたのではないか。これはZero-width non-joinerと呼ばれる文字だ。

しかしそのあとの (eval) についてはどう判断しようか?

 (eval \

(?%+?i+ (guys)) ||
(atnd)))

?%+?i は、ただの文字列結合だ。(guys) はただの文字列なので、()で括ることには特に意味がない。 || (atnd) は、ただのダミーだ。本当に何も意味がない。

ところで、この構文を奇妙には感じなかっただろうか。

 (atnd = ->(){


}.
(

))

これは、ちゃんと書くと (atnd = ->(…){ … }.(…)) だ。非常に知名度が低いのだが、obj.(arg)obj.call(arg) の構文糖(Syntactic Sugar)だ。

このコードでしっかり obj#call メソッドが呼ばれることは、次のような短いコードで確認できる。

class Poor < BasicObject

def call (*args); "Called#{args.inspect}"; end
end
p Poor.new.(:foo, :bar, fizz: 'Buzz')
#=> "Called[:foo, :bar, {:fizz=>\"Buzz\"}]"


起動

さあ、以上まででメインルーチンは完成したので、あとは起動するだけだ。

 (display. (atnd))) end)

前の章で説明した通り、display.call atnd の構文糖だ。特に補足することも残ってないのだが、わざわざ . ( と空白をあけたところがわざとらしくひどい。もちろん、ふつうは display.(atnd) と、間を空けずに書くべきだろう。

end) はコード冒頭から始まる (Class.new do の終端だ。わざわざ無名クラスを定義したのだが、メインのルーチンは既に終了したので、哀れなことにせっかく作られたクラスのインスタンスが生成されることはない。


ふつうのバージョン

特におふざけもせず、ゴルフもせず、ただの変哲もないRubyスクリプトとして書くと、このようになるだろう。

names = %i(

tadsan
tadakiti
tadsong
tadstudy
be_tadsan
__hoge__
zonu_exe
)

def display (arg)
puts "\nAttenndee: [#{arg.join ' '}]\n"
end
atnd = ->{
loop do
member = names.sample 5
p member unless member.include? :tadsan
return member
end
}
display atnd.call

やっぱりふざけたほうがたのしいです ヾ(〃><)ノ゙☆


あとがき

昨年のRubyKaigi 2013には TRICK 2013 (第一回 超絶技巧 Ruby 意味不明コンテスト in RubyKaigi) なるコンテストがあり、本稿を書く途中で本件の存在を思ひだしてことしのTRICKに応募することを検討したが、ことしはどうやら実施されないようなので承認欲求を満たすべき安心して本稿を投稿する。

TRICK 2013の授賞式のようすは http://rubykaigi.org/2013/talk/S53 で見られるので、へんなコードを読むのが趣味の型は、ぜひ御覧いただきたい。

また、もしこの奇妙な記事を呼んで、ほんのすこしでもプログラミング言語について興味の湧いた型がいらっしゃるならば、つくって学ぶプログラミング言語 RubyによるScheme処理系の実装 - 達人出版会を読むと良い。