Skip to content

Instantly share code, notes, and snippets.

@boletales
Created October 19, 2024 14:06
Show Gist options
  • Save boletales/165ef25131e643d8037172f445c334c2 to your computer and use it in GitHub Desktop.
Save boletales/165ef25131e643d8037172f445c334c2 to your computer and use it in GitHub Desktop.
まなみのなかみ(副題:簡単!プログラミング言語のつくりかた)

まなみのなかみ(副題:簡単!プログラミング言語のつくりかた)

#深夜の真剣レポート60分一本勝負 第2回 お題:「好きな一言」

みなさん、好きな一言はありますか? 私はDiscord bot「うだまなみ」の起動時メッセージ「おはようっ!」です。

前提

友人のDiscordサーバーに導入されているDiscord bot「うだまなみ」には、「!1d6」のようなダイスロールコマンドがあり、極めて好評でした(!6d200でポケモンの種族値を生成する、などの遊び方を観測しました)。

ダイスロールコマンドへの入力として「!1d6*10」などの計算式を試しているひとがいたので、私はふと思い立って電卓機能「!calc」コマンドを開発しPRを送ることにしました。

!calcの仕様

!calcは、入力文字列を計算式として読み取り、評価し、その結果を返信する、電卓のようなコマンドです。計算式として、以下のものがサポートされます:

  • 整数・浮動小数点数
  • 四則演算・冪乗・ダイスロール
  • 基本的な関数(sinとか、lnとか、†fix†とか)
  • 配列・オブジェクト
  • †ラムダ式†

……電卓?

入力例

!calc 1+2*3

7

!calc (1d(24*7))+"時間ぐらいまでなら遅刻してもええんちゃう?"

"160時間ぐらいまでなら遅刻してもええんちゃう?"

!calc (l => "H:"+l[0]+" A:"+l[1]+" B:"+l[2]+" C:"+l[3]+" D:"+l[4]+" S:"+l[5]+" (total:"+sum(l)+")")(repeat(()=>1d200, 6))

"H:108 A:128 B:43 C:196 D:19 S:112 (total:606)"

動作の概要

!calcの中身は、一般的にはプログラミング言語の「インタプリタ」と呼ばれるものです。これは、以下のように動作します:

  1. 入力文字列を解釈し、抽象構文木(略称:AST 後段で扱いやすい形式の数式のデータ)を生成する。
  2. 得られたASTに対して、「評価」(数式の内容に応じて、適切な方法で計算をすること)を行う。

順に見ていきましょう。

入力文字列の解釈

「まなみちゃんは、わたしのともだちのいもうとです。」←この文を音読してみてください。

日本語に慣れ親しんだひとであれば、「ま」「な」「み」……と、一文字ずつ区切って読むようなことはしなかったと思います。

おそらく、

「まなみちゃん」は
    「わたし」の
  「ともだち」の
「いもうと」です。

というように、文中から意味のある単語を切り出し、修飾-被修飾の関係を理解した上で音読したはずです。

同じように、!calcに渡された文字列「1-2*3+4」は、算術演算子の順序を考慮したうえで、次のような抽象構文木に解釈されます:

加算(
  減算(
    整数要素「1」,
    乗算(
      整数要素「2」,
      整数要素「3」
    )
  ),
  整数要素「4」
)

執筆時間が60分しかないので深入りしませんが、このような文字列の解釈をするプログラムは「パーサ」と呼ばれています。今回はnomというパーサを実装するためのライブラリを利用しました。

評価

次に、得られた抽象構文木から計算結果の値を得る必要があります。

ここで、抽象構文木上の要素と計算結果は、は厳密に区別しなければいけません。(区別しないと実装する際に頭がこんがらがって死にます)

抽象構文木上の、加算(整数要素「1」, 整数要素「2」) 浮動小数点要素「9.8」は、計算の「命令」のようなものです。

一方で、計算結果の「1」や「9.8」は、計算の「結果」です。

上で出てきた要素のそれぞれについて、以下のような規則で評価を行います:

  • 整数要素「n」:nを計算結果として返します。
  • 加算・減算・乗算(構文木a, 構文木b)aを評価し、bを評価し、その結果を足したものを計算結果として返します。

この規則で、先に挙げた構文木を評価してみます。

  • 一番外側の加算を評価するために、その内側の2つの構文木を評価し、結果を足します。
    • 内側の減算を評価するために、さらに内側の乗算整数要素を評価し、結果を引きます。
      • 整数要素「1」を評価すると、計算結果として1が返ります。
      • 乗算を評価するために、整数要素「2」整数要素「3」を評価し、結果を掛けます。
        • 整数要素「2」を評価すると、計算結果として2が返ります。
        • 整数要素「3」を評価すると、計算結果として3が返ります。
        • そのため、この乗算の計算結果は 2*3 = 6です。
      • そのため、この減算の計算結果は 1-6 = -5です。
    • 整数要素「4」を評価すると、計算結果として4が返ります。
    • そのため、この加算の計算結果は -5+4 = -1です。

以上のような再帰的な「評価」を繰り返すと、複雑なASTから計算結果を得ることができます。

なお、ラムダ式の評価については「評価戦略」だとか「クロージャー」だとかいった複雑な話が出てきますが、時間切れなので割愛します。気が向いたら来週書く

まとめ

一般のプログラミング言語でも、「インタプリタ」と呼ばれる種類の動作をするものは、以上の2つの手順で動作しています。なお、コンパイラと呼ばれるものは、2つ目の手順で計算を実際に行う代わりに、より機械寄りのコードを生成します。こいつの実装は100倍大変なので、みなさんもやってみてください。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment