オフラインリアルタイムどう書く E26 の実装例(Ruby)

問題 : https://cedretaber.github.io/doukaku/e26/ 実装リンク集 : https://qiita.com/Nabetani/items/0bcabb80bdcbc9b2ff52

負けました。

とりあえず当日中に書いたのはこんなの。

github.com

縦n列目については n + 1 列目にあるやつをうごかせば揃うなーというのは手で動かしてて気づいたので、それで最下段以外は揃えておく。
このあと最下段が揃った後に、最下段にパターンがあるかなーと思ってそれをなんか後から直せないかなというのが回答方針です。が、パターン見つかるわけないよねーということで敗北。

想定解法まであと2ステップほどひらめきが必要だったので、まあ惜しくもないですね。はい。

この後想定解法を実装し終えたらまた追記します。

追記: 2018-09-03 00:00頃

Comparing 4cfee85e1d9745a95caf954bbe485cef0f114e1c...a38c3c3afdd3c2997603d3661871a4f9808e3ef5 · hkdnet/misc · GitHub

解けました。
なんかしらんが x と y を入れ違えてたりでIFの重要性を感じますね……。Coordinate とか作っときゃよかった(´・ω・`)
見どころはたぶん最初の行の全探索のやり方です。パターン生成するのがめんどかったので to_s(4) で4進数表記したものから生成してます。ほかは面白いとこないかと。

@commands = []
n.to_s(4).chars.reverse_each.with_index do |c, idx|
  cnt = c.to_i
  @commands += Array.new(cnt) { [idx + 1, 1] }
end

# 6 なら 12 なので右端から1つめを2回、2つめを1回 まわすとする

WIP: ast でメソッドコールを探すやつを作り始めました + designing data-intensive application を読んでいます

本日は2本立てでお送りします(特にネタがないので小ネタというか終わってないネタを2つ合わせて記事にしとくか……という記事です。

ast でメソッドコールを探すやつを作り始めました

ほしくない?と思って作っています

github.com

使い方は何かと言うとある特定のメソッドの呼び出しを一覧にしたいぞ、というのがもとの欲求です。grep でもいいんですが、せっかく AST parser あるんだから使えばいいじゃん、ということでやってます。まだ NODE でとれるだけです。

grep よりもいい点としてレシーバや引数の情報ももとに検索できることでしょうか。そういう要望があるのかすげーあやしいのですが。あ、でも第二引数がはいってるかどうかをみることでオプション引数がない状態で呼ばれてないか、という確認はできますね。

インターフェースとして今は ruby コード書いてるけどそうじゃなくてなんかいい感じのクエリかけたらいいなーと思いつつ、特にアテがない状況。

テスト環境としては mtsmfm さんの docker image を採用。毎日 trunk でビルドしてくれるのでまだ安定版の存在しない RubyVM::AST のテストにはいいイメージですね。

https://hub.docker.com/r/mtsmfm/ruby-trunk/

designing data-intensive application を読んでいます

読み始めていますがまだ chapter 2 とかです。平易な英語でとても読みやすいのですが単純に英語であるだけで読むのがつらいらしくて全然進みません。すみません。

chapter 1 では、reliable, scalable, maintainable なアプリケーションについて話しています。アプリケーションの良さの指標として前述の3つを挙げて、それぞれについて解説していました。chapter2 では、まあ雑にいうと relational model vs document model という話をしていて、結構面白いです。mysqlJSON 型すら全く使ったことがないのでなんか使うとよさそうなところで使ってみたいですね。

relational model におけるテーブル設計と制約などは少しはノウハウがあるのでなんとなーくわかるのですが、document model においてデータの健全性をどう保てばいいのかよくわかりません。その健全性についても read 時のチェックと write 時のチェックだ、と対比されていましたが正直業務データとか突っ込むには不安が……。個人でやっててもあんまり痛い目みなさそうだし、ダメージが少なそうなところに突っ込むしかないのかなあ……とどう導入しようかいろいろ検討中です。

僕が途中で挫折しないように、感想とかを言える仲間を募集していますので twitter とかでお気軽にどうぞ。

【PR】転職ドラフトで転職してから1年が過ぎたので体験を振り返る

【こちらの記事は転職ドラフト体験談投稿キャンペーンに参加しています】

job-draft.jp

もう転職して1年1ヶ月になりますね。1年も前なので転職ドラフト自体も変わってはいるのでしょうが、体験談を投稿するとアマギフをくれるというので頑張って書こうと思います。


当時の僕は2社目に転職して1年が過ぎた頃でした。技術的に閉鎖的だった1社目から転職しており、当時の会社は技術面では特に問題はない一方で賃金が低いということに対してうっすら不満がありました。そんな中で某 podcast で「転職ドラフトの提示額をもとに翌年の給与の交渉をした」という話を聞いて自分でも試してみたのがきっかけでした。

指名自体は13件頂いていたようです。ありがたや。2017年頭くらいの僕がどんなだったかというと過去記事を参照するのがよさそうです。

hkdnet.hatenablog.com

箇条書きスキルセットはこんなん

  • Ruby + Rails 業務で1年くらい
  • jsをバニラ、jQueryで3年、Reactとか雑にはかけるけど redux とかの設計は無理
  • その他 docker, crystal, php, go とかの経験をちょこちょこ

あんまり経験なさそうだけど、ECS使うとかDeepLearning本一通り実装したとか、そういうある程度の技術トレンドは追ってた感じがありますね。転職する際に有利に働いたんじゃないかな。

指名をもらったあとは、通常の面接が始まる感じですね。面接の内容は、1回目の転職活動と比べて技術レベルとかの話にスッと入っていけた印象はあります。といっても1回目の転職活動はほげナビとかそういうの経由だった1ので比較してよいのかは議論の余地がありそうです。たとえば最近の m○ffers とかと比べてどうなのっていうと、うーんどうなんでしょうね。2
ちなみに m○ffers は登録時にレジュメの添削(人力)があって、そういうところは後発なのもあって頑張ってるなーと思いました。

この後は、転職ドラフト経由での転職おめでとう品をもらうというイベントがあるくらいで他の転職サイトと変わらないのではないでしょうか。転職後すぐの頃は「ある程度マッチングした状態で始まるから楽」とゆーてたのですが、実際に面接官をする立場になってみるとぶっちゃけどの採用経路で入ってきた人でもあんまり変わらない気がしてきました。
なので「転職ドラフト経由でいった会社の面接がやりやすい」と感じていたのは単純な勘違いか、転職ドラフトに指名参加する会社ならだいたい僕がやりやすいという話なのかもしれません。たぶん後者かなあ。

というわけでつらつら書き連ねましたがこんなところでしょうか。転職記念で Apple ギフトカードをいただきまして、それで買った iPad pro はいまでも技術書3を読むのによく使っています。よいサービスだと思っておりますので今後もうまく発展していくといいなとこっそり応援しています。

(1452文字)


  1. 結局リファラルで入ったけど

  2. そもそも m○ffers が、激似すぎるという話があったりなかったりするわけですが

  3. とマンガ

「AESで暗号化した時の文字列の長さってどれくらい変わるの?」という質問に答えようと調べた話

経緯

「AESで暗号化した時の文字列の長さってどれくらい変わるんだろう、って調べているけど全然出てこねえ。実際試してみると1.5倍くらいっぽいが。」

という質問が友人から投げられてきたので調べました。

前提

  • AES256
  • 平文は 99 文字

調査ログ

padding のせいだよ説

AESはブロック暗号方式であるので、ブロックごとの長さに足りない分をpaddingされるだけではないか?と予想。 AESのブロック長は 128bit = 16byte なので Math.ceil(99/16) * 16 = 112 で 112 文字になるのでは?

→ 1.5倍だから150文字くらいになっているのだろう……おかしい

99文字とはいったが1文字1byteとは限らないのでは?

→ ascii 範囲内であることが判明。utf-16とかじゃなければ1文字1byteになりそう(´・ω・`) 99文字 → 152文字らしいので差が53byteある。そもそもブロック長以上の差分が出るわけがないのでおかしい。1

padding 説は濃厚だがそれだけではなさそう。

padding って実際なにしてんの

ここでインターネットに本格的に頼りだす。記事とそこで挙げられている参考実装を読む

www.atmarkit.co.jp

github.com

参考実装だと 128 bit ごとに入れてるのでそれより長い長さの暗号文をどう扱うかが問題。
もう実装読むしかねえという気持ちになり javax.crypto のソースを漁ると Cipher.getInstance("AES/CBC/PKCS5Padding"); という記述を発見。
また、本人から参考にしている Qiita 記事をもらう。

qiita.com

PKCS5Padding

どうも名のある padding 方式らしいので調べたらRFCとかにもある模様。PKCS#5, PKCS#7のバリエーションがあって、基本的に余った文字数が1だったら 0x01で、2だったら 0x02でっていうふうに埋めてく模様。

blog.shin1x1.com

余談ですが、Java の javax.crypto.Cipher にある PKCS5Padding は、名前は PKCS5 ですが、実質は PKCS#7 相当の動きをするようです。

ひどい話だ……。実用上あんまり問題はないんだろうが。やはりこれを読んでても padding でやたら長くなるのはおかしそうだ。

base64 で膨れてる説

Q: もしかして暗号文って最後に==って出てません?

A: 出てるよ

base64 エンコードされていたことが判明。base64エンコードは長さが変わるのでこれっぽい。 平文→暗号化に入るとき(128bit単位にパディングされて暗号化)→暗号化(パディングされたのと長さ同じ)→base64

というわけで 99 bytes を変換したら 152 bytes になるかを検証。

平文: 99 bytes
平文(パディング): 112 bytes
暗号文: 112 bytes
base64: 152 bytes ( = Math.ceil(Math.ceil(112 * 8 /6) / 4) * 4 )

ok

そんでもとの質問は「AESで暗号化した時の文字列の長さってどれくらい変わるの?」だったので、ざっくり1.5倍かなーと思いつつ試算

  4(n+16)/3 >= 3n/2
⇔ 128 >= n

ダメじゃん!2

仮に2倍とっておくと 8>=n におさえられて安心。
input の平文の長さが決まってれば 2 倍じゃなくてかなりきれいに扱えそう。

実験

ここまで理解したところで実験。

encrypted len - Google スプレッドシート

たしかに1.5倍だとところどころ抑えられていないことを確認。 とゆーわけで input の長さによるが、2倍あればおおむねいける。固定長ならもっと攻めることができる、という感じでした。

# https://docs.ruby-lang.org/ja/latest/class/OpenSSL=3a=3aCipher.html
require 'openssl'
require 'base64'

class Encryptor
  def initialize(data)
    @salt = OpenSSL::Random.random_bytes(8)
    @pass = "**secret password**"
    @data = data
  end

  def exec
    # 暗号化器を作成する
    enc = OpenSSL::Cipher.new("AES-256-CBC")
    enc.encrypt
    # 鍵とIV(Initialize Vector)を PKCS#5 に従ってパスワードと salt から生成する
    key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(@pass, @salt, 2000, enc.key_len + enc.iv_len)
    key = key_iv[0, enc.key_len]
    iv = key_iv[enc.key_len, enc.iv_len]
    # 鍵とIVを設定する
    enc.key = key
    enc.iv = iv

    # 暗号化する
    encrypted_data = ""
    encrypted_data << enc.update(@data)
    encrypted_data << enc.final

    encrypted_data
  end
end

1.upto(255) do |n|
  enc = Encryptor.new('a' * n).exec
  enc_size = enc.size
  base64_size = Base64.encode64(enc).size

  puts "#{n},#{enc_size},#{base64_size}"
end

  1. ここで utf-8 は ascii 範囲内の文字は ascii 互換で 1byte 文字であること、日本語の「あ」とかは3byte文字であることなどを説明

  2. というか 99 -> 152 の時点で 1.5倍超えてる

icfp pc 2018の問題を解く、前準備を勧めている

だいたいタイトルどおりの話です。なおまだ問題全然解いてないです。

ICFP Programming Contest 2018

icfp pc はなんか数日間頑張って最適化とかの問題を解いてスコアを競うやつです。今回のはざっくりいうと3Dプリンタ最適化問題で、指定の座標を塗りつぶしたモデルを作成するためにボットを操るやつです。なおかなり前に終了しています。

モデルのデコーダ

モデル = 指定の座標がどこか示してあるやつ。これのデコーダがないとその後扱える気がしなかったのでとりあえず書いたのだが、冷静になってみるとブラウザでレンダリングして簡単そうなのを選んだほうがよかったかもしれない。
フォーマットは、僕の理解ではこんな感じ。

  • 先頭1バイトはサイズrを指定。 r * r * r のサイズの空間でなんかやる
  • 座標(x, y, z)に対応した整数として x * r2 + y * r + z (r進数的に解釈したやつ)を考える
  • 2バイト目以降は座標に対応したビットが立ってるかどうかで判定
    • 2バイト目は [7, 6, 5, 4, 3, 2, 1, 0]
    • 3バイト目は [15, 14, 13, 12, 11, 10, 9, 8]

いまの版ではこんな感じです。最初goで書いてたんだけどrubyのが楽だった。bit操作になれてなさすぎてbitを真面目に扱うより to_s(2) して文字列操作したほうが楽であることが判明した。これは今後も使っていきたいテクニックである

コレ自体はコンテスト期間中も書いてたのだけど、あとで viewer にかけたら chars.reverse の reverse がないせいでバグってたことが判明して昨日直したのと、あといま書いてて r が奇数のときはバイトが余るので to_s(2) しても8桁にならない可能性があって、それを format で直すの忘れてたんのでなおした。

つもりになってたが今直したほうは最上位 bit が立ってないケースも該当してて、それは reverse してるから特に問題なかったな……。はい

require 'model'
require 'coordinate'

class ModelDecoder
  def initialize(src)
    @src = src
  end

  def decode
    return @model if defined?(@model)
    _decode
  end

  private

  def _decode
    @r = bytes.first
    indexes = []
    bytes.drop(1).each.with_index do |b, i|
      base = i * 8
      str = "%80d" % b.to_s(2)
      str.chars.reverse.each.with_index do |c, j|
        if c == '1'
          indexes << base + j
        end
      end
    end
    cs = indexes.map do |i|
      x, tmp = i.divmod(@r*@r)
      y, tmp = tmp.divmod(@r)
      z = tmp
      Coordinate.new(x, y, z)
    end
    @model = Model.new(r: @r, fulls: cs)
  end

  def bytes
    @bytes ||= File.open(@src) do |io|
      io.each_byte.to_a
    end
  end
end

コマンドのエンコーダ/デコーダ

これは特に書いてて面白いことなかったのでパス。固定長じゃなくて2バイト読む必要があったのがめんどかった。配列渡して shift していく方式にして必要なら余分に shift すればおk

viewer

three.js つかって書いたら結構簡単にできてすごーいってなった。texture に適当にそのへんにあった water.jpg を使っているのだが見にくい。
左が公式のやつで右が僕のやつです。公式のやつ影とかついてて偉いな……。

f:id:hkdnet:20180812144930p:plain

なお、コレをみてなんかモデルのデコード失敗してんだなと理解して直した↓

f:id:hkdnet:20180812145026p:plain

そんなに真面目にフロント書くわけじゃないし webpack とか使う気にならず、 ln -s ../node_modules/three/build/three.min.js three.min.js とかで symlink はって終わらせました。

画像が回転できないと話にならないので回転させたんだけど最初は scene の rotate ではなくカメラのポジションをぐるぐるまわして、中心に向き直ってーとか考えてた。
原点を中心とした球の面上を移動すればよくて、(0, 0, R) を始点として zx 側の角度と zy 側の角度がわかれば球上の1点を指定できる気がした……のだがこれがどうもうまくいかない。たぶん球上の点になってないけどベクトルはあってるみたいな状態になってるので何倍かしてやればよかったな、と今更思った。

document.addEventListener("DOMContentLoaded", function() {
  var delta = 10;

  const modelSelect = document.getElementById("model");
  const viewerDiv = document.getElementById("viewer");
  const rightArrow = document.getElementById("right-arrow");
  const leftArrow = document.getElementById("left-arrow");
  const topArrow = document.getElementById("top-arrow");
  const downArrow = document.getElementById("down-arrow");
  fetch("/models")
    .then(e => e.json())
    .then(function(list) {
      const fragment = document.createDocumentFragment();
      list.sort().forEach(e => {
        const opt = document.createElement("option");
        opt.value = e;
        opt.textContent = e;
        fragment.appendChild(opt);
      });
      modelSelect.appendChild(fragment);
    });
  modelSelect.addEventListener("change", function() {
    const value = this.value;
    fetch(value)
      .then(e => e.json())
      .then(function(model) {
        const r = model.r;
        scene = new THREE.Scene();
        camera.position.z = 2 * unit * r;
        model.fulls.forEach(function({ x, y, z }) {
          scene.add(cubeCreator(r, x, y, z));
        });
        requestAnimationFrame(animate);
      });
  });
  rightArrow.addEventListener("click", function() {
    scene.rotateY(THREE.Math.degToRad(delta));
    requestAnimationFrame(animate);
  });
  leftArrow.addEventListener("click", function() {
    scene.rotateY(THREE.Math.degToRad(-delta));
    requestAnimationFrame(animate);
  });
  topArrow.addEventListener("click", function() {
    scene.rotateX(THREE.Math.degToRad(delta));
    requestAnimationFrame(animate);
  });
  downArrow.addEventListener("click", function() {
    scene.rotateX(THREE.Math.degToRad(-delta));
    requestAnimationFrame(animate);
  });
  document.addEventListener("keydown", function(e) {
    if (e.keyCode === 37) {
      leftArrow.click();
    }
    if (e.keyCode === 38) {
      topArrow.click();
    }
    if (e.keyCode === 39) {
      rightArrow.click();
    }
    if (e.keyCode === 40) {
      downArrow.click();
    }
  });

  var camera, scene, renderer;
  const unit = 1;
  const width = 820;
  const height = 820;
  const cubeCreator = (function() {
    const texture = new THREE.TextureLoader().load("textures/water.jpg");
    return function(r, x, y, z) {
      const geometry = new THREE.BoxBufferGeometry(unit, unit, unit);
      const material = new THREE.MeshBasicMaterial({ map: texture });
      const mesh = new THREE.Mesh(geometry, material);
      mesh.position.x += (x - r / 2) * unit;
      mesh.position.y += (y - r / 2) * unit;
      mesh.position.z += (z - r / 2) * unit;
      return mesh;
    };
  })();
  init();
  function init() {
    camera = new THREE.PerspectiveCamera(70, width / height, 1, 1000);
    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(width, height);
    viewerDiv.appendChild(renderer.domElement);
  }
  function animate() {
    if (scene) {
      renderer.render(scene, camera);
    }
  }
});

simulator

やる気にならなくていまブログ書いてるところです

今後

シミュレータがあれば適当に手で生成した命令列を実行したときの結果が見えるから戦略を考えてざーっと確かめられそう。でも下回りが整ったら飽きそうではある。

trick2015 ko1さんの1つ目のやつを読むやつ

前回に引き続き trick コードを読んだのを書きます、といっても実はこっちのほうが先だったのですが。最近これに関連するものを twitter で教えてもらったので書きます。

前回はこちら

hkdnet.hatenablog.com

trick とは

なんかよくわかんないけどすごい ruby コードのコンテストです。すごい ruby コードは ruby に対する深い理解に基づいて書かれていることが多く、読むだけで勉強になったりならなかったりします。

ko1_1

バック・トゥ・ザ・フューチャーネタのほうです。

https://github.com/tric/trick2015/blob/master/ko1_1/entry.rb

コアコンセプトはそんなに難しくなくて、すべてのオブジェクトにアクセスできる ObjectSpace から iso8601 形式で書かれた時刻を昇順で取り出して、unixtime化して 0xff論理積をとるといい感じにコードポイントに落ちてメッセージが表示できる、という感じです。1

一応小ネタを解説しておくと。 Integer#chr は自身をコードポイントとして見たときに対応する文字を1つ返すやつです。対応表は ascii 対応表とかを見るとよいと思います。

97.chr # => 'a' 
'a'.ord # => 97

最終的な出力は eval によって行われています。evalp に変えて実行してみると、ローカル変数 s をごにょごにょした結果は最終的には puts '"Nope. Already been there."' という文字列になり、これを eval して出力してることがわかります。

だけども

iso8601形式の文字列たちが実際の実行コードを作っていることはわかりました。しかし、 ObjectSpace.each_object{|u|s<<u} でその時刻文字列を拾ってこれるのはなんで?という疑問はあります。

例えば以下のコードだとローカル変数 f が保持しているインスタンスを見れるのは妥当な気がします。

class Foo
end

f = Foo.new
p ObjectSpace.each_object { |e| break e if e.is_a?(Foo) } # => #<Foo:0x00007fb5c98fdd70>

一方で今回のコードでは 0.times { } 内で宣言されてる %w リテラルです。これは 0.times なのでランタイムでは実際にオブジェクトを生成する必要はないですし、作成してても GC.start したら消えそうな気もします。

と、ここで RubyKaigi2018 で tenderlove の発表を思い出すと、Rubyソースコードを解釈したあとASTを作成し、VMに実行させるための命令列を作成すること、またその命令列は Ruby 上のオブジェクトであるという話でした。2 つまり、実行していなくても生成した iseq (命令列) の中に文字列リテラルがいるような気がしてきます。僕の理解が正しければあの発表の後半は、そういった iseq 上の Ruby オブジェクトをGCされないように mark array を使っているがそれのメモリ使用量を減らすために〜という話だったはず。

というわけで iseq が掴んでるんじゃないかという推測をしていたのですがどうやって確かめたらいいのかよくわかりません……。iseqが掴んでいるかどうか……?そんなんわかるの……?と思っていたら twitter で教えてもらいました。

↑フクロウとお化けのリプライにカエルが恐縮する図

たしかに --dump=insns で iseq が文字列リテラルを掴んでそうな様子が確認できます。

$ ruby --dump=insns ko1_1/entry.rb
== disasm: #<ISeq:<main>@ko1_1/entry.rb:1 (1,0)-(46,54)> (catch: TRUE)
== catch table
| catch type: break  st: 0000 ed: 0005 sp: 0000 cont: 0005
| == disasm: #<ISeq:block in <main>@ko1_1/entry.rb:1 (1,7)-(39,2)> (catch: FALSE)
| == catch table
| | catch type: redo   st: 0001 ed: 0079 sp: 0000 cont: 0001
| | catch type: next   st: 0001 ed: 0079 sp: 0000 cont: 0079
| |------------------------------------------------------------------------
| 0000 nop                                                              (   1)[Bc]
| 0001 putstring                    "2422-02-10T21:45:38+09:00"         (   2)[Li]
| 0003 putstring                    "2580-06-19T08:53:09+09:00"         (   3)
| 0005 putstring                    "2233-01-20T02:06:42+09:00"         (   4)

また謎 patch により current_iseq というメソッドが生えて iseq にアクセスして object_id をとることができるようになって、確かに object_id まで一致していることが確認できました。まだ謎 patch の中身が完全に謎なのでそのへんを追っていきます。疲れたのでまた次回。


  1. このくらいなら割と素直に読めると思ってしまうが(突然のイキり)読書会参加者の雰囲気を見るとそうでもないのかもしれない

  2. このへんから https://speakerdeck.com/tenderlove/reducing-memory-usage-in-ruby?slide=102

trick2015 yoshi-taka さんの作品を読むやつ

最近毎週水曜日に trick 読書会というのが開催されておりましてそこで読んだネタをブログ記事にしておくやつです。

先週読んだのはコレ。

trick2015/entry.rb at master · tric/trick2015 · GitHub

とりあえずぱっとみ markdown ファイルにしか見えないやつですが、なんと ruby スクリプトとして実行すると正常終了します。不思議

いくつかコアの概念があるので読んでて気づいたものを紹介します。

後置 if と短絡評価の and

ruby スクリプトとして実行するには2つの壁があります。 syntax error にならないこと、例外なく実行できることの2つです。この後置 if は後者の壁をすり抜けるのによく使います。

# 存在しないメソッド呼び出し、ローカル変数参照、定数参照はエラー
no_such_method

no_such_method if false # 後置 if の条件が満たされないので メソッドがなくてもエラーにならない
NoSuchConstant if false # 同様に存在しない定数でもエラーにならない

また同じような処理として11行目などは後置 until の後置 rescue と重ねられています。

後置 if により、この ruby スクリプトのうち実行される部分がめちゃくちゃ少なくなっています。

行末ピリオド

ruby はメソッド呼び出しのカッコを省略できることもあり、適当に単語を羅列していてもなんとなく syntax としては通ります。

foo bar baz # ok
foo(bar(baz)) # 上と同じ意味

ですが、これだけでは英文を書くことはできません。なぜなら改行した瞬間に式の終わりとみなされて評価がされてしまうからです。このまま実行すると NoMethodError で落ちることになります。しかし、それを行末にピリオドを置くことで回避しています。

まるで文末に置かれてる句点かのようにみえる . ですが、 ruby においてはレシーバーに対するメソッド呼び出しの起点になっています。これによってパーザに「まだこの行は終わってないよ」と錯覚させることができます。

Description from Ky.
----------
What if # 以下略

これは Ky に対する - メソッド呼び出しで、引数が単項演算子 -@ が 9個ついた What として解釈されます。 -> Ky.-(---------What)

なのでこの1, 2 行目には後置 if がないようにみえますが実際には3行目の後置 if まで1つの式としてつながっており、キャンセルすることができます。

ちなみに2行目のハイフン列は上記のように解釈されていますが、7行目の場合には単項 -@ を10個ならべた 2020 について9行目で演算する形式になっておりネタかぶりを防いでいます。

さりげない : と - と .

22行目の行末には : があり、英文的にも「以下のように」と実例を見せる感じで使われてますね。でも ruby スクリプトなので : が解釈できるかどうかが争点になります。実際にはこれは like メソッドのキーワード引数 this として this: 'ruby (略)' を渡している形式になります。これはさりげなく上手いなと思いました。

また、ruby -v- は単項演算子 -@ を用いて ruby(-v) となっていますし、 ruby entry.rb. はメソッド呼び出しになっています。コマンドライン上の操作も ruby スクリプトとして syntax ok になっているのもおもしろポイントですね。


今回は2015でしたが2013についてもなんとなく読んではいて、いずれ記事を書くかもしれません。