octahedron

LemとSKKとCommon Lispでたたかうプログラマのブログ

tanasinnをぼくの手の上に

tanasinnをぼくの手の上に

 この記事ではGoを使ってコマンドラインプログラムを書いたよーという話をします。

tanasinnコマンドがほしい

 ところでみなさん、tanasinnを知っていますか? tanasinnというのはインターネットミームのひとつです。2ちゃんねる掲示板に投稿されたのが初めてで、どこか見ていると不安になってくるようなシュールなアスキーアート群についた名称です。ニコニコ大百科の当該記事によれば、以下のようなものです。

  • 退廃的で人にどこ∵∴∵な(○)不安を感じさせる雰囲気。
  • 侵蝕に対する人類意識の根源的な恐怖感・畏∵(●)∴∵徴。
  • インターネット内に存在する別世界・別精神で∵り、全世界の根幹を成し操作している。我々はtanasinnに常に見られている。tanasinn(●)決して怖くな∵∴         (●)
  • Don't think. Feel and you'll be tanasinn.

 この不安になる感じがとてもよいですね。

 ぼくもtanasinnになりたい!!!!!

 そこで手軽にtanasinnになれるコマンドをGoの勉強がてら作成してみました。

tanasinnコマンド

 まずはtanasinnコマンドをどのように作るか考えてみます。
 利用ケースとしてはSlackなどで発言するときなどが考えられますが、後々なんにでも使えるようなデザインにしておきたいです。そこでtanasinnコマンドはシェルコマンドのフィルタプログラムのように実装することにしました。引数でファイルを受け取るかあるいはパイプされた文字列の文字をランダムにtanasinn化して吐き出すプログラムとして実装します。tanasinn化度合いを引数で指定できるといいかもしれません。

いかにtanasinnするか

 さて、文字列をtanasinn化する方法を考えるのですが、文章におけるtanasinnっぽさを突き詰めると隠れマルコフモデルだとかなんだとか、ちょっとよく知らない概念あるいは宇宙の真理に手を出さなければならなくなりそうです。ここでは簡単に、入力文字列中の文字がランダムに所与のtanasinn文字列に置き換えることでtanasinn化します。つまりPythonっぽい疑似コードで書けばこうです:

for 文字 in 入力文字列:
    n = 0.0から1.0乱数を生成

    if nが閾値を超えていたら:
        tanasinn文字列をランダムにピックアップして出力
        
    else:
        文字を出力

 ここでまず上記コードのfor文の中身を行う補助関数を用意します。

func tanasinnize(th float64, r rune) string {
    s := []string{
        "∴",
        "∵",
        "∴∵",
        ")",
        "(・)",
        "(・",
        "∴・",
        "∴",
        "・・",
        "・:",
        "..",
    }
    if th < rand.Float64() {
        return fmt.Sprintf("%s", s[int(rand.Uint32()) % len(s)])
    } else {
        return fmt.Sprintf("%c", r)
    }
}

 tanasinn化文字列は関数内の定数として保持しています。生成した乱数がもし引数で与えられた閾値を超えていたらtanasinn化文字列を出力し、そうでなければ入力された文字を返すといういたってシンプルなものです。

 あとはこれを文字列内の文字についてのループで呼び出してやり、標準入力まわりを整えてやれば完成です。

 できたものがこちらになります。

github.com

 動かすとこのようになります。

$ echo "Don't think. Feel and you'll be tanasinn." | ./tanasinn
(・on't th..nk. ・:eel ∵nd y∴u'l∴ be ta∴∴sinn.

 これで気軽にtanasinn化できるようになりました。

だんだんtan(・sinn∴なっ・・い∴∵..・:

 しかしあまりにシンプルすぎてちょっと面白くないです。具体的には「あぁ……消えて…い…k……」みたいなことがしたいです。なのでtanasinn閾値を引数で弄れるようにし、さらに閾値区間指定できるようにします。

 コマンドライン引数-tに数値(float64)を指定するとそれが閾値として採用されます。また、閾値1.0,0.6,0.3,0.1のようにカンマ区切りで渡すと、入力文字列を区間数で等分した領域の閾値を前から指定できます。たとえば閾値1,0のとき、区間数は2なので前50%が閾値1、後ろ50%が閾値0としてtanasinn化されます。1.0,0.6,0.3,0.1のときは最初の25%が1.0、次の25%が0.6、……という感じです。

 この文字列をパースして閾値のスライスを取り出す処理をparseThreasholdsという補助関数にしました。ループや分岐、スライスの扱い、エラーハンドリング等の基本的なGoの要素が登場し、入門に最適な感じです。

func parseThreasholds(th string) ([]float64, error) {
    var ths []float64

    if strings.Contains(th, ",") {
        list := strings.Split(th, ",")
        for i := 0; i < len(list); i++ {

            f, err := strconv.ParseFloat(list[i], 32)
            if err != nil {
                log.Fatal(err)
                return ths, &parseError{s: th, reason: "not float in the list"}
            }

            ths = append(ths, f)
        }

    } else {
        f, err := strconv.ParseFloat(th, 32)
        if err != nil {
            log.Fatal(err)
            return []float64{}, &parseError{s: th, reason: "not float"}
        }
        ths = []float64{f}
    }

    return ths, nil
}

 こいつを引数のパース処理に組込み、標準入力の内容をいったんメモリに全て読み込むように変更し(区間指定するには全体の流さが必要だからです)、あとは文字についてのループを以下のように変更します。

 for n := 0; n < len(text); n++ {
        p := float64(n) / float64(len(text))
        i := int(p * float64(len(option.th)))
        r := text[n]
        if r == '\n' {
            fmt.Printf("\n")
        } else {
            fmt.Printf("%s", tanasinnize(option.th[i], r))
        }
    }
}

 pは入力文字列中の今の位置の割合です。それをoption.thという閾値スライスの長さを掛けて小数点以下を切り捨てることで、閾値スライスの中での位置を計算しています。

 ということで実装したものがこちらのリポジトリです。

github.com

使ってみるとこのような感じです。

# 閾値0.2
$ echo "だんだんtanasinnになっていく……" | ./tanasinn -t .2
)∴∵・・..・:an∴・・:・:∴・∴・:∴・()∴・・・・・…∴・

# 閾値0.7
$ echo "だんだんtanasinnになっていく……" | ./tanasinn -t .7
∴・∴だんtanas..・・nに)()ていく……

# だんだんtanasinn化する例
$ echo "だんだんtanasinnになっていく……" | ./tanasinn -t 1,0.6,.4,.2
だんだんta∴(()()nnにな・:)..く..…

おわりに

 この記事ではさくっと作ったtanasinn化コマンドの紹介と中身の解説をしました。やってることは単純ですがこれで気軽にtanasinnごっこができるようになりました。