LoginSignup
2
0

More than 1 year has passed since last update.

Brainf*ck でマイナンバーのチェックデジットを計算する

Last updated at Posted at 2022-12-20

はじめに

  • Brainf*ck には、コードゴルフとは別の楽しみ方もあるよ
  • Brainf*ck でマイナンバーのチェックデジットを計算する
  • Brainf*ck で実用的(?)なコマンドを作ろう

想定読者は、Brainf*ck を軽く知っていて、制御構造(ループや分岐)の書き方がわかっているレベルの人です。

Brainf*ck には、コードゴルフとは別の楽しみ方もあるよ

「釈迦に説法」かもしれませんが、Brainf*ck という難解プログラミング言語(esolang)があります。

8文字だけの記号でプログラミングができる言語で、きっと誰しも一度は Brainf*ck インタプリタ等を作ったことがあるかと思います。

今回は、Brainf*ck をもっとカジュアルに使えるようにする方法論(?)の紹介と、試しに実用的(?)なコマンドの一例として、マイナンバーのチェックデジットを検証するコマンドを作成してみます。
マイナンバーのチェックデジット検証は、7年ぐらい前に流行った物ですね。

あまり、コードゴルフ的な(コードをできる限り短くする)努力はしないですが、自分で部品を作って、それを使ってプログラムを組み立てるような、DIY 的な楽しみが伝わればよいなと思っています。

また、英語版の wikipedia には記載があるのですが、Brainf*ck の実装にはいろいろなバリエーションが存在して、移植性の問題があります。

今回は、おそらく一番多い実装であるだろう、以下を想定しています。

  • 1セルのサイズを 1byte (8bit) とする
  • アレイサイズ(セル配列の最大/最小数)は、古典的なサイズを踏襲(最大サイズは少なくとも 30000 個はある。負の値は存在しない)
  • 改行コードは \n (LF) 単独
  • 文字読み込み . の EOF は -1 (255) が読み込まれる(C 言語の EOF 相当)

Brainf*ck をもっとカジュアルに使う

正直、Brainf*ck のソースコードを直接人間が読み書きするのは、非常に大変だと思います。

いっそのこと、Brainf*ck のソースコードは、アセンブラ言語レベルだと割り切って、その上に高級言語とまでは行かないですが、簡易言語ぐらいの仕掛けを用意して、それを使ってプログラミングをしたほうが楽ができるのでは無いかと思います。

生粋の Brainf*cker に言わせれば邪道かもしれませんが。

ちゃんとした言語仕様を考えるのは難易度が高いので、ここでは簡単なマクロレベルの仕掛けを python で実装してきたいと思います。

もし、「そんな簡単なものじゃ嫌だ、もっと使いやすいものが欲しい」という人や、「俺の作った高レベルなものをみんなに使ってほしい」という人がいる場合には、esolang 版の LLVM ともいうべき、ELVM を調べると幸せになれるかもしれません。

基本的な部品

では、自力で細かい部品を作っていきます。

まず、指定の位置(アドレス)で、指定の処理を実行するマクロを作ります。
前提として、今アドレス 0 の位置にポインタがある前提とします。
また、マクロは文字列として Brainf*ck のソースコードを返すことにします。

def target_op(target: int, statement: str) -> str:
    return ">" * target + statement + "<" * target

これを使えば、好きな位置(アドレス)を加算することも、減算することもできます。
しかし、アドレス 1 で加算して、アドレス 2 を減算して、などとすると、無駄な移動がたくさん発生してしまいます。

そこで、無駄な移動や、無駄な計算を相殺する、あるいは不要な操作を削除するメソッドを用意します。

例えば、ポインタの右移動(>)と左移動(<)が隣り合っていれば、それは何もしていないことと同じことだと判断します。
同様に、加算(+)と減算(-)が隣り合っていても同じことです。
また、ゼロクリア処理([-])の直前の加減算も、無駄な処理になります。

他にも削除できる無駄な処理があるかもしれませんが、いったんここまでにします。

また、プログラムの末尾に移動や加減算があっても結果には影響を与えないため、これも削除できるようにします。

import re

# プログラム途中の無駄な移動/計算を削除
def remove_waste(statement: str) -> str:
    while True:
        if "<>" in statement:
            # 無駄な移動の相殺・その1
            statement = statement.replace("<>", "")
            continue
        if "><" in statement:
            # 無駄な移動の相殺・その2
            statement = statement.replace("><", "")
            continue
        if "+-" in statement:
            # 無駄な加減算の相殺・その1
            statement = statement.replace("+-", "")
            continue
        if "-+" in statement:
            # 無駄な加減算の相殺・その2
            statement = statement.replace("-+", "")
            continue
        if "+[-]" in statement or "-[-]" in statement:
            # ゼロクリアの前の加減算の削除
            statement = re.sub(r'[-+]+\[-\]', "[-]", statement)
            continue
        if "[-][-]" in statement:
            # 複数回のゼロクリアを1回に
            statement = statement.replace("[-][-]", "[-]")
            continue
        break
    return statement

# プログラム全体の無駄な移動/計算を削除.
def remove_waste_all(statement: str) -> str:
    statement = remove_waste(statement)
    while statement:
        if statement[-1] in "-+><":
            # 末尾の "+" "-" ">" "<" は削除
            statement = re.sub(r'[-+><]+$', "", statement)
            continue
        if statement.endswith("[-]"):
            # 末尾の "[-]" は削除
            statement = re.sub(r'\[-\]$', "", statement)
            continue
        break
    return statement

ソースコードの一部の無駄を削除するなら remove_waste() を呼び出して、ソースコードの全体の無駄を削除するなら remove_waste_all() を呼び出せばよいですね。

では、target_op() を使って、単純な部品を用意していきます。

まずは指定位置(target)をインクリメントするマクロ。

def inc_data(target: int) -> str:
    return target_op(target, "+")

指定位置をデクリメントするマクロ。

def dec_data(target: int) -> str:
    return target_op(target, "-")

指定位置をゼロクリアするマクロ。

def clear_data(target: int) -> str:
    return target_op(target, "[-]")

指定位置を指定の値に設定するマクロ。
指定の値は 0 以上の値であることを前提としています。

def set_value(target: int, value: int) -> str:
    return remove_waste(
        clear_data(target)
        + target_op(target, "+" * value)
    )

Brainf*ck を詳しい人が上のコードを見ると、無駄じゃないか、ループを使ってコードを短くできるだろう、と思うはずです。
しかし、ここではあまりソースコードの長さを短くすることは考えないことにします。
どうしても気になる人は短いソースを出力できるように改良してみてください。

また後で述べますが、ループで短く書くよりも、素直に + を繰り返した方がコンパイル時の最適化が効きやすいと思われます。

次に制御構造です。

単純な while ループです。指定位置が 0 になっていたらループを終了します。

def while_loop(target: int, statement: str) -> str:
    return remove_waste(
        target_op(target, "[")
        + statement
        + target_op(target, "]")
    )

単純な for ループです。指定位置をデクリメントしながらループして、指定位置が 0 になったらループを抜けます。
そのため、終了後には指定位置は 0 になっています。

def for_loop(target: int, statement: str) -> str:
    return while_loop(target,
        dec_data(target)
        + statement
    )

ループがあるので、ループの途中終了用のマクロです。
while ループあるいは for ループの制御用の位置を強制的にゼロクリアすることによって、ループを終了します。
いわゆる break 相当ではなく、次のループの終了 ] まで処理は続行するところに注意してください。

内容は clear_data() を呼び出しているだけなので、わざわざ別のマクロを用意する必要がないと思うかもしれませんが、後日ソースコードを読む際にプログラミングの意図が明確になるように別のマクロにしています。

def loop_last(target: int) -> str:
    return clear_data(target)

ループが使えるようになると、移動・コピー系のマクロも作れるようになります。

まずは、データを移動。

def move_data(source: int, destination: int) -> str:
    return for_loop(source, inc_data(destination))

次は、データをコピー。

データのコピーをするには、ワークエリアが必要なため、引数の work でワークエリア用のアドレスを渡してもらいます。
作業用のエリアは、ゼロクリアされた状態で渡されることが前提です。
また次にそのワークエリアを使う時のことを考えて、マクロの終了時点でもゼロクリアされている状態にしておくことにします。

# データをコピー
def copy_data(source: int, destination: int, work: int) -> str:
    return remove_waste(
        clear_data(destination)
        + for_safe(source, work, inc_data(destination))
    )

前述の for ループは指定位置を破壊してしまうので、破壊しない版の for ループ(for_safe())も用意します。
見てわかる通り、ループ終了時点では元の値に復元されますが、ループ中は違う値になっているため、ループ中に指定位置を参照・更新すると問題が起きると思います。
また、loop_last() も使用できないので注意が必要です。

def for_safe(target: int, work: int, statement: str) -> str:
    return remove_waste(
        for_loop(target, inc_data(work) + statement)
        + move_data(work, target)
    )

単純な if 文です。指定位置が 0 以外なら then 節を処理します。else 節は存在しません。
判定後に、指定位置はゼロクリアされます。

def if_nz_then(target: int, then_statement: str) -> str:
    return while_loop(target,
        clear_data(target)
        + then_statement
    )

次の if_one_then() は、上記 if_nz_then() の変形です。
複雑な処理を実装しようとすると、フラグを使用することが多くなります。
そのような、フラグの値が 0 か 1 のどちらかであることが明確な場合のために if_nz_then() の余計なゼロクリア処理を、単なるデクリメントに置き換えたものです。
コンパイルした場合にはさほど違いが出るわけではないので、これ(if_one_thne())を使わずに if_nz_then() だけを使っても良いかもしれません。

判定後に、指定位置はゼロクリアされます。

def if_one_then(target: int, then_statement: str) -> str:
    return while_loop(target,
        dec_data(target)
        + then_statement
    )

上記、if_nz_then() は else 節がなかったので、else 節あり版も用意します。
判定後に、指定位置はゼロクリアされます。

def if_nz(target: int, work: int,
        then_statement: str,
        else_statement: str = "") -> str:
    if else_statement == "":
        return if_nz_then(target, then_statement)
    else:
        return remove_waste(
            inc_data(work)
            + if_nz_then(target, then_statement + dec_data(work))
            + if_one_then(work, else_statement)
        )

指定位置がゼロクリアされると困るケースもあるため、事前にデータをコピーしてから if_nz() を実行する版も用意します。

def if_nz_safe(target: int, work1: int, work2: int,
        then_statement: str,
        else_statement: str = "") -> str:
    return remove_waste(
        copy_data(target, work1, work2)
        + if_nz(work1, work2, then_statement, else_statement)
    )

「指定位置が 0 以外」ではない if 文も用意します。
これは、指定位置1の値 < 指定位置2 の場合に then 節が、そうでない場合に else 節が実行されます。
判定後に、指定位置1/2はゼロクリアされます。

def if_lt(target1: int, target2: int, work1: int, work2: int,
        then_statement: str,
        else_statement: str = "") -> str:
    return remove_waste(
        for_loop(target1,
            if_nz_safe(target2, work1, work2,
                dec_data(target2),
                loop_last(target1)))
        + if_nz(target2, work1, then_statement, else_statement)
    )

逆の条件、指定位置1の値 >= 指定位置2 は、if_lt() の then / else を入れ替えれば(あるいは指定位置1/2を入れ替えれば)実現できます。
判定後に、指定位置1/2はゼロクリアされます。

def if_ge(target1: int, target2: int, work1: int, work2: int,
        then_statement: str,
        else_statement: str = "") -> str:
    return if_lt(target1, target2, work1, work2,
        else_statement,
        then_statement)

他にもいろいろな条件の if 文マクロが作れると思いますが、今回の目的にはこれぐらいで十分かと思います。

またもう一つ、指定の文字列を改行付きで出力するマクロを用意します。

def puts(work1: int, message: str) -> str:
    message += "\n"
    result = ""
    for ch in message:
        value = ord(ch)
        result += remove_waste(
            set_value(work1, value)
            + target_op(work1, ".")
        )
    result += clear_data(work1)
    return remove_waste(result)

これがあれば、Hello World レベルのプログラムはすぐに実現できますね。

Brainf*ck でマイナンバーのチェックデジットを計算する

では、これらの部品を使ってマイナンバーのチェックデジットを検証するプログラムを作っていきます。

マイナンバーのチェックデジットはいわゆる「モジュラス11 ウェイト2~7」です。

つまり、各桁の「数値」に「ある値」(ウェイトとよばれる2~7の値)を掛けて合計を求めることや、その数値を11で割ったあまりを求める必要があります。
他にも、入力された文字が数字を表すかの判定や、数字を表す文字を数値に変換する機能、最終的にチェックデジットが正しいかの判断する部品が必要になります。

順番に作っていきます。

まずは、11で割った余りを求める部品。

割り算というと難しい気がしますが、余りを求めるだけなので 11 以上の値であれば 11 を引く、という処理を繰り返せば求められます。

def mod11(target:int, works:int) -> str:
    return remove_waste(
        # ループ用フラグを1にする
        set_value(works+0, 1)
        + while_loop(works+0,
            # データをコピー
            copy_data(target, works+1, works+2)
            # コピーした値と 11 を比較
            + set_value(works+2, 11)
            + if_ge(works+1, works+2, works+3, works+4,
                then_statement=
                    # 11 以上の場合には、指定位置から 11 を引く
                    target_op(target, "-"*11),
                else_statement=
                    # 11 より小さい場合にはループ終了
                    loop_last(works+0))
        )
    )

次に、ある値にウェイトを掛けて、合計を求める部品。
これも、単純なループで実現できます。

以下の場合、src アドレスにある値(入力されたマイナンバーの1桁の数値が入っている想定)に value の値(これは即値。ウェイトに当たる)を掛けたものを、dest に加算します。

def mul_imm(dest:int, src:int, value:int) -> str:
    return remove_waste(
        for_loop(src,
            target_op(dest, "+"*value))
    )

次に、文字が数字を表すかの判定と、文字を数値に変換する部品を作ります。
文字コードとしては ASCII を想定しているため、指定位置(target の示すアドレスの値)から 0x30 ('0' の ASCII コード)を引いて、その結果が 0 ~ 9 の間であるかを判定しています。
数値の場合には then 節を実行、そうでない場合には else 節を実行します。

def if_digit(target:int, work1:int, work2:int, work3:int, work4:int,
        then_statement:str, else_statement:str = "") -> str:
    return remove_waste(
        # '0' を引く(8x6 = 48 = 0x30).
        set_value(work1, 8)
        + for_loop(work1,
            target_op(target, "-"*6))
        # 10 よりも小さい(元の値が '0'~'9' の範囲)かどうか
        + copy_data(target, work1, work2)
        + set_value(work2, 10)
        + if_lt(work1, work2, work3, work4,
            then_statement, else_statement)
    )

ここでは、ループを使って 0x30 を引いていますが、直接 48 個の - でも良かったかもしれません。

上記部品を組み合わせて、チェックデジットの 1 桁分の計算をする部品を組み立てます。

1文字読み込み、その文字が数字ならウェイトを掛けて合計を求める場所に加算します。

ただし、この処理は繰り返し実行されるため、前段の処理でエラーが検知されている場合など、処理をスキップしたいことがあります。
そのため、現状問題がないかを示すフラグを表すアドレス(valid_flag)を受け取り、それが真(つまり 0 ではない場合)のみ処理するようにします。

def calc_one_digit(target:int, valid_flag:int, works:int, weight:int) -> str:
    return remove_waste(
        if_nz_safe(valid_flag, works+0, works+1,
            # 1文字読み込み
            target_op(works+2, ",")
            # 数字か?
            + if_digit(works+2, works+3, works+4, works+5, works+6,
                then_statement=
                    # 数字なら、重みを掛けて加算
                    mul_imm(target, works+2, weight),
                else_statement=
                    # 数字でなければ中断
                    clear_data(valid_flag)
                    + clear_data(works+2)))
    )

また、最後のチェックデジットの検証ロジックを作ります。

これは最後の1文字を読み込み、それと今まで計算してきた合計の値を11で割った余りと比較して問題ないか確認しています。

def validate_check_digit(target:int, valid_flag:int, works:int) -> str:
    return remove_waste(
        if_nz_safe(valid_flag, works+0, works+1,
            # チェックデジットを読み込む
            target_op(works+2, ",")
            + if_digit(works+2, works+3, works+4, works+5, works+6,
                then_statement="",
                else_statement=
                    # 数字でなければ、中断
                    clear_data(valid_flag)
                    + clear_data(works+2)))
        + if_nz_safe(valid_flag, works+0, works+1,
            copy_data(target, works+3, works+4)
            + set_value(works+4, 2)
            + if_lt(works+3, works+4, works+5, works+6,
            then_statement=
                # 余りが 0 または 1 の場合は、チェックデジットも0であること
                if_nz_then(works+2, clear_data(valid_flag)),
            else_statement=
                # そうでない場合には 余り + チェックデジット = 11 であること
                set_value(works+7, 11)
                + for_loop(works+2, dec_data(works+7))
                + for_loop(target, dec_data(works+7))
                + if_nz_then(works+7, clear_data(valid_flag))
                + clear_data(works+7)))
    )

おまけで、桁が余計に存在しないことのチェック部品を用意します。
正しい桁数であれば、1文字読み込めば EOF あるいは改行コードなどが読み込まれるはずなので、数字でないことを確認しています。
ここはちょっと手抜き実装で、たとえば後ろに英字などが付いていても問題ないことになります。

def check_eof(valid_flag:int, works:int) -> str:
    return remove_waste(
        if_nz_safe(valid_flag, works+0, works+1,
            # 1文字読み込む(何もなければEOFが来るはず)
            target_op(works+2, ",")
            + if_digit(works+2, works+3, works+4, works+5, works+6,
                then_statement=
                    # 数字なら中断
                    clear_data(valid_flag)))
        + clear_data(works+2)
    )

これで全ての部品ができたので、マイナンバーのチェックデジットの検証プログラムを作ってみます。

VALID_FLAG=0
RESULT=1
INPUT=2
WORKS=3

print(remove_waste_all(
    set_value(VALID_FLAG, 1)
    # 下から数えて12桁目~7桁目を計算
    + calc_one_digit(RESULT, VALID_FLAG, WORKS, 6)
    + calc_one_digit(RESULT, VALID_FLAG, WORKS, 5)
    + calc_one_digit(RESULT, VALID_FLAG, WORKS, 4)
    + calc_one_digit(RESULT, VALID_FLAG, WORKS, 3)
    + calc_one_digit(RESULT, VALID_FLAG, WORKS, 2)
    + calc_one_digit(RESULT, VALID_FLAG, WORKS, 7)
    # 桁あふれしないように mod11
    + mod11(RESULT, WORKS)
    # 下から数えて6桁目~2桁目を計算
    + calc_one_digit(RESULT, VALID_FLAG, WORKS, 6)
    + calc_one_digit(RESULT, VALID_FLAG, WORKS, 5)
    + calc_one_digit(RESULT, VALID_FLAG, WORKS, 4)
    + calc_one_digit(RESULT, VALID_FLAG, WORKS, 3)
    + calc_one_digit(RESULT, VALID_FLAG, WORKS, 2)
    # 最後にmod11を求める
    + mod11(RESULT, WORKS)
    # チェックデジットの検証
    + validate_check_digit(RESULT, VALID_FLAG, WORKS)
    # データが13桁以上ないことをチェック
    + check_eof(VALID_FLAG, WORKS)
    # 結果を出力
    + if_nz(VALID_FLAG, WORKS,
        puts(WORKS+1, "true"),
        puts(WORKS+1, "false"))
    ))

ここで一つ気を付けないといけないのは、各桁にウェイトを掛けて合計を求める際の「桁あふれ」です。
今回は Brainf*ck の1セルは 1byte を想定しているので、上の桁から処理していくと 9*6 + 9*5 + 9*4 + 9*3 + 9*2 + 9*7 = 9*(6+5+4+3+2+7) = 243 となり、上(左)から 6 桁分はあふれないで計算できますが、7 桁目の計算時にはあふれる可能性が存在します。

最終的に 11 で割ったあまりを求めるので、合計の途中でも 11 で割った余りを計算しても問題ないため、上(左)から 7 桁目(下から6桁目)を加算する前に、一度 11 で割った余りを求めるようにします。

結果としては、true あるいは false の文字列を出力するようにします。

実際に上記の python スクリプトを実行すると以下のようになります(実際には 1 行で出力されますが、見やすさのため fold コマンドで折り返しをしています)

[-]+>>>[-]<<<[->>>>+<+<<<]>>>>[-<<<<+>>>>]<[[-]>>,>[-]++++++++[-<------>][-]<[->
>+<+<]>>[-<<+>>][-]++++++++++<[->>[-]<[->>+<+<]>>[-<<+>>]+<[[-]<->>-<]>[-<<<[-]>
>>]<<<]>>+<[[-]<<[-<<<<++++++>>>>]>>>-<]>[-<<<<<<<<[-]>>>>>[-]>>>]<<<<<][-]<<<[-
>>>>+<+<<<]>>>>[-<<<<+>>>>]<[[-]>>,>[-]++++++++[-<------>][-]<[->>+<+<]>>[-<<+>>
][-]++++++++++<[->>[-]<[->>+<+<]>>[-<<+>>]+<[[-]<->>-<]>[-<<<[-]>>>]<<<]>>+<[[-]
<<[-<<<<+++++>>>>]>>>-<]>[-<<<<<<<<[-]>>>>>[-]>>>]<<<<<][-]<<<[->>>>+<+<<<]>>>>[
-<<<<+>>>>]<[[-]>>,>[-]++++++++[-<------>][-]<[->>+<+<]>>[-<<+>>][-]++++++++++<[
->>[-]<[->>+<+<]>>[-<<+>>]+<[[-]<->>-<]>[-<<<[-]>>>]<<<]>>+<[[-]<<[-<<<<++++>>>>
]>>>-<]>[-<<<<<<<<[-]>>>>>[-]>>>]<<<<<][-]<<<[->>>>+<+<<<]>>>>[-<<<<+>>>>]<[[-]>
>,>[-]++++++++[-<------>][-]<[->>+<+<]>>[-<<+>>][-]++++++++++<[->>[-]<[->>+<+<]>
>[-<<+>>]+<[[-]<->>-<]>[-<<<[-]>>>]<<<]>>+<[[-]<<[-<<<<+++>>>>]>>>-<]>[-<<<<<<<<
[-]>>>>>[-]>>>]<<<<<][-]<<<[->>>>+<+<<<]>>>>[-<<<<+>>>>]<[[-]>>,>[-]++++++++[-<-
----->][-]<[->>+<+<]>>[-<<+>>][-]++++++++++<[->>[-]<[->>+<+<]>>[-<<+>>]+<[[-]<->
>-<]>[-<<<[-]>>>]<<<]>>+<[[-]<<[-<<<<++>>>>]>>>-<]>[-<<<<<<<<[-]>>>>>[-]>>>]<<<<
<][-]<<<[->>>>+<+<<<]>>>>[-<<<<+>>>>]<[[-]>>,>[-]++++++++[-<------>][-]<[->>+<+<
]>>[-<<+>>][-]++++++++++<[->>[-]<[->>+<+<]>>[-<<+>>]+<[[-]<->>-<]>[-<<<[-]>>>]<<
<]>>+<[[-]<<[-<<<<+++++++>>>>]>>>-<]>[-<<<<<<<<[-]>>>>>[-]>>>]<<<<<][-]+[>[-]<<<
[->>>>+<+<<<]>>>>[-<<<<+>>>>][-]+++++++++++<[->>[-]<[->>+<+<]>>[-<<+>>]+<[[-]<->
>-<]>[-<<<[-]>>>]<<<]>>+<[[-]<<[-]>>>-<]>[-<<<<<----------->>>>>]<<<][-]<<<[->>>
>+<+<<<]>>>>[-<<<<+>>>>]<[[-]>>,>[-]++++++++[-<------>][-]<[->>+<+<]>>[-<<+>>][-
]++++++++++<[->>[-]<[->>+<+<]>>[-<<+>>]+<[[-]<->>-<]>[-<<<[-]>>>]<<<]>>+<[[-]<<[
-<<<<++++++>>>>]>>>-<]>[-<<<<<<<<[-]>>>>>[-]>>>]<<<<<][-]<<<[->>>>+<+<<<]>>>>[-<
<<<+>>>>]<[[-]>>,>[-]++++++++[-<------>][-]<[->>+<+<]>>[-<<+>>][-]++++++++++<[->
>[-]<[->>+<+<]>>[-<<+>>]+<[[-]<->>-<]>[-<<<[-]>>>]<<<]>>+<[[-]<<[-<<<<+++++>>>>]
>>>-<]>[-<<<<<<<<[-]>>>>>[-]>>>]<<<<<][-]<<<[->>>>+<+<<<]>>>>[-<<<<+>>>>]<[[-]>>
,>[-]++++++++[-<------>][-]<[->>+<+<]>>[-<<+>>][-]++++++++++<[->>[-]<[->>+<+<]>>
[-<<+>>]+<[[-]<->>-<]>[-<<<[-]>>>]<<<]>>+<[[-]<<[-<<<<++++>>>>]>>>-<]>[-<<<<<<<<
[-]>>>>>[-]>>>]<<<<<][-]<<<[->>>>+<+<<<]>>>>[-<<<<+>>>>]<[[-]>>,>[-]++++++++[-<-
----->][-]<[->>+<+<]>>[-<<+>>][-]++++++++++<[->>[-]<[->>+<+<]>>[-<<+>>]+<[[-]<->
>-<]>[-<<<[-]>>>]<<<]>>+<[[-]<<[-<<<<+++>>>>]>>>-<]>[-<<<<<<<<[-]>>>>>[-]>>>]<<<
<<][-]<<<[->>>>+<+<<<]>>>>[-<<<<+>>>>]<[[-]>>,>[-]++++++++[-<------>][-]<[->>+<+
<]>>[-<<+>>][-]++++++++++<[->>[-]<[->>+<+<]>>[-<<+>>]+<[[-]<->>-<]>[-<<<[-]>>>]<
<<]>>+<[[-]<<[-<<<<++>>>>]>>>-<]>[-<<<<<<<<[-]>>>>>[-]>>>]<<<<<][-]+[>[-]<<<[->>
>>+<+<<<]>>>>[-<<<<+>>>>][-]+++++++++++<[->>[-]<[->>+<+<]>>[-<<+>>]+<[[-]<->>-<]
>[-<<<[-]>>>]<<<]>>+<[[-]<<[-]>>>-<]>[-<<<<<----------->>>>>]<<<][-]<<<[->>>>+<+
<<<]>>>>[-<<<<+>>>>]<[[-]>>,>[-]++++++++[-<------>][-]<[->>+<+<]>>[-<<+>>][-]+++
+++++++<[->>[-]<[->>+<+<]>>[-<<+>>]+<[[-]<->>-<]>[-<<<[-]>>>]<<<]>>+<[[-]>-<]>[-
<<<<<<<<[-]>>>>>[-]>>>]<<<<<][-]<<<[->>>>+<+<<<]>>>>[-<<<<+>>>>]<[[-]>>>[-]<<<<<
[->>>>>>+<+<<<<<]>>>>>>[-<<<<<<+>>>>>>][-]++<[->>[-]<[->>+<+<]>>[-<<+>>]+<[[-]<-
>>-<]>[-<<<[-]>>>]<<<]>>+<[[-]<<[[-]<<<<<[-]>>>>>]>>>-<]>[->>[-]+++++++++++<<<<<
[->>>>>-<<<<<]<<<<[->>>>>>>>>-<<<<<<<<<]>>>>>>>>>[[-]<<<<<<<<<<[-]>>>>>>>>>>][-]
<<]<<<<<][-]<<<[->>>>+<+<<<]>>>>[-<<<<+>>>>]<[[-]>>,>[-]++++++++[-<------>][-]<[
->>+<+<]>>[-<<+>>][-]++++++++++<[->>[-]<[->>+<+<]>>[-<<+>>]+<[[-]<->>-<]>[-<<<[-
]>>>]<<<]>[[-]<<<<<<<[-]>>>>>>>]<<<<]>>[-]<<+<<<[[-]>>>>[-]+++++++++++++++++++++
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++.[-]+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++++++++++++++++++++++++++++++.[-]+++++++++++++++++++++++
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
++++++++++++++.[-]++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++++++++++++++++.[-]++++++++++.[-]<-<<<]>>>[->[-]++++++++
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
++++++++++++++.[-]++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++++++++++++.[-]+++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.[-]+++++++++
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
++++++++++++++++++++++++++.[-]++++++++++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++++++++++++++++++++++++++++.[-]++++++++++.[-]<]

コードゴルフ的にはかなり短縮できそうな感じですが、とりあえず動くものができました。

Brainf*ck で実用的(?)なコマンドを作ろう

動くものができたのですが、Brainf*ck のインタプリタは、残念ながらどこにでもあるとは言いにくい状況です。

そこで、Brainf*ck のコンパイラ(正確には C 言語へのトランスレータ)を用意して、実行可能ファイルにすることにします。

きっと皆さんは一度ぐらい Brainf*ck のインタプリタを作っていると思います。

C 言語へのトランスレータレベルのなんちゃってコンパイラであれば、Brainf*ck の場合、インタプリタよりも簡単に実装できると思います。
また、(何の説明もなく前述していた通り) C 言語のコンパイラの賢い最適化に頼ることも可能になります。

コンパイラ(トランスレータ)としては、以下の物を使用します(昔、私が作った物です)。

このコンパイラと GCC を使って、上記のマイナンバー検証プログラムを実際に作ってみます。

$ python3 my_number_validator.py | fold > my_number_validator.bf
$ ./bf2c -o my_number_validator.c -M -V "my_number_validator 0.0.1" -C "MIT License" my_number_validator.bf
$ gcc -O2 my_number_validator.c -o my_number_validator
$ ./my_number_validator 123456789018
true
$ ./my_number_validator 123456789017
false
$ ./my_number_validator -h
my_number_validator 0.0.1
MIT License
Usage:
  ./my_number_validator [options] [ <input-message> ]
Options:
  -f, --file <file>   : input file path.
  -m, --message <str> : input message.
  -o, --output <file> : output file path.
  -v, --version       : display version information.
  -h, --help          : display help message.
  -s, --size <number> : array size (default:30000)

ちなみに、Brainf*ck の入出力は、通常、標準入力と標準出力に割り当てられているのが一般的なイメージがあります。
しかし、実際にはそのような制限はありません(少なくとも言語仕様的には)。

それこそ、1つの入力ポートと1つの出力ポートを持つ、埋め込み機器上でも Brainf*ck のプログラムは動かせるはずです。

上記コンパイラでは、標準入力/ファイル/コマンドライン引数のどれをデフォルトの入力とするかを指定が可能です。
今回のコンパイル時には -M をつけて、コマンドライン引数をデフォルトとしています。

また、一般的なコマンドオプションである -h --help -v --version にもデフォルトで対応しています。
コンパイル時にバージョンメッセージなどは指定が可能です。

このようなコンパイラなどを用意することで、Brainf*ck でも実用的なコマンドを作成することが可能になります。

ぜひ、Brainf*ck でいろいろな実用的なコマンドなどを作ってみてください。

おまけ

今回は、比較的小さめのコマンドだったのでそれほど難しくなかったのですが、複雑な処理を実施しようとすると、どうしてもワークエリアの管理などが面倒になってきます。

そこで、スタック処理言語的なマクロ(あるいは簡易言語)を作ってみると、ワークエリアの管理が少し楽になるかもしれません。

以前作った、マンデルブロ集合を描くプログラムは、実際にスタック処理言語的に作ってみました。

また今回、最適化は C 言語のコンパイラの力に頼り切りでしたが、トランスレータで頑張る案もあります。
例えば、以下のような先人の知恵も参考になるかもしれません。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0