🔖

Brainfuckの文章出力を短く書こう

に公開

皆様ご存知、+-<>[],.の8文字だけでプログラムを記述するBrainfuckという言語があります。Brainfuck及び基本的な構文については以下の記事が大変わかりやすいので、ぜひご参考にしてください。
https://zenn.dev/kitsunecat/articles/9b99b68efaf9d0

さて、この世にはBrainfuckで「Atcoderの問題を解く勢力」と、「文章出力をいかに短く書くかを競う勢力」の2大勢力が存在しており、今回は後者の 「文章入力をいかに短く書くかを競う勢力」に参入する皆様のためのチュートリアル的な記事 となります。

なお、私はこちらのインタープリタを使用しています。今回のプログラムには複雑なものもありますが、このインタープリタの「ステップ実行」をすることで理解につながるかと。また、今回はこちらのASCIIコード変換機がかなり重宝します。

今回の動機(余談)

今回の記事を書こうと考えた理由は2025年のお正月に際して「Happy new year!」をいかに短く書くかを競い合ったためです。詳しくは、こちらのXの返信ツリーへ。なお、記事執筆時点での私の記録は、以下の103文字でした。より短くかけた方は是非教えてください。

Happy new year!(103文字)
++++++++
[>
    >--
    >+
    >+++
    >++++
    >-------
    [+++++++++++<]
<-]

>>.
>+.
>..
>+.
>.
<<--.
<++++.
>>--.
>.
<++.
<<.
----.>
++++.
>>+.

初期化部分と調整・出力部分

Brainfuckで文章を出力するプログラムの多くは、メモリの値をおおよそASCIIの文字部分にする「初期化部分」とそのメモリの値を出力したい文字に調整して出力する「調整・出力部分」に分かれています。
なお、これらの用語は今回便宜上使用する造語です。

例えば、私の名前である「ralutet」を出力するプログラムを以下のように分解できます。

ralutet(62文字)
# 初期化部分
# 0 0 114 96 108となる
++++++
[>
    >++
    >-
    >+
    [+++++++++++++++++<]
<-]

#調整・出力部分
>>.     # r 114
>+.     # a 97
>.      # l 108
<<+++.  # u 117
-.      # t 116
>++++.  # e 101
<.      # t 116

初期化部分

前述の通り、まずはメモリの値をだいたいASCIIの文字部分にします。ASCIIでは、32が空白、65~90がA~Z、97~112がa~zに割り当てられているため、ここではループ等を駆使して、そのあたりの値にします。

初期値とその配置を決める

後述するループ等を使用して初期化する前に、まずはどの値に初期化するかを考える必要があります。主に以下の点を考慮して決定していますが、使うループによって考えることは変わってきます。

  1. 概ね出力する文字のASCIIコードとなっていること
    • 特に、一番最初に出力する文字となっているとよい
  2. 全てある値の倍数となっていること
    • 理論上はASCIIコードの平方根となる7~12が望ましい...
  3. グループ間の移動距離が少ないようにする

例えば、「ralutet」のASCIIコードを縦軸にプロットすると以下のようになり、概ね97~101(a~e)と108(l)と114~117(r~u)にグループ分けできます。そして、グループ間内の一番最初に出力する文字のASCIIコードの差は6と11で、さらに96,108,114が全て6の倍数なので、6の倍数で初期化しましょう。また、グループ間の移動回数を考えると、97~101(a~e)のグループと114~117(r~u)のグループが隣接して配置していると都合がよいです。
ということで、114 96 108とメモリの値を初期化することに決定します。

ループで初期化する

次にループを使ってメモリの値を初期化するプログラムを書きます。その時に使える技が何個かありますので、ひとつづつご紹介いたします。

基本のループ

brainfuckで一番基本となるループです。しかし、+や<が何度も表記されている感じ、さらに工夫の余地がありそうです。

基本のループ(55文字)
# 0 0 81 90 81 63
+++++++++
[
    >+++++++++
    >++++++++++
    >+++++++++
    >+++++++
<<<<-]

帰るときに全体に足すループ

先ほどの同じ文字が何度も表記される問題を解決したループです。右に行くときに細かい値を足し引きして、左に帰るときにまとめて大きな値を足していきます。汎用性が高いのでおすすめです。先日あじさんのツールで知りました。

汎用で便利なループ(34文字)
# 0 0 81 90 81 63
+++++++++
[>
    >+
    >++
    >+
    >-
    [++++++++<]
<-]

アンダーフローでのループ

brainfuckの各メモリは0~255で、基本的に0の状態から1を引くと255になります。64付近などの特定の場合に限りますが、これを利用することでかなり短く書くことができます。また、オーバーフローとアンダーフローは意外と様々なところで登場するので、頭に入れておくといいです。

アンダーフローでのループ(13文字)
# 0 63
----
[
>+
<----]

少しずつ増やすループ

二重のループを使って、そこそこ短いプログラムで少しずつ違う値を作成できます。ただし、メモリの値の順番は固定であり、大文字/小文字が混在している場合はあまり使えません。空白(32)くらいは後の方法で対応することが可能です。これはredditで知りました。

少しずつ増やすループ(36文字)
# 0 0 120 116 112 108 以下省略
++++++
[
    >+++++
<-]
↑ここまでで 0 30 を作る
>
[>
    [++++>]
    ++++
    [<]
>-]

出力に空白(32)や大文字(65~90)を含めたい場合、メインのループの横でその30を作ることができます。

少しずつ増やすループ改(45文字)
# 0 0 30 76 120 116 112 108 以下省略
++++++
[
    >+++++
<-]
↑ここまでで 0 30 を作る
>
[>
    +>       #ここで30を作る
    ------>  #ここで76を作る
    [++++>]
    ++++
    [<]
>-]

最初の30を作る部分をオーバーフロー/アンダーフローを使ってどうにかかけないかと思いましたが、難しそうですね。

調整・出力部分

先ほど大体作成したメモリの値を微調整して、目的となる文字を出力します。これは特に工夫しなくても短く書けますが、ループを使って短くかける場合もあります。書くことがあまりないですね。

変則技

基本的には初期化部分と調整・出力部分に分かれると言いましたが、それ以外に分類されるものも一応存在します。

頭の大文字を崩す

まず大きなループで大文字を作成してから、その値を使用して、その後使う小文字の初期化を行う方法です。

頭の大文字を崩す(50文字)
----
[
>+
<----]
# ↑ここまでで63を作る
>++.       # A  65
# ↑ここまででA(65)を出力する
[>
    [++++++>]
    -
    [<]
>-]
# ↑少しずつ増やすループで 0 0 127 121 115 以下省略 を作る
>>>---..   # pp 112
>-.        # l  108
>--.       # e  101

さいごに

もし面白い短い記法があれば、教えていただけると幸いです。
皆様もよきbrainfuckでの「文章入力をいかに短く書くかを競う勢力」としての生活をお楽しみください。ここまでお読みいただきありがとうございました。

Discussion