これは「語学・言語学・言語創作 Advent Calendar 2023」の 18 日目の記事です。
こんにちは、初めてアドカレに参加させていただいたぐらふぃーむです。
Migdal で記事を投稿するのも初めてということで、まず香港でお馴染みのアニメキャラクターのマクダル(麥兜 /mɐ̀k̚.tɐ́w/)を供養します1:
すべての表現は自然言語で述べている以上曖昧さを生じかねません。人工言語で書けば解決できると思うかもしれませんが、一般的な文は構造的な多義性のない人工言語で書くとしても語彙的な多義性は完全に排除できるわけではありません。そこで、数学のような厳密な分野なら意味論での曖昧さはないので、完璧に曖昧さのない文が作れるのではと思い、今回は Mathematica というパワフルな数学ソフトで入力された数式をその場でロジバンの表記に変えられるプログラムを書いてみました。
プログラムは主にロジバン大全(The Complete Lojban Language)という本のコミュニティ版[PDF]の第 18 章を参考に作っています。
技術に偏りすぎないよう、Wolfram 言語(後述)の関数や演算子などの説明の割愛は多々あります(それでも随分偏ってますが)。ご了承ください。
もしこの記事に誤字・脱字やおかしな表現、それとこの記事に対するご意見があれば、是非コメント欄で教えてください!
では本文に入りましょう。
概要
ロジバンについて
周知のように、Lojban は世界で最も有名な人工言語の一つであり、文法的な曖昧を排除しつつ柔軟性を持たせるように、そして何よりコンピューターも人間も容易に理解できるように設計されています。また、重要な性質として、ロジバンで書かれた文はすべて述語論理の命題として解釈しうるので、数学的記法で書き換えることもできます。
Mathematica とは
Mathematica は、ロジバンが1歳を迎えた 1988 年に初めてリリースされた数式処理システムです。機能は多岐にわたり、数学に限らず、科学、天文学、地理学から自然言語処理を含む機械学習にまで、幅広く応用されています。Mathematica は Wolfram Language というプログラミング言語を使って様々な操作を利用できます2。Wolfram Alpha も Mathematica の上に構築されているし、Mathematica で制限なく Wolfram Alpha にアクセスできます(今回は使いませんが)。ドキュメントもとても豊富で、(英語から)日本語(と中国語のみ)に 100% 翻訳されています。
例を挙げるとバブルソートはわずか1行で実装できます3:
{6, 3, 4, 2, 5, 1} //. {x___, y_, z_, k___} /; y > z -> {x, z, y, k}
(* = {1, 2, 3, 4, 5, 6} *)
「6・3・4・2・5・1」という配列を、「任意の長さの要素によるシーケンス x・要素 y・要素 z・任意の長さの要素によるシーケンス k」でできた配列のうち y > z の場合に限り「シーケンス x・要素 z・要素 y・シーケンス k」に繰り返して置き換える
もちろん、ビルトインの関数は 6000 個を超えていますので、ソートを自分で実装する必要はありません。
Q&A: Wolfram Language ではなく他の言語で書けばずっと楽勝なのでは?
全くもってそうです。とても大変でしたが、自分の慣れ親しんだ TypeScript ⊃ ECMAScript (JavaScript) よりも慣れる余地のある Wolfram Language でやった方がたくさん学べるし、ビルトインで数式を入力できる数学ソフトでやりたかったので。あと楽しかったので無問題。
下準備
前置きはこれくらいにして、実践していきたいと思います。
大雑把に言うと、構造を取得して再帰で式を変換していく作業をしていきます。構文を評価(Evaluate)しないようにするには Hold
、完全形にするには FullForm
を、可視化するには TreeForm
を使います4:
ここで問題が発覚されました。Mathematica では引き算と割り算はそれぞれ「a + -1 × b」と「a × b⁻¹」の形に自動的に置き換えられることが分かります。単項マイナス演算も同じく、-1 を掛ける形に置き換えられます。これを解決するためには、計算を影響しない中継の形式に上書きします。ここは厄介なところがあまりにも多く、話をしすぎると趣旨から離れちゃいますので、後日技術記事に特化した Qiita で解説したいと思います。
これを書くだけですごく時間かかってしまって大変でした。
追記:しかしこのハックにも盲点がある。「計算を影響しない」ようにしていますので、下の「計算させよう」の節が示したように、評価した出力はこの中継の形に置き換えることはありません。
数をロジバンに
ここからが本番です。まずは数をロジバンの読みに変えましょう。
まずは小数を扱います。RealDigits
を使うと桁を取り出すことができ、整数部分と小数部分の切れ目も教えてくれますので、TakeDrop[list, index]
を使って配列を分割します(Wolfram 言語では配列の要素は1から数えます。後述)。%
は直前の出力結果を表します:
しかし末尾に0が生えてしまうことが分かります。さらにいくつかの例を試すと、切れ目は負の数もあれば、配列の長さを超えることもあります:
これらを解決するためには、配列を ArrayPad
で適宜に0を充填・削除し、インデックスを負でない数に制限します:
次に、Partition
で各部分をそれぞれ3桁ずつ分けます。整数部分は右寄せ(-1)、小数部分は左寄せ(1)です:
ロジバン大全(CLL)の 18.7 節より、分けた各グループの先頭のゼロは省略できます……としていますが、小数部分に4桁以上(2グループ以上)ないと ki'o は入らないので、0.1 と 0.001 を区別するためにゼロは省略できません。言い換えると、ki'o の後ろにくる no は消去できますが、小数部分に ki'o がない限り pi の後ろにくる no は消去できません。このルールに従ってゼロを消します:
最後に各桁をロジバンにしてセパレーターを入れれば概ね完成です(10~19 の桁の名前は今後のために残しておきます):
{Re[z], Im[z]}
を返す ReIm[z]
を使えば、複素数もそのままいけます:
式がやや乱れているので整理し関数化するとすっきりします:
ロジバン大全には「演算子には優先順位はない」と明記しましたが、この部分から筆者が勝手に優先順位を「ka'o > fi'u = pi > ki'o」と指定したことが分かります。そもそも順位がないのは VUhU 類の方だけで PA 類には何も言っていないと思います。(間違ったらコメント欄で教えてください!)
変数と定数をロジバンに
愚直に文字毎に変換する関数を書きます5。Characters
は文字列を分解し配列にして返す関数です:
ここで二つの問題点が見られます:
- 大文字を処理していない
- ピリオドが連続する
そこで SequenceReplace
で並ぶ大文字を「ga'e …… to'a」で囲んで StringReplace
で連続するピリオドを一つにします:
あとは定数も処理します。CLL で名前が定義されたものであればそのまま使い、それ以外は上記の関数で名前を出します:
構文を解析する
Wolfram 言語で構文を扱えるようにするには一つのコンセプトを理解する必要があります:
すべての式は頭部(Head)と要素(引数、パラメータとも)でできています。例えば f[x, y]
の頭部は f
、要素は x
と y
です。頭部は定義されない限り何も行いませんので、単にラベルに過ぎません。このように未定義の頭部を使うのはよくあることです。なお配列の頭部は List
なので、{x, y, z}
の完全形は List[x, y, z]
になります。
各成分を取り出すには ……[[n]]
を使います。第0要素は頭部、第1以降が引数です。よって {x, y, z}[[1]]
と (x + y + z)[[1]]
はいずれも x
です。なぜなら x + y + z
の完全形は Plus[x, y, z]
で、二つの式の違いは頭部にしかありません。……[[0]]
と ……[[1]]
はそれぞれ Head[……]
と First[……]
に等しいです。また、exp = f[g[a, b, c]]
とすると、exp[[1, 0]]
は g
、exp[[1, 1]]
は a
になります:6
それでは mekso
(ロジバンで「数式」)という関数を考えます。mekso
に与えた式が評価されないよう二つの措置を取る必要があります。
-
mekso
に処理される前に評価されないようmekso
自体にHoldAll
という属性を適用します。 -
mekso
に処理される途中で誤って評価されないよう初っ端から与えた式をHold
で囲みます。
次に、Hold
で囲まれた式をどうするかを考えます。最初はシンプルに 1 + 1 == 2
から始まりましょう。
一旦、式の完全形を出力するように書いて、後で上書きします:
どの演算を行うのは頭部によって決まりますので、まず頭部を抽出します。しかし Hold
もまた一つの式なので Head[exp]
は Hold
になってしまいます。そこで、本当の中身の頭部を取り出すには exp[[1, 0]]
を使います。稀に頭部は評価する余地がありますので、特別に exp[[1, 0]]
ではなく Extract[x, {1, 0}, Hold]
を書くことで、頭部を取り出すことと同時に Hold
で囲みます。これを head
関数として定義します:
次に各要素も抽出します。同様に Hold
で囲む必要があります。つまり Hold[f[x, y, z]]
を {Hold[x], Hold[y], Hold[z]}
に変えられる関数を考えます。そこで @@@
7 で中身の頭部を配列(リスト)に置き換えて Distribute
で分配します(Distribute
は元々「a × (b + c)」を「a × b + a × c」に展開する(分配法則の適用という)のに使う関数です)。これを oprands
関数として定義します:
成分を取得する関数があったら、後は頭部に応じて式を変換するだけです。最後に配列を平坦化(フラット化)し各要素の間に空白文字を入れます:
このように演算子を種類に応じて分類し追加していけばほぼ完成です:
では試してみましょう:
ここで、そのまま平坦化してはいけないことが分かります。構造を括弧に該当する「vei …… ve'o」で囲む必要があります。
演算子には優先順位はなく、左からの順番に計算していくので、各構造の一番左の要素は配列であれば平坦化できます。それ以外は「vei …… ve'o」で囲み、各 sumti の末尾の「ve'o」と「ku'e」、それと「ve'o」直前の「ku'e」を消します:
これでようやくなんとか出来上がりだと言えるでしょう。
計算させよう
最後に、数学ソフトを活用する面白い遊びをします。式を Evaluate
で囲むと、HoldAll
属性や Hold
は無視され中身が評価されます。この原理を使って kanji
(ロジバンで「計算」)という関数を定義して、解答をロジバンで明らかしましょう:
終わりに
如何だったでしょうか。年末の忙しい時期にこれほど時間を費やしてしまったのは正解かどうか分かりませんが、今回を通じてロジバンと Mathematica についてたくさん学んだことは間違いないでしょう。
「語学・言語学・言語創作」の趣旨から離れてしまったと思うので、申し訳なく思いますし、これはちゃんとした記事かどうかすら疑わしいところです。アドカレに参加している皆さんとこの記事を拝見している皆さんはすごい方ばかりで、私はこれくらい拙い文章しか書けない気もします。
続編は、十進法以外の数、指数表記、複素数の扱いや、微積、線型代数、集合論、命題論理、述語論理などの分野に使われる記号と表現を順次対応していきたいと思います。今後さらに余裕ができたら、これとは逆のパーサー(ロジバンで書かれた数式を Wolfram 言語や などの形式に変換する解析器)ももしかしたら作りますのでお楽しみください!
一読してくださった皆さんに感謝の意を申し上げます。
Q&A: なぜラベルの
In[…]:=
とOut[…]=
は水色じゃなく緑色なの?思うままに変えました。UI のテーマファイルを弄ったわけではありません。実は以上の写真と文書はどうやって作成したかというと、
Notebook
関数で.nb
ファイルの中身と同じように作ったノートブックをそれぞれExport
とCloudDeploy
で出力/アップデートしています。自分で作っている上、色々カスタマイズできますので、関数の翻訳ラベルをオフし、セルのラベルを緑色に変えました。
-
Migdal は元々ヘブライ語で「塔」という意味らしいですが、これとは無関係の言葉遊びとして捉えていただければと思います。 ↩
-
Wolfram 言語のみでアプリケーションを作ることも普通にできます。実際、Mathematica の中身の大部分は Wolfram 言語で書かれています。 ↩
-
https://en.wikipedia.org/wiki/Wolfram_Language#Pattern_matching ↩
-
exp // f
とf[exp]
とf@exp
の違いはありませんので…… // Hold // FullForm
はFullForm[Hold[……]]
に等しいです。 ↩ -
今回は ASCII のアルファベットと数字のみ扱うとします。 ↩
-
https://reference.wolfram.com/language/tutorial/Expressions.html ↩
-
f @@@ exp
の完全形はApply[f, exp, {1}]
です。バージョン 13.1 以上ではMapApply[f, exp]
と書くこともできます。 ↩
新しい順のコメント(3)
マクダルちゃんかわいい!
そこかい
ロジバン知らないし数学も苦手なもので((