ふるつき

私は素直に思ったことを書いてるけど、上から目線だって言われる

Ponyがいい言語に見える

 最近D言語と倦怠期に突入して(classを引数にとって中身をがんがんいじっていた自分のプログラミングが嫌になり、structを使えばいいのかclassを使えばいいのか考え出して死んだ。そもそもぱっと出てきた変数が値型なのか参照型なのかわからないの辛くないですか? 僕は Rangeはclassだと思ってたよ)、

書いていて楽しいプログラミング言語を探す旅に出たんですが、その中でであったものの一つがPonyでした(他にはElixirとかCommon Lispをみてた。やつらも楽しい感じがありますね)。

github.com

 Ponyは静的型付けのオブジェクト指向コンパイル言語だけど、核にあるのはActor Model な並列プログラミングだと思います。私はActorモデルよく知らないので全然わからないけど、言語仕様を見ていて好きな感じがしました。私はPonyのこんなところがスキっていうのを列挙します。

  • 静的型付け(強そう)である
  • capability って言われる(ほんまか)制約みたいなんがあって格好良い
  • コンパイルする言語でバイナリが結構小さい
  • GCがあるっぽい
  • プログラムの見た目が好き
  • 例外の扱いが好き
  • その他なんとなく哲学が好き(グローバル変数がないとか、stdoutやargsは全部Envというクラスのインスタンスにまとめられてエントリポイントに渡されるとか)
  • ドキュメントを結構たくさん書いてくれてる

まあなんというか、書いていて楽しそうという感じがしました。何故か日本語の情報が少ない*1 のですが GitHub では Syntax Highlight されてるし多分メジャーになってきてるとか、メジャーだとかそういう言語なんだと想います。


ちょっと見て下さい。これはフィボナッチ数列を列挙するプログラム*2です。

primitive Fibonacci
    fun fib[A: Integer[A] val](a: A, b: A, out: OutStream) =>
        out.print(a.string())
        if (a+b) < a then
            None
        else
            fib[A](b, a+b, out)
        end

        
actor Main
    new create(env: Env) =>
        try
            let a = env.args(1)?.u128()?
            let b = env.args(2)?.u128()?
            Fibonacci.fib[U128](a, b, env.out)
        end

プログラムは Main actor のコンストラクタから始まります。コンストラクタは new <constructor name>(args) => <body> という形になっていて、脱線すると自由に名前をつけることができます。実質オーバーロードみたいなものですが。Main と書いたら引数なしの create が呼ばれるけどこれは Main.create() でも同じことです。 もし new with_arg(arg1: U32) みたいなコンストラクタが定義されていたら Main.with_arg(10) みたいな感じでコンストラクタを呼び出すこともできます。なんか好き。

閑話休題で、まあその下に書いてあるのが body だっていうのはわかると思われます。何をしてるかも雰囲気で読めますよね? あと、これはちょっと罠っぽいんですが、インデントはプログラムの挙動には関係ないです。その代わり、フィールドの宣言、コンストラクタの定義、関数の定義、befavior の定義っていう順番に書かないとダメという決まりがあります。なるほどね。

env.args はなんとなく Array[String] みたいな型を持ちそうということは推測できそうですが((実際には val という immutability を表す capability がついて Array[String val] val なんですが))、これにいくつ要素が入ってるかわからないので その () オペレータ (( apply という関数の呼び出しなんで args.apply(1)? とも書ける)) の返り型は A? です。これは A 型の値を返すかあるいは 例外を発生する事を表していて*3Javathrows みたいなもんですね)こういう関数*4は呼び出すときに ? を付けないとだめだし、呼び出している関数も partial function になるか、それとも try...else...then...end 構文で例外を捕捉しに行かないとだめです(おわかりかと思いますが elsecatch 相当、 thenfinally 相当です)。

また脱線すると例外の扱いがすごい好きで、いわゆる raise とか throw に当たるのは error という構文なんですが、これの面白いのが値を一緒に渡せないし型がないところで、つまり一切の例外を識別できないんですね。これはパフォーマンス的な理由からと書いてあるような気がするんですが、私はこれかなり好きです。例外ハンドリング嫌いなので(でもこれDBのカラム名間違ってましたみたいなエラーの原因特定するの難しそう)。

まあそんな感じで、もし引数があれば、それは String なので u128 関数で U128 型の値として取り出します。当然これも失敗しうる。ちなみに整数型は U8 U16 ... U128I8 ... I128 に加えて {U,I}{Size,Long} があります。

次は fib の呼び出しなんですが先に定義の方を解説します。

primitive はいろいろな使われ方をしていて enum のように使うときと 「メソッドまとめ」のように使うときとあります。今回は後者。 Main の関数でも良かったといえばそうだけど。

なんか雰囲気ありますがこれは Generics を使っていて ちょっとどういう書き方でこうなってるのかわかってないんですが、 「Integer に属していて読み取り専用」 の型Aを引数に取っている気持ちです。やってることは単純なのでわかるとおもいます。あ、末尾最適化があります。

定義がわかると呼び出しもわかります。いい感じの言語に見えませんか?

そう言えばコンパイルも面白くて、特定のディレクトリ以下で ponyc とすると勝手にソースコードを探索する仕組みになっています。便利と言えば便利だけど慣れないのでキモいですね。 make してる気持ちに慣ればいいんでしょう。できるバイナリはディレクトリ名をしてます。


もういっこサンプルを。折角Actorモデルがつかえるので書きたかったんですがわからなかったやつ。

use "collections"
use "itertools"

actor Worker
    be calculatePiFor(receiver: Main, start: U32, stop: U32) =>
        let iterator: Iterator[U32] = Range[U32](start, stop)
        let x: F32 = Iter[U32](iterator)
            .map[F32]({(x: U32) => x.f32()})
            .map[F32]({(x: F32) => 4.0 * ((1-((x%2)*2)) / ((2*x)+1))})
            .fold[F32](0.0, {(sum: F32, x: F32) => sum + x})
        receiver.receive(x)
        
actor Main
    var _pi: F32 = 0.0
    let _range: U32 = 10000
    let _actor_nums: U32 = 1000
    let _out: OutStream
    new create(env: Env) =>
        var start: U32 = 0
        for i in Range[U32](0, _actor_nums) do
            Worker.create().calculatePiFor(this, start, start + _range)
            start = start + _range
        end
        _out = env.out


    be receive(x: F32) => 
        _pi = _pi + x
        _out.print(_pi.string())
        

これは、 ライプニッツの公式による円周率の導出です。 1000個の actor に計算を投げてそれの集約をしてる……んですが、うまく create の中で actor の計算終了を待って出力みたいなことができませんでした。どうやるんだろ。

ところでこれも気持ちがあると読めると思うんですが、 beactor 特有のアレで、 behavior の略です。なんでこんな略し方をしたのか。 Rust の fn みたいな反感の買い方をしそう。まああの、 be は partial にできなかったり返り値を返せなかったりしますが*5、そのかわり勝手に並列になります(一つのActorインスタンス内で並列になることはないので安心)。

あーあとすっごい不思議なんですが、すべての演算子には 括弧がついていて順序付けされている必要があります。 1 + 2 * 3は許可されて無くて (1 + 2) * 31 + (2 * 3) じゃないとだめ(でもなぜか 1 + 2 + 3 のように同じ演算子の連続はOKらしい)。これ鬱陶しい。ついでに U32 + F32 とか U32 + U128 みたいな演算は定義されてないので悪しからず。さらに言えば 加算代入演算子っていうんでしたっけ、 こういうの += も存在しません。この辺なんでだろ……。

まあこんな感じの言語があって良さげだし、もうちょっと勉強したいですね。今回は capability にも言及できなかったので(わかってないから。良さそうに見えるけど)、そこもどうにかしていきたいです。

*1:矢二郎の顔ばかり出てくる

*2:本当にどうでもいい話だけど math package が最高に面白い

https://github.com/ponylang/ponyc/tree/8a8ee28f8dec1f5336f9c6e3176e41d133e5b68b/packages/math

*3:型としてはA or None を表している

*4:partial functionと呼ばれる

*5:つねにNoneが返ることになってる

SECCON2017 国内決勝に出た

KOSENSECで優勝したので theoldmoon0602、thrust2799、kyumina8376、miwpayou0808 からなる insecure というチームで SECCON 国内決勝に出ました。 結果は 300pt で 24チーム中 19 位でした。私は300pt をSubmit したので一応WriteUpを書きます。はあ〜〜〜〜負けた。

https://i.gyazo.com/c73c95915dee890e2a2d8ffe15b1985f.png

梅田

画像Uploaderの問題です。King of the Hill なので flag を書き込むべきページみたいなのがあって、そこにでかでかと SECCON{hogehoge} が鎮座していました。他のチームはこれを速攻で見つけていたっぽくて開始と同時に AC がN回行われていました。クソ焦る。私は 船橋を検討して無理だな! と結論付けた後にこれを見つけてSubmitした。梅田に取り組んでいるチームメイトもいたんですがね。 100pt

幕張

golang 製のバイナリが二つ渡されます。まずはAだけが配られていたのでAについて話す。最初は「えっなにこれ即終了するじゃ〜ん。私の環境では動かねぇのかよ」とか思って放置していましたが、 miwpayou0808 がちょっと解析して 適当に command line arguments を渡すと usage を吐いてくれることを教えてくれてやる気が出たのでもうちょっとやることにしました。

よくわかんないけどバイナリを IDA でみていたら END PRIVATE KEY みたいな文字列に遭遇して「おっ秘密鍵ジャーン」と思ってコピー&ペーストしてvim で整形しました。鍵があるということは通信しているとあたりをつけて、 wireshark でパケットを監視しながらもう一度起動しました。秘密鍵もってるし秘密通信を解読できるね! と思っていたら解読できなかったんですが、鍵交換アルゴリズムの中にフラグっぽい文字を見つけました。

後から調べたら、このバイナリはもう二つ、公開鍵証明書みたいなものを持っていて、それの mail address の欄がそうなっていたっぽいです。

この頃にはこのバイナリが MQTT みたいなプロトコルで通信していることがわかりました。 もう一つのBのバイナリも MQTT で通信するプログラムっぽかった。

けどいろいろよくわからなくて時間を無限に溶かしていましたが、 無限に溶かした時間のうち少しが有意義だったっぽくて、 MQTT には topic なる概念があって、まあ IRC で言う channel みたいなものだと思うんですけど、まあそういうものがあるということがわかりました。

で、このバイナリが何ていうchannel で通信してるかを調べたら、サービス内に飛んでるメッセージを拾えると思ったので、 クライアントはここから https://qiita.com/n-yamanaka/items/91dbd7bd9fed5b3fbed4 頂戴してきて、バイナリの中から topic っぽい文字列を探そうとしたのですが、 IDA でも gdb でも全然わからず、 miwpayou0808 が探してきていた delve という go binary 用の gdb みたいなやつを使いました。 b Subscribe→c→si→args でSubscribeしてるときの引数を見られたんですが、ここに /mkhr/v/1/unlock/1 のような名前があって、 「これじゃーん!」となりました。でもこれに対して subscribe してみても全然飛んできませんでしたどういうことだよ。

miwpayou0808 がもう一個のバイナリもおんなじように調べたら今度は 別のtopic 名があって、そっちを subscribe したらいろいろ飛んできて「うーわこれじゃん」ってなりました。その中にしれっと FLAG も飛んできてた。

多分これで subscribe の代わりに defense キーワードを publish すると defense ポイントが入るような気がしたんですが↑の時点で残り時間が 5分もなかったので意味なし!!!!!

追記

さいこうですね


これで 300 点でした。 miwpayou0808 は基本的に 幕張の rev をやってて、残りの二人は府中梅田船橋を横断していたっぽいですけどよくわからなかった。完全に食いの残る競技と言うかはーつらい。つらいなー。insecureは弱いということです。


どうでもいいことですが懇親会みたいなやつ、完全に電池が切れて端っこで座ったり寝たりしていました。人間がたくさんいて喋ってるのが本当に苦手っぽい。友達を作り損ねた。

来年はもう参加できないと思うけど参加できたら師匠に出てもらってもうちょっとまともに戦えてる風を演出したいです

手書きLLパーサにおける左結合性を持つ演算子の左再帰をループで解決する

 まあ人間が弱いと、構文解析再帰を書き下すしかできないんですが、ナイーブな実装だと左再帰問題に出会います( A->A+B みたいなルールをコードに落とし込むと、 parseA の呼び出しで無限再帰になる)。右結合性の演算子のパースならぽいっとできるんですが、左結合の演算子だと苦戦しました。いろいろグーグルしてみるんだけど気持ちが上がらないとちゃんと読まないし、私が読めるコードが欲しい気持ちになったので書き残しておきます。n回ここに来るんだろうなぁ。

 ちなみに a + b + c(+ (+ a b) c) になるのが左結合性、 (+ a (+ b c)) になるのが右結合性です。

gist.github.com

なんということは無くて、ただのループです。こんなのは知ってるかどうかという気持ちになってきた。 ちなみに明らかに parseAddparseMul が同じ形をしているのでテンプレートとかmixinに落とし込みたいところですよね。 任意の左結合性二項演算子を parseする関数を返す高階関数とか。

これをOCamlで書けるようになりたい。

言語処理100本ノック2015 をD言語でやる【第1章】

 自然言語処理別に興味あるわけじゃないんですが、 yamasy がこれをやっててなかなか楽しそうじゃんと思って練習がてらです。

yamasy1549.hateblo.jp

私はD言語でやりたいのでD言語でやります。 リポジトリは これ。

github.com

00. 文字列の逆順

char[] answer() {
    import std.algorithm;
    char[] str = "stressed".dup;
    reverse(str);
    return str;
}

D言語だと、 BidirectionalRangeRandomAccessRange を反転できる reverse 関数が algorithm パッケージにあります。これは受け取った Range を変更するものなので、引数は mutable である必要がある。個人的には新しいRangeを作って返してくれればいいのでは……と思いますが。とか書きつつ調べたら、遅延評価で逆順に辿ってくれる retrorange パッケージに用意されていました。こちらを使って書くとこうですね。

string answer() {
    import std.range, std.array, std.conv;
    return "stressed".retro.array.to!string;
}

retro の返す型は Result みたいな独自の型なので array で配列に直してあげるとか、 D言語では文字列を Range として扱うときは dchar の Range になるから tostring に変換してやってるとかそういうイディオムが登場してます。D言語でこういう文字列を扱うとき、どういう型で受取り、どういう型で処理をして、どういう型を返せばいいのかわからないです(この例でも char[]string は別の型なので……)。

01. 「パタトクカシーー」

char[] answer() {
    import std.range;
    import std.array;
    import std.conv;
    dchar[] patatokukasy = "パタトクカシーー".to!(dchar[]);
    return patatokukasy[1..$].stride(2).array().to!(char[]);
}

日本語を文字単位で扱うために dchar の配列にしてます。 dcharUTF-32 のcode unit一つに対応していて、今の所 UTF-32 なら日本語一文字を 固定長で表せます(Code Unitってなに)。なんか rune みたいな型ほしいですよね(これはよしなにしたいという意味)。

それでですが、奇数インデックスを持つ要素だけを抽出したいので range.stride を使いました。これは Range (すくなくとも InputRange なのでわりとどんなRangeでも) を n-1 個飛ばしで辿るRangeにしてくれる関数です。 スライスって言うのかわかりませんが、配列の範囲を [1..$] ($はアクセサの中では「配列長」になる)にして先頭一文字を抜いた配列を対象にしてるので「タ」から1文字置きに要素をたどれます。Rangeが返ってきているので array で配列に変換してやり、 dchar の配列から char の配列に変換しておきます。なんとなく、インターフェースとしては char[] を扱うのがいいと思っているので。もしかしたら普通にRangeを返すのが関数としては綺麗なのかもですね。わからん

ところで、 Rangeの先頭を落としたRangeを返す dropOne という関数もあったので patatokakusy[1..$]patatokakusy.dropOne() でいいですね。

02. 「パトカー」+「タクシー」=「パタトクカシーー」

char[] answer() {
    import std.range;
    import std.conv;

    string str1 = "パトカー";
    string str2 = "タクシー";

    dchar[] result;

    foreach (c1, c2; zip(str1, str2)) {
        result ~= c1;
        result ~= c2;
    }

    return result.to!(char[]);
}

こういうのは見た瞬間に 「 zip だ!」ってなりますね。今回は foreach で書きましたが zip.map.join でもよかったかも。

03. 円周率

size_t[] answer() {
    import std.array;
    import std.algorithm;
    import std.uni;

    string given = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.";
    return given.split(" ").map!(w => w.count!(isAlpha)).array;
}

単語に分割して単語ごとの isAlpha にマッチする文字数を数えています。 D言語の関数の喚び出し方は基本的に func!(template arguments)(arguments) で、必要ない () は省略できる && 関数の第一引数はレシーバのようにかけるので(f(a,b)a.f(b) とかいていいし、 引数bを取らないなら a.f でいい)、こんな呼び方になってます。

04. 元素記号

string[size_t] answer() {
    import std.array;
    import std.algorithm : canFind;

    string given = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.";

    auto idxs = [1, 5, 6, 7, 8, 9, 15, 16, 19];
    string[size_t] assoc;
    foreach (i, v; given.split(" ")) {
        if (idxs.canFind(i+1)) {
            assoc[i] = v[0..1];
        }
        else {
            assoc[i] = v[0..2];
        }
    }
    return assoc;
}

string[size_t]size_t をキー、 string を値としてもつ連想配列の型です。今回のこれはかなりそのままな実装ですね。

05. n-gram

import std.traits;
import std.range;

auto n_gram(Range)(Range r, size_t n)
if (isInputRange!(Unqual!Range))  // unqaulはqualifierを外すのでconst(int) -> int ということ
in
{
    assert(n >= 1);
}
do
{
    // とりあえずInputRangeということで
    // front, popFront, empty を持ってる
    static struct Result {
 private:
        alias R = Unqual!Range;
        alias T = ElementType!R;
        size_t n;
        T[] buf;
 public:
        R source;

        this(R source, size_t n) {
            this.source = source;
            this.n = n;
            this.buf = [];
            foreach (i; 0..n) {
                this.buf ~= this.source.front();
                this.source.popFront();
            }
        }

        @property {
            bool empty() {
                return this.buf.length < this.n;
            }
            auto ref front() {
                return this.buf;
            }
        }
        void popFront() {
            this.buf.popFront();
            if (!this.source.empty) {
                this.buf ~= this.source.front();
                this.source.popFront();
            }
        }
    }

    return Result(r, n);
}

auto answer1() {
    import std.array;
    string str = "I am an NLPer";
    return n_gram(str.split(" "), 2);
}
auto answer2() {
    import std.array;
    string str = "I am an NLPer";
    return n_gram(str.split(""), 2);
}

うえぇぇ。読みたくない。D言語のRange、実装すると結構読みづらい感じになりますよね。インターフェースは良いんだけど。 関数のボディの前にある if は引数の型に制約を与えるものです。 in は関数呼び出し時の事前条件で、 do のあとのブロックが関数本体です。 body じゃなくなったんですかね。

n_gram 関数でやってるのは新しい構造体を作って返すだけ。です。Rangeを返す関数の実装はだいたいこんな感じになってて、これのせいで返り型が auto になってる関数が標準ライブラリに散見されます。

@property bool empty();@property T front();void popFront() を持つ構造体は InputRange インターフェース(インターフェースというと嘘)を満たしています(この辺Interfaceじゃなくてコンセプト(?)で型を縛ってるのなんでですか)。なんとなくRangeを作りたくなったのでつくった。

06. 集合

import std.stdio;
import ngram;

void main()
{
    import std.array;
    import std.algorithm;

    auto X = "paraparaparadise".n_gram(2).array().sort!"a < b"().uniq().array();
    auto Y = "paragraph".n_gram(2).array().sort!"a < b"().uniq().array();

    writeln("X: ", X);
    writeln("Y: ", Y);
    
    writeln("Union: ", multiwayUnion([X, Y]));
    writeln("Intersection: ", setIntersection(X, Y));
    writeln("Difference: ", setDifference(X, Y));


}

D言語にはsetがない のですが、配列を set のように扱う関数がいくつか用意されています。 sortのテンプレート引数がキモいですが、 sort!((a,b) => a < b) と同じです

07. テンプレートによる文生成

char[] func(X, Y, Z)(X x, Y y, Z z) {
    import std.format;
    return "%s時の%sは%s".format(x, y, z).dup;
}

char[] answer() {
    return func(12, "気温", 22.4);
}

やるだけみたいなところがある。 X, Y, Z はテンプレート引数です。

08. 暗号文

string cipher(string src)
{
    import std.algorithm;
    import std.array;
    import std.uni;

    return src.map!((dchar c) => (c.isAlpha && c.isLower) ? cast(char)(219 - c) : cast(char)c).array;
}

この頃になると入力と出力を string にしてますね。これ、「c」みたいな文字にも反応しそうで怖いなぁ。そのまえに char に入らなくて死ぬか。

09. Typoglycemia

string typoglycemia(string str)
{
    import std.algorithm;
    import std.array;
    import std.random;
    import std.string;

    auto rnd = rndGen();

    return str.split(" ").map!((string word) {
        if (word.length <= 4) { return word; }
        ubyte[] sore = word[1..$-1].representation.dup;
        randomShuffle(sore, rnd);
        return cast(string)(word[0] ~ sore.assumeUTF ~ word[$-1]);
    }).array.join(" ");
}

気合を入れると読めます。そんなに難しくはない……はず。


D言語で文字列を扱う関数のインターフェースどうしたらいいのかわからない……。そこかしこで array 使ってるのめっちゃ罪悪感ある……

懺悔

なんだか急に、このことを書かなければならないと思ったので記しておく。僕は今までこのことを語ってこなかったし、なかったことにしていた。だけどそれではいけないような気がして、せめて衆目にさらすことにした。彼女には申し訳ないと思う。


僕は小学五年生だった。小学五年生の僕は「頭はいいけれど、変なやつ」で通っていたと思う。少なくとも僕はそう自分のことを見ていた。

ある時期、一緒の班になった女の子とよく喋るようになった。彼女は少し背が低かった。目がくりくりと大きくて少しあひる口だった。ドナルドダックが好きで、筆箱のストラップはドナルドダックとデイジーダックだった。髪の毛がまっくろくて、ふわりとカーブを描いて卵のような輪郭を作っていたと思う。ギリシャの地名と同じ三文字の名前で、漢字がきれいに当たっていてその響きは好きだった。明るくて、色んな人と喋る子だった。勉強はあまりできなかった気がする。

三学期の中頃だったんだと思う。ある日の放課後の帰り際、その時隣の席だった女の子に手紙を渡された。脇道にそれるが、その女の子は「ほえ〜」とかそういうことを真顔で言ってのける「不思議ちゃん」だった。渡された手紙は彼女からだった。その場で読んで、もう一度目は、音読した。なんと書いてあったか、具体的なことは忘れてしまったが、「好きです」か「付き合ってください」「返事は三学期が終わるまでにお願いします」に相当する内容が、彼女らしい文字、文章で書かれていた。これも確かな文字列は思い出せないが「ドナルド好きより」で結んであった気がする。

音読なんてしてしまったものだから、その場にいた人はそのことを知った。何人いたか全然憶えてないが、女の子が二人いて「嬉しいやろ」と言われたのは憶えている。担任もいて、口に人差し指をあてて「そういうのを人前で言わない」と諭してきたのを憶えている。それは無視したが。

帰り道が同じのクラスメイトに、手紙を見せてくれと言われて見せた。そのあと、返してもらうのを忘れたのだったか、それとも「あげる」と言ったのだったか、その手紙はそれ以降見かけていない。

学年末の大掃除の日、椅子を運んでいるときに、彼女に叩かれ「返事、よろしくね」と言われた。僕は曖昧に頷いた。

僕はその日、そのまま帰った。彼女に好意を告げられたことは嬉しかったし、彼女のことは可愛いと認識していたけど、彼女が直接告げてこなかったこと、手紙を友達経由で渡してきたことを言い訳に、僕は彼女に「YES」も「NO」も言わなかった。なかったことにしたのだ。彼女と付き合って、他の可愛い子、気になる子と付き合うチャンスがなくなってしまうのを惜しいと感じていたような気もするし、付き合うってなにかわかってなかった、怖かったという思いがあった気もする。

中学校に上がって、彼女と同じクラスになったけど、彼女はこちらに話しかけてくることはなかったし、何にもないような顔をしていた。僕は最初に顔を合わせたときには何を言われるものかと心配していたが、その後はそのことは忘れてしまった。

僕は生徒会に入っていて、その時はたまたま、僕と、同じクラスの女の子と、女子の先輩が二人で作業をしていた。先輩のうち一人は生徒会長だったが、その人が僕達に恋話はないのかと話をふってきた。同じクラスの子はもちろん僕が何をしたか知っていて、そのことを先輩二人に話した。そのとき僕がどんな気持ちだったのか全然憶えていない。先輩らは「それはひどい」と評した。「何も言わずになかったことにしようだなんて」。全くそのとおりだと思った。僕は何か反論した気がする。あるいは「反省してますよ」と言ったのだったか。クラスメイトのその子も僕になにか鋭い言葉を言って、僕はしんどかったはずだった。その頃僕はその子のことを可愛いなと思っていて、その子が生徒会役員に立候補したから僕も立候補した、みたいなエピソードも持っていた。

これでこの話は終わりだ。その子がいまどこで何をしてるかなんて全然知らない。なんならこの話の中に出てきた人の誰一人の行方も知らない。僕は彼女に酷いことをしてしまったと思っているし、あのときに戻ったら必ず返事をしたいと思っている。もしあのとき「YES」なり「NO」なりを告げていれば、今とは何かが違ったかもしれないとも。

幸運だったのは、僕が高専に進学して、この話を知る殆どの人と縁が切れたこと。この話を知る人の誰も、このブログを見に来るような人はいないということだ。

シェル芸でうんこつるんする

いわゆるとこのズンドコきよしです。

シェル芸botに食わせたんだけどうまく行かなくて悔しかったのでこっちに書いて溜飲を下げることにします。

やること

「う」「ん」「こ」の三文字のうちどれか一文字をランダムに流し続け*1、たまたま「うんこ」の順番で流れてきたときに、「つるん」します。

どのようにそのシェル芸を組み立てるかという話をね、していきます。

「う」「ん」「こ」を流す。

まずは無限ストリームを作りたいですね。こういうときは yes です。一文字ずつ区切って使うことを考えると yes う ん こ と空白区切りで出力してやるのが無難です。

ここまでのスクリプト

> yes う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
...

一文字だけランダムに選ぶ

awk でやるのが良さそうだったんですが、 randの使いやすさから perl で行きます。

-nle オプションはシェル芸人御用達で、 -n 一行ずつ処理 。 -l 改行する。 -e コマンドラインスクリプトを指定。です

-a がつくと awk モードになって $F が使えます。 $F は入力をsplitしたやつで、標準入力が 「う ん こ」だったら ('う', 'ん', 'こ') になっていることが期待できます。 というわけで配列からランダムに一つ引っ張ってきますので perl -anle 'print $F[int(rand(3))]' になりそうです。ちなみにオプション引数の順番は気をつけたほうがいいらしい。

ここまで

> yes う ん こ | perl -anle 'print $F[int(rand(3))]'
ん
う
う
う
ん
う
こ
う
こ
ん
ん
こ
ん
...

うんこ判定

perlでいきます。 -n オプションがあると行毎に処理することになりますが、 BEGIN{} END{} があるとコンテキストを持てます。 というわけで起動時に配列を作っておき一文字ずつ先頭に追加。splice で消し飛ばしつつ直近3文字を保持します。

そして、 join で結合したものが こんう と一致したら直近3つが「うんこ」になっているので、 exit してperlを終了します。いい感じだ。もちろん入力は吐き直さないとだめなので print $_ はします。

ここまで

> yes う ん こ | perl -anle 'print $F[int(rand(3))]' | perl -nle 'BEGIN{@a=()} unshift(@a,$_); splice(@a,3); print $_; if (join("", @a) eq "こんう"){ exit();} ' 
ん
う
う
こ
う
ん
こ

「つるん」する

echo つるん

> yes う ん こ | perl -anle 'print $F[int(rand(3))]' | perl -nle 'BEGIN{@a=()} unshift(@a,$_); splice(@a,3); print $_; if (join("", @a) eq "こんう"){ exit();} ' ; echo つるん
う
う
ん
こ
つるん

見栄えがわるい

つるんだけ横になっていて気持ち悪いので、うんこまでの道のりもちゃんとヨコ読みにしてあげます tr -d '\n' で改行文字を消し飛ばせるのは常識ですね。

> yes う ん こ | perl -anle 'print $F[int(rand(3))]' | perl -nle 'BEGIN{@a=()} unshift(@a,$_); splice(@a,3); print $_; if (join("", @a) eq "こんう"){ exit();} ' | tr -d '\n'; echo 'つるん'
ここうこんこううこうここうんんうんうこうんうこうここううんこつるん

あー、スッキリ……!

perl に触るのなんて初めてなので苦労しました。

追記

うんこシェル芸で右に出るものはいないと噂の @grethlen さんがすごいきれいに出してた。シェル芸って感じですねすごい

*1:流すとかトイレですね。だれうま

シェル芸botの宣伝をさせてください

 この記事はShell Script Advent Calendar の19日目に向けて書かれた記事です。参加する気はなかったんですが、ブログでシェル芸botについて言及したことがなかったのと、ふとカレンダーを見たら空いてたのとで入れてもらうことにしました。よろしくお願いします。


シェル芸botについて

 シェル芸bot ( @minyoruminyon )をご存知でしょうか。シェル芸botは、「TLに現れた #シェル芸 または #危険シェル芸 とタグのついたツイートについて、そのツイートをシェルスクリプトとしての実行を試み、正常終了すれば結果を引用でツイートする」というbotになります。「シェル芸」とはなんぞやという方は こちら の定義や #シェル芸 - Twitter Search の例などをご参照下さい。

 シェル芸botは現在 386 フォロワーを獲得しており、生まれてから 2633 のシェル芸を実行してきました。このシェル芸botを作ったのが私ふるつきで、せっかくなのでセールストークをさせていただきます。

 シェル芸botが生まれたのは 2017年の 6月であると、 シェル芸史 にあります。以前から、 base64 encoded なTweetをする界隈に身をおいていたこともあり、ある程度の「シェル芸」というワードに親しんでいた私はしかし、TLに流れてくる不可思議な記号列=シェル芸をターミナルに貼り付けて実行するのが面倒&怖かった憶えがあります。そこで、ある種のサンドボックスとして、シェル芸botを作成することにしました。もちろん、シェル芸界隈のノリの良さのようなものを目にしており、これはイケるだろうと思っていなかったわけがありませんが。

 斯くしてシェル芸botはTL上に生を受け、瞬く間にシェル芸人たちの間に広がった、はずです。はじめは不慮の停止が数度あった気がしますが、現在は安定して稼働を続けています。シェル芸botはユーザフレンドリーなbotを目指しているので、シェル芸人の皆様の活動に合わせて日々利用できるコマンドが追加されています。

@paiza_run との関係

 TL上のプログラムを実行するとくれば、まず思い浮かぶのが paiza_run です。シェル芸botと動きは丸かぶりですが、シェル芸botはどちらかといえば、「ツイートしたシェル芸が思いがけず実行され、結果が見られる」という public な向きのbot です。

 paiza_run は凍結されてしまいましたが、以前にはコラボレーションしたこともありました。

togetter.com

シェル芸bot に触れてみよう

 大したハードルもないですが、シェル芸bot に初めて触る方向けのチュートリアルです。

  1. シェル芸botをフォローしましょう
  2. シェル芸botのフォローバックを待ちましょう(手動です)
  3. echo-sd シェル芸楽しい!! #シェル芸 とツイートします

 これであなたもシェル芸人です。巷では様々なシェル芸が飛び交っていますので眺めるも解読するも創り出すも良しです。お楽しみ下さい。  

シェル芸bot のこれから

 ある程度安定してきたシェル芸botですが、これからも「あんなコマンドがほしい」「こんなコマンドを作った」が尽きることはないと思うので、ガンガンアップデートをかましていきたいです(何かのタイミングで私の目に止まれば可及的に速やかに更新致します)。そしてできれば私もシェル芸人の仲間入りを果たしたい……!

 ところでシェル芸bot さんの顔グラがないのが味気ないので募集しています。よろしくお願いします。