ChatGPTを用いた日本語による自作プログラミング言語の実装とレポートを間違えて提出してくる数式処理ソフトと化したChatGPT


# あらまし

ChatGPTでは、ものごとを説明する文を与えることで知識の追加や上書きができる。
また、既知の情報を組み合わせて論理的な思考を行わせることができる。
そこで、ChatGPTに日本語で文法の定義を行うことで、自作プログラミング言語Sanaの実装を試みた。

Sana言語には、最終的に四則演算、条件分岐、ラムダ式の文法が追加された。
ChatGPTにSana言語のコードを与えると、計算結果を得ることができるようになった。
ただし、ChatGPTはSana言語を不完全に数式処理し、しばしば間違ってしまった。

# ChatGPTについて

[ChatGPT](https://ja.wikipedia.org/wiki/ChatGPT)とは、チャットAIのひとつである。
自然言語などを入力でき、多くの場合、自然言語の形で返してくる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/0cae26a1-4856-77e1-6b4b-d9a0214ee03c.png)

その他にも、プログラム的なコードを出力させることもできる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/2eb85000-308d-29cc-2fbc-02dbaf331e6e.png)

このチャットAIは決められたテンプレートを返すのではなく、ちゃんと「頭で考えて」その場でコードを生成しているように見える。
実際に試していくと、自然言語で扱えるものであれば、かなり様々な思考ができるようである。

# カリー＝ハワード同型対応

ところで、[カリー＝ハワード同型対応](https://ja.wikipedia.org/wiki/%E3%82%AB%E3%83%AA%E3%83%BC%EF%BC%9D%E3%83%8F%E3%83%AF%E3%83%BC%E3%83%89%E5%90%8C%E5%9E%8B%E5%AF%BE%E5%BF%9C)という原理があり、数学的な証明はコンピューターで扱うことができる。
述語は型、証明はプログラムに対応し、数学の証明とプログラムの実装の類似性を示している。

そして、「Aならば、Bである」という文章は、「A型の引数を受け取り、B型の値を返す関数」と対応する。

例えば、Wikipediaの[三段論法](https://ja.wikipedia.org/wiki/%E4%B8%89%E6%AE%B5%E8%AB%96%E6%B3%95)の記事にあるこれは、概ね次のような疑似コードとして表せる。

```
全ての人間は死すべきものである。
ソクラテスは人間である。

ゆえにソクラテスは死すべきものである。
```

```
function 人間死亡定理(h : 人間) : 死すべき
function ソクラテス人間定理(s : ソクラテス) : 人間

var d : 死すべき = 人間死亡定理(ソクラテス人間定理(ソクラテス.INSTANCE))
```

これを見ただけでも、言語的な洞察の過程をコンピュータで処理できる可能性を感じさせる。

---

実際、冒頭の怪文書は、詩を作る関数とハンバーガーが火星人であるという定理を合成して出来ている。
「A is B」という形の文をたくさん与えれば、それ全体が一つのプログラムのように振舞うことができるのだ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/c0c923ed-329a-774e-fb19-529196349548.png)

ChatGPTは、細かく条件を与えることで、それなりの確率で論理的に正しい答えを導くことができる。

# 日本語でプログラミング

一般的な言語処理系では、コードを入力することで何らかの計算を行い、結果が出力される。
ChatGPTでは、日本語の文章を与えることで何らかの処理を行い、結果が出力される。

ChatGPTがどこまで一般的なプログラミング言語の処理を模倣できるかについては不明な点が多いものの、
様々な条件を組み合わせて詩などの生成をすることができる。
これはもはや日本語プログラミング言語と呼んでも過言ではないのではないだろうか。

# Sana言語

であるならば、「A is B」という形式の定義のリストを宣言的に与えることによって、
VM言語「日本語」を用いて自作プログラミング言語「Sana」の実装を試みてみよう。
Sanaの語源は三好紗南（アイドル育成ゲームの登場人物）である。

## 範囲演算子 `整数 .. 整数`

まずはChatGPTが記号等を適切に処理できるかを見てみよう。
入力は記号を交えた日本語で与え、文法の定義とテストの実行という形式を採る。
以下では、上下に並んだ引用ブロックの上側が筆者による入力であり、下側がChatGPTによる出力である。

> プログラミング言語Sanaでは、二つの整数の間に「 .. 」を書くことで、左の整数から始まり、1ずつ増加し、右の整数で終わる整数の配列を生成することができます。例えば、「3 .. 5」では、3と4と5からなる配列を作ることができます。それでは、プログラミング言語Sanaで15から21までの整数の配列を生成するにはどのように記述すればよいですか？

> Sanaでは、「..」演算子を使用して、範囲を指定した整数の配列を作成することができます。15から21までの整数の配列を作成するには、以下のように書くことができます。
> 
> ```
> 15..21
> ```
> 
> これは、15から21までの整数を1ずつ増加させた整数の配列を生成します。具体的には、[15, 16, 17, 18, 19, 20, 21]という配列が生成されます。
> 
> Sanaでは、このように範囲演算子を使用して、簡単に整数の配列を生成することができます。

日本語を読解し、定義に従って、問題に正しく回答できている！
感動である。

## パイプ演算子 `配列 | 式`

次は筆者が好きな、JavaScriptのArray#mapメソッドに相当するパイプ演算子を定義してみる。
これはシェルスクリプトや[jq](https://ja.wikipedia.org/wiki/Jq_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E))などではよく使われる演算子であり、1文字で済むのが楽である。

> Sanaでは、配列と式の間を「 | 」で区切って記述することで、左辺の配列の各要素に対して、右辺の式を評価した結果である配列を得ることができます。左辺の配列の要素は、右辺では「\_」という1文字のアンダースコアという特別な変数で参照することができます。式では、足し算「+」、引き算「-」、掛け算「*」、割り算「/」の四則演算を使うことができます。例えば、「2 .. 5 | \_ + 10」というSanaの構文は、12、13、14、15という配列になります。それでは、「1 .. 10 | \_ * 100 + 23」というSana言語の構文を評価すると、どのような結果になりますか？

> 「1 .. 10 | \_ * 100 + 23」というSanaの構文は、1から10までの整数の配列を作成し、それぞれに100を掛けて23を足した結果の配列を生成します。つまり、以下のようになります。
> 
> [123, 223, 323, 423, 523, 623, 723, 823, 923, 1023]
> 
> 具体的には、
> 
> - 1 * 100 + 23 = 123
> - 2 * 100 + 23 = 223
> - 3 * 100 + 23 = 323
> - 4 * 100 + 23 = 423
> - 5 * 100 + 23 = 523
> - 6 * 100 + 23 = 623
> - 7 * 100 + 23 = 723
> - 8 * 100 + 23 = 823
> - 9 * 100 + 23 = 923
> - 10 * 100 + 23 = 1023
> 
> となります。

一般的なプログラミング言語の知識を利用させないために、
敢えてあまり一般的でない演算子をいきなり導入したが、正しく理解することができた。
これからの時代、文章で定義されたマニュアルというのも案外捨てたものではないのかもしれない。

## タプル演算子 `[要素, ...]`

ここで、将来的に九九表を出力することを考えてみる。
九九表を出力するためには、値を縦に並べるルールと横に並べるルールを定義しなければならない。
しかしながら、printlnを複数回呼び出すことで縦に整形しようとすると、命令型になりややこしくなってしまう。

そこで、これまでに登場していた「配列」を出力する際には縦に出力することに仕様を変更する。
そして、横に並べて表示する配列の亜種「タプル」を新しく追加する。
これにより、タプルの配列を出力させることで表を表示できる。

配列は縦、タプルは横である。

> Sana言語では、配列の他にタプルという概念があります。タプルは配列に似ていますが、それ自体で単一の値のように扱います。タプルから要素を取得する場合、「タプル[番号]」のように記述します。番号は0から始まります。負の番号や、タプルの要素数に収まらない番号を与えると、「添え字≪添え字≫はタプルの境界範囲≪指定できる最小の添え字≫..≪指定できる最大の添え字≫の範囲内にありません！」という文を例外として出力し、そこで計算を中断します。タプルは、半角カンマおよび半角角括弧を使って表現します。例えば、「[2,3,4]」では、2、3、4からなるタプルを表します。
> 
> Sana言語では、配列を画面に表示する際は1要素ごとに1行ずつ、タプルを画面に表示する際は各要素を「,」で区切って表示します。Sana言語では、配列を作る際には、括弧類で囲む必要はありません。すなわち、「2,3,4」は2、3、4からなる配列を表します。
> 
> それでは、「[1,2,3],[4,5,6],[7,8,9]」というSana言語の構文を評価してください。

> 「[1,2,3],[4,5,6],[7,8,9]」は、3つの要素からなるタプルを表しています。各要素は、それぞれ「[1,2,3]」「[4,5,6]」「[7,8,9]」となります。これを画面に表示する場合、以下のようになります。
> 
> ```
> [1, 2, 3]
> [4, 5, 6]
> [7, 8, 9]
> ```

ちゃんと数値が正しくマトリクス状に並んでいる！
配列は縦に並べるというルールの上書きも正しく処理されている。

ちなみに、実際のChatGPTの出力結果では、角括弧がハイライトされて出力されていた。
全く未知の言語にもかかわらず気が利いている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/d688d558-5c79-b92e-4c8f-7c4ee868fac5.png)

## 括弧演算子 `(値)`

ここで、「配列の配列を作ろうとすると自動的に平坦化されて1次元配列になる」という衝撃のルールを追加してみよう。
このルールは、パイプ演算子 `|` がシェルスクリプトの模倣であること、
そして配列要素が1行分のテキストを表すことを考えるとこの意図を理解しやすいかもしれない。

> Sana言語では、任意の部分を半角丸括弧で囲うことで、その部分をまとめることができます。すなわち、「(1 + 2) * 10」の結果は30となります。
> 
> Sana言語では、配列を配列の中に入れることはできず、自動的にフラットになります。すなわち、「1,2,(3,4),5,6」は、「1,2,3,4,5,6」と同じ結果を得ます。また、配列をタプルに格納することもできず、ただのタプルになります。すなわち、「[1,2,(3,4),5,6]」は、「[1,2,3,4,5,6]」と同じ結果を得ます。一方、タプルはそれ自体が一つの値のように扱われるため、タプルが別のタプルや配列の要素となっても、自動的に展開されることはありません。それでは、「1 .. 7 | [1, 2 .. 5, \_, 7]」というSana言語の構文を評価してください。


> 「1 .. 7」という配列は、[1,2,3,4,5,6,7]となります。そして、各要素に対して「[1, 2 .. 5, \_, 7]」というタプルを適用します。このタプルは、1,2,3,4,5,6,7の各要素に対して、1,2,3,4,5,7という値を返します。したがって、最終的な結果は以下のようになります。
> 
> ```
> [1, 2, 3, 4, 5, 7]
> ```
> 
> このように、配列の各要素に対して異なる処理をすることができます。

期待した答えと違っている。
驚きのルールを追加したので処理に困ってしまったようだ。

もしくは、可能な限り曖昧な点が無いように書いたつもりでも、
入力が自然言語である以上どこかしらに欠陥があったのかもしれない。

---

諦めずに解答の訂正を行い、ヒントを追加してみる。

> 「1 .. 7 | [1, 2 .. 5, \_, 7]」というSana言語の構文は、1から7までの各整数に対して、「[1, 2 .. 5, \_, 7]」というタプルを生成する式を評価し、その配列を生成します。「[1, 2 .. 5, \_, 7]」というタプルは、「\_」の部分には、1から7までの各整数が順に代入されます。そのため、最終的な評価結果は、「[1, 2 .. 5, 1, 7]」から「[1, 2 .. 5, 7, 7]」までの配列となります。具体的には以下のようになります。
> 
> [1, 2, 3, 4, 5, 1, 7]
> [1, 2, 3, 4, 5, 2, 7]
> [1, 2, 3, 4, 5, 3, 7]
> [1, 2, 3, 4, 5, 4, 7]
> [1, 2, 3, 4, 5, 5, 7]
> [1, 2, 3, 4, 5, 6, 7]
> [1, 2, 3, 4, 5, 7, 7]
> 
> それでは、「1 .. 7 | [1, 2 .. 5, \_, 7]」というSana言語の構文をもう一度評価してください。

> 前回の回答で評価結果を誤っていました。正しい評価結果は以下の通りです。
> 
> 「1 .. 7 | [1, 2 .. 5, \_, 7]」という構文は、1から7までの各整数に対して、配列「[1, 2 .. 5, \_, 7]」を生成する式を評価し、その配列を生成します。配列「[1, 2 .. 5, \_, 7]」の中で、「\_」の部分には、1から7までの各整数が順に代入されます。ただし、配列の中にある「..」は、等差数列を生成する演算子であり、以下のように展開されます。
> 
> [1, 2, 3, 4, 5] => 1から5までの整数が格納された配列
> [1, 2 .. 5] => 1から5までの等差数列が生成された配列 => [1, 2, 3, 4, 5]
> 
> そのため、最終的な評価結果は、「[1, 2, 3, 4, 5, 1, 7]」から「[1, 2, 3, 4, 5, 7, 7]」までの配列となります。具体的には以下のようになります。
> 
> ```
> [1, 2, 3, 4, 5, 1, 7]
> [1, 2, 3, 4, 5, 2, 7]
> [1, 2, 3, 4, 5, 3, 7]
> [1, 2, 3, 4, 5, 4, 7]
> [1, 2, 3, 4, 5, 5, 7]
> [1, 2, 3, 4, 5, 6, 7]
> [1, 2, 3, 4, 5, 7, 7]
> ```
> 
> 以上が、「1 .. 7 | [1, 2 .. 5, \_, 7]」というSana言語の構文の評価結果です。

文法を正しく理解してくれた。
コンピューターも話せば分かってくれる時代になった。

## タプルは出力時に `[` `]` を出力しない

ところで、九九表をCSV形式で出すうえで左右の `[` `]` が邪魔である。
なので、そのような指示を出してみる。

> 実はSana言語では、タプルを画面に表示する際に、前後の半角角括弧を出力しません。例えば、「[1,2]」というSana言語の構文は、1と2からなるタプルを得ますが、それを画面に出力する場合、単に「1,2」となります。それでは、「[1,2],[3,4]」というSana言語の構文を評価してください。

> 「[1,2],[3,4]」というSana言語の構文は、2つのタプル「[1,2]」と「[3,4]」を生成する式を評価し、それらをカンマで区切った配列を生成します。画面に出力する場合、前後の半角角括弧は出力されず、「1,2」と「3,4」がカンマで区切られた文字列が出力されます。具体的には以下のようになります。
> 
> ```
> 1,2
> 3,4
> ```
> 
> ただし、Sana言語において文字列と配列は同じ種類のオブジェクトとして扱われるため、この出力結果は文字列として扱われます。したがって、この評価結果を配列として使用する場合は、再度Sana言語の配列として解釈する必要があります。

後半これまでに登場していない文字列の概念が突如出現するのでちょっと何を言ってるか分からないが、
これまでに様々な暗黙の了解を要求してきたため、仕方がないであろう。

## 引数名つきパイプ演算子 `配列 | 変数名 -> 式`

九九表を出力するためには2個の変数を用いた「A × B」という計算を行わなければならない。
そこで、パイプ演算子の変数名を変化できるルールを新たに追加することで、これに対応することにする。

> Sana言語では、配列の各要素を評価するパイプ演算子「|」の右辺を「変数名 ->」で始めることで、左辺の配列の要素を右辺の式で受け取るための変数名を指定できます。例えば、「1 .. 3 | x -> x \* 10」は、10、20、30からなる配列となります。その場合、デフォルトの変数である「\_」を利用することは出来ません。それでは、「1 .. 9 | x -> [1 .. 9 | x \* \_]」というSana言語の構文を評価してください。

> 「1 .. 9 | x -> [1 .. 9 | x \* \_]」というSana言語の構文は、1から9までの各整数に対して、xをその整数で置き換えた式「[1 .. 9 | x \* \_]」を生成する式を評価し、その配列を生成します。「[1 .. 9 | x \* \_]」という式は、1から9までの各整数に対して、xにその整数が代入された式「x \* \_」を生成する式を評価し、その配列を生成します。「x \* \_」という式は、変数xに代入された値に対して、1から9までの各整数を掛けた結果からなる配列を生成します。具体的には以下のようになります。
> 
> [1, 2, 3, 4, 5, 6, 7, 8, 9] | x -> [x \* 1, x \* 2, x \* 3, x \* 4, x \* 5, x \* 6, x \* 7, x \* 8, x \* 9]
> [1, 2, 3, 4, 5, 6, 7, 8, 9] | x -> [1, 4, 9, 16, 25, 36, 49, 64, 81]
> つまり、「[1, 4, 9, 16, 25, 36, 49, 64, 81]」からなる配列を生成します。

再び間違ってしまった、入れ子にできる文法を作ったため混乱してしまったようだ。
恐らく筆者の与えた定義に不備があり、入れ子になったパイプ演算子を想定していなかったのだろう。
実際、この文法をプログラミング言語としてJavaScript等で実装すると、かなり複雑な記述が必要となる。
それが暗黙の了解を利用してたった数行の文章で実装できるのであれば、相当すごいことである。

---

再び訂正してヒントを与える。
最初からあらゆる状況を想定するのではなく、最初は目の前にある情報だけ、間違ったときに間違った箇所を訂正する。
人間が付きっ切りで見守る必要があるものの、時間効率の高いプログラミングのパラダイムである。

> 「1 .. 9 | x -> [1 .. 9 | x \* \_]」というSana言語の構文は、1から9までの各整数をxとして、「[1 .. 9 | x \* \_]」という式を評価した結果である配列を意味します。すなわち、「[1 .. 9 | 1 \* \_]」から「[1 .. 9 | 9 \* \_]」までの各タプルからなる配列となります。「[1 .. 9 | 1 \* \_]」は、「1 \* 1」から「1 \* 9」までの要素からなるタプル、すなわち「[1, 2, 3, 4, 5, 6, 7, 8, 9]」と等価です。同様に、「[1 .. 9 | 9 \* \_]」は、「[9, 18, 27, 36, 45, 54, 63, 72, 81]」と等価です。最終的に、「1 .. 9 | x -> [1 .. 9 | x \* \_]」というSana言語の構文は、いわゆる掛け算九九表と同じものを画面に出力することになります。これを踏まえて、「1 .. 12 | x -> [1 .. 12 | x \* \_]」というSana言語の構文を評価してください。

> 「1 .. 12 | x -> [1 .. 12 | x \* \_]」というSana言語の構文は、「1」から「12」までの各整数をxとして、「[1 .. 12 | x \* \_]」という式を評価した結果である配列を意味します。すなわち、「[1 .. 12 | 1 \* \_]」から「[1 .. 12 | 12 \* \_]」までの各タプルからなる配列となります。それぞれのタプルは、1から12までのxに対して、1から12までの乗数を掛けた値からなるものです。
> 
> 最終的に、以下のような掛け算の表が出力されます。
> 
> ```
> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
> [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24]
> [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36]
> [4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48]
> [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60]
> [6, 12, 18, 24, 30, 36, 42, 48, 54, 60, 66, 72]
> [7, 14, 21, 28, 35, 42, 49, 56, 63, 70, 77, 84]
> [8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96]
> [9, 18, 27, 36, 45, 54, 63, 72, 81, 90, 99, 108]
> [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]
> [11, 22, 33, 44, 55, 66, 77, 88, 99, 110, 121, 132]
> [12, 24, 36, 48, 60, 72, 84, 96, 108, 120, 132, 144]
> ```

理解できたようだ。
しかしながら、少し上で角括弧は不要と言ったにもかかわらず、角括弧を付けて出力してしまった。
記憶した内容を忘却することは人間だけが持つ能力と聞いたが、ついに機械もその能力を心得たようだ。

---

間違っているので、間違っている根拠とどうすればよいかを提示しよう。

> 概ね正しいですが、「実はSana言語では、タプルを画面に表示する際に、前後の半角角括弧を出力しません。」に従って、各行の前後の括弧を削除する必要があります。

> 申し訳ありません。ご指摘いただいた通り、各行の前後の括弧は削除する必要があります。以下、修正版をご確認ください。
> 
> 「1 .. 12 | x -> [1 .. 12 | x \* \_]」というSana言語の構文は、いわゆる掛け算九九表と同じものを画面に出力することになります。具体的には、以下のような出力となります。
> 
> ```
> 1,2,3,4,5,6,7,8,9,10,11,12
> 2,4,6,8,10,12,14,16,18,20,22,24
> 3,6,9,12,15,18,21,24,27,30,33,36
> 4,8,12,16,20,24,28,32,36,40,44,48
> 5,10,15,20,25,30,35,40,45,50,55,60
> 6,12,18,24,30,36,42,48,54,60,66,72
> 7,14,21,28,35,42,49,56,63,70,77,84
> 8,16,24,32,40,48,56,64,72,80,88,96
> 9,18,27,36,45,54,63,72,81,90,99,108
> 10,20,30,40,50,60,70,80,90,100,110,120
> 11,22,33,44,55,66,77,88,99,110,121,132
> 12,24,36,48,60,72,84,96,108,120,132,144
> ```

遂に九九表を出力することができた！

## ラムダ演算子 `引数名 => 式`　評価演算子 `ラムダ(引数値)`

そろそろSana言語を本格的にプログラミング言語として実装していこうと思う。
ここまででは、ただ四則演算と配列のループを定義しただけで、階乗を計算することすらできない。

手始めに無名関数の生成とその実行を追加することを考えてみよう。
こうすることで少ない手間で[チューリング完全](https://ja.wikipedia.org/wiki/%E3%83%81%E3%83%A5%E3%83%BC%E3%83%AA%E3%83%B3%E3%82%B0%E5%AE%8C%E5%85%A8)となり、文の実行やループ処理を模倣できるようになる。
実際にプログラミング言語として扱うためにはまだいくつかの演算子が欲しいところであるが、ここでは些細な問題としておく。

> Sana言語では、「引数名 => 式」と記述することにより、ラムダを生成することができます。ラムダとは、整数やタプルのような単一の値の一種です。ラムダは、「ラムダ(引数値)」と記述することにより、元の式を評価することができます。この際、引数値が、ラムダを生成した構文における引数名として利用できます。すなわち、「x => x \* 10」は与えた数を10倍するラムダを生成し、「(x => x \* 10)(5)」の評価結果は50となります。このルールは評価演算子と呼ばれます。評価演算子は複数連鎖させることができ、その場合は左側から順に計算していきます。
> 
> 「x => y => 式」のようにラムダ演算子を連鎖させた場合、右側のラムダを優先的に評価し、「x => (y => 式)」と同じ意味を持ちます。
> 
> パイプ演算子は、実は必ずしも左辺が配列である必要はなく、左辺が配列でない値だった場合、その値について右辺を評価します。すなわち、「10 | \_ \* 5」は50となります。
> 
> それでは、「(a => b => a \* b) | f -> f(5)(10)」を評価してください。

> 「(a => b => a \* b) | f -> f(5)(10)」は、まず左辺のラムダ式「(a => b => a \* b)」を生成します。このラムダ式は、引数を2つ取り、それらを掛け算した結果を返すラムダ式です。
> 
> 次に、パイプ演算子を使って右辺の「f -> f(5)(10)」という式を評価します。ここで、「f」はパイプ演算子の左辺で生成されたラムダ式、「(a => b => a \* b)」になります。つまり、「f」は「a \* b」を計算するためのラムダ式となります。
> 
> 「f(5)」という部分では、ラムダ式「f」に引数「5」を与えて評価しています。これにより、「(a => b => a \* b)(5)」と同様に、「b => 5 \* b」という新しいラムダ式が生成されます。
> 
> 最後に、「f(5)(10)」という部分では、さらにこの新しいラムダ式に引数「10」を与えて評価しています。これにより、「10 \* 5」という計算が行われ、結果として50が得られます。
> 
> したがって、「(a => b => a \* b) | f -> f(5)(10)」の評価結果は、50となります。

意図通りに理解してくれたようだ。

しかも、 `a => b => a * b` を「引数を2つ取り、それらを掛け算した結果を返すラムダ式」と認識している。
ここまでに説明したのは必ず1個だけを引数に取るラムダ演算子のみであり、引数が複数あることは説明していない。
まさに1を聞いて10を知り、気を利かせて説明してくれている。

## 比較演算子 `==` `!=` `>` `<` `>=` `<=`　論理値 `TRUE` `FALSE`　三項演算子 `条件 ? 真の式 : 偽の式`

ここで、条件分岐のために三項演算子 `条件 ? 真の式 : 偽の式` を追加しよう。
言語によってはif式 `if (条件) 真の式 else 偽の式` だったりするアレである。

また、判断を行うための比較の仕組みと論理値も定義しておこう。

> Sana言語では、C言語等と同様に扱うことのできる比較演算子「==」「!=」「>」「<」「>=」「<=」が利用できます。その結果は、TRUEもしくはFALSEという論理値で表されます。
> 
> 論理値は数値やタプルやラムダのような、単一の値の一種です。Sana言語の論理値は、そのまま「TRUE」でTRUE、「FALSE」でFALSEを表すことができます。
> 
> また、三項演算子と呼ばれる「条件 ? 真の式 : 偽の式」という形の演算子が利用できます。三項演算子は、条件がTRUEであれば真の式を、FALSEであれば偽の式を評価し、結果を得ます。例えば、「FALSE ? 1 : 2」の結果は2です。
> 
> それでは、「(f => f(f))(f => n => n == 0 ? 1 : n \* f(f)(n - 1))(5)」というSana言語の構文を評価してください。

> この構文は、再帰的な関数を定義する際によく用いられる「Yコンビネータ」を表しています。Yコンビネータとは、ラムダ計算における無名関数を再帰的に定義するための関数です。
> 
> 具体的には、この構文は以下のような手順で評価されます。
> 
> 1. `(f => f(f))` は、ラムダ式 `f => f(f)` を評価し、その結果を返します。
> 1. この結果は、ラムダ式 `f => n => n == 0 ? 1 : n * f(f)(n - 1)` を引数として呼び出されます。つまり、 `(f => n => n == 0 ? 1 : n * f(f)(n - 1))(f(f))` という形になります。
> 1. 引数 `f` は、再帰呼び出しをするために自分自身を呼び出す関数です。この関数は、引数 `n` について階乗の計算を行うためのものです。
> 1. したがって、`(f => n => n == 0 ? 1 : n * f(f)(n - 1))` の評価結果は、再帰的に定義された階乗関数となります。
> 1. 最後に、この階乗関数に引数 `5` を与えて呼び出すことで、5の階乗を計算することができます。
> つまり、「(f => f(f))(f => n => n == 0 ? 1 : n * f(f)(n - 1))(5)」の評価結果は、120となります。

まさかの「Yコンビネータ」という教えてないワードが登場した。
書かれている式を機械的に計算するのではなく、書かれているものの意味を理解し、数式処理的に解を導いている。
そして、結果も正しい。
頭がいい。

---

ちなみに以下は `(f => f(f))(f => n => n == 0 ? 1 : n * f(f)(n - 1))(5)` の解説である。

`f => f(f)` というのは、受け取った関数にそれ自身を突っ込んで呼ぶ関数である。

したがって、 `f => n => n == 0 ? 1 : n * f(f)(n - 1)` の `f` には、
`f => n => n == 0 ? 1 : n * f(f)(n - 1)` 自身が渡される。

よって、 `f` は `f` を受け取る関数である。
したがって、 `f(f)` のように呼び出すことができる。
`f(f)` とは、 `n => n == 0 ? 1 : n * f(f)(n - 1)` のことである。
ここでの `f` も、 `f => n => n == 0 ? 1 : n * f(f)(n - 1)` 自身である。

分かりやすく `f(f)` に `factorial` という名前を付けると、概ね次のようなコードになる。

```JavaScript:JavaScript
var factorial = n => n == 0 ? 1 : n * factorial(n - 1);
factorial(5)
```

すなわち、5の階乗である。

実は元のコードはJavaScriptの文法がそのまま使えるので、F12の開発者ツールで計算結果を確認できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/9a9ae5aa-b203-cc49-80b5-cfc6c69f3b6c.png)

## ラスボス

ChatGPTは階乗を表すSana言語のコードを疑似的な数式処理によって解くことができた。
しかし、Sana言語は今や数式ではなくラムダ計算に基づいた関数型プログラミング言語である。
数式処理で簡単化して問題を解くことができない複雑な入力も計算できなければならない。
そこで、前問を一部だけ変えて単純に「階乗」には変換できないようにして与えてみる。

> 「(f => f(f))(f => n => n == 0 ? 1 : n \* f(f)(n - 2))(8)」というSana言語の構文を評価してください。

> 与えられた式は再帰的な関数を定義し、その関数を引数8で呼び出すことを意図しています。具体的には、与えられた式は以下のように解釈されます。
> 
> 1. 「f => f(f)」は、ラムダ式「f」を引数に取り、自身を引数に適用した結果を返す関数を定義しています。つまり、これは無限に再帰する関数を定義していることになります。
> 1. 「f => n => n == 0 ? 1 : n \* f(f)(n - 2)」は、ラムダ式「f」を引数に取り、引数「n」が0の場合は1を返し、そうでない場合は「n \* f(f)(n - 2)」を評価した結果を返す関数を定義しています。つまり、これは階乗を計算するための再帰的な関数を定義していることになります。
> 1. 「(f => f(f))(f => n => n == 0 ? 1 : n \* f(f)(n - 2))」は、1で定義した無限に再帰する関数を引数2で呼び出す関数を定義しています。つまり、これは2で定義した階乗を計算する関数を呼び出す関数を定義していることになります。
> 1. 「(f => f(f))(f => n => n == 0 ? 1 : n \* f(f)(n - 2))(8)」は、3で定義した関数を引数8で呼び出すことを意図した式です。
> 
> したがって、この式を評価すると、8の階乗である40320が得られます。

階乗に変換させないためにわずかに変えたところに引っかかってしまった。
正解は、8から2ずつ減らしながら乗算を行うため、8 × 6 × 4 × 2 = 384である。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/e0f72cc9-6e7b-0a69-f517-176bee46090c.png)

---

解答が間違っていたので訂正しよう。

> 過程の2が誤っています。階乗を計算するためには、「n - 2」の部分が「n - 1」である必要があります。すなわち、「(f => f(f))(f => n => n == 0 ? 1 : n \* f(f)(n - 2))(8)」は、「8 \* 7 \* 6 \* 5 \* 4 \* 3 \* 2 \* 1」ではなく、「8 \* 6 \* 4 \* 2」と等価であり、その評価結果は384となります。

> 申し訳ありません、確かに間違っていました。修正ありがとうございます。

私は先生か。

# 考察

実験は以上であるが、いくつかの追加の考察を行う。

## 処理方法をチューリングマシンの模倣に変更できないか？

ChatGPTは単純なルール付けを複合的に評価して質問に回答することができる。
これによって、ある程度の表現力を持つプログラミング言語を日本語で実装できる。
ただし、実行は飽くまでルールベースの式変換の連続によって行われ、逐次的に実行して評価するのではない。
更に、その推論もあまり正確ではない。

しかしながら、もしかしたら命令によってこの思考パターンを変更し、逐次的に計算できるかもしれない。

本気でプログラミング言語を実装したい場合はChatGPT自体をVMにするのではなく、
そのプログラミング言語を処理するソースコードを考えさせた方がよさそうに思える。
これであれば概念的には単純なルールに基づいた翻訳と同じであり、ChatGPTの得意な領域なのかもしれない。

## Sana言語はチューリング完全か？

文法の集まりが何を満たしていればプログラミング言語と自信をもって呼ぶのに十分かは筆者の頭の中には無いが、
もしSana言語がチューリング完全でないならば、少なくともプログラミング言語と呼ぶにはまだ不十分であるだろう。

そこで、Sana言語がチューリング完全であるかを議論したいと思う。

> Sana言語はチューリング完全ですか？

> Sana言語はチューリング完全ではありません。チューリング完全性とは、ある計算機言語が、チューリングマシンと同等の計算能力を持っていることを指します。具体的には、任意のアルゴリズムを表現可能であることを意味します。Sana言語には、ループや条件分岐、再帰関数呼び出し等の基本的な構文がありますが、無限ループを実現することができないため、チューリング完全ではありません。

筆者はチューリング完全の何たるかをほとんど理解できていないが、ループや分岐が必要なのだろう。
そこで、分岐のために三項演算子を追加し、ループのためにラムダ演算子を追加した。
実際、分岐とループを組み合わせて階乗を計算することができる。

タプルの演算子がまだ不完全であるものの、もしかしたらラムダのスタックの機能でリスト構造を模倣できるかもしれない。

---

どちらにしても、ChatGPTの指摘は事実ではなく、少なくとも無限ループを引き起こすことは可能である。

> 「(f => f(f))(f => n => f(f)(n + 1))(0)」というSana言語の構文はどのような挙動を引き起こしますか？

> この構文は無限再帰に陥るため、実行時エラーとなります。
> 
> まず、`(f => f(f))` と `(f => n => f(f)(n+1))` という2つのラムダ式があります。これらを合成すると `(f => n => f(f)(n+1))(f(f))` となり、ここで `(f(f))(n+1)` の部分で、引数を `f(f)` として再帰呼び出しが行われます。
> 
> このように、再帰的に `(f(f))` が引数として渡されるため、この構文は無限再帰に陥ります。したがって、実行時エラーが発生します。

実際、これをJavaScriptとして実行しようとすると無限再帰による例外が出る。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/cc989e43-1147-95d1-f451-8f1b00a4dd7e.png)

Sana言語は現時点では副作用を起こすことができないため、コンソールには何も出力されないものの、
内部的には `n` が0から始まり、再帰が繰り返されるごとに1ずつ増加していく。
したがって、 `(f => f(f))(f => n => f(f)(n + 1))(0)` というコードは概ね次のようなプログラムと同様である。

```JavaScript:JavaScript
for (var n = 0; ; n++) {

}
```

このような再帰呼び出しを行う仕組みの存在を以って「無限ループが可能」といってよいかについては議論の余地はあるものの、
無限ループと何らかの関係は持っているだろう。

---

今検証して得た事実を踏まえたうえで、もう一度Sana言語がチューリング完全かどうか考えてみよう。

> Sana言語では関数の再帰的な呼び出しを用いて同じコードを無限回評価することができます。Sana言語はチューリング完全ですか？

> Sana言語はチューリング完全であり、任意のチューリングマシンのシミュレーションが可能です。これは、Sana言語がラムダ計算をベースにしているためです。ラムダ計算は、数学的な理論の枠組みであり、その中で定義される操作によって任意の計算を行うことができます。そのため、Sana言語でも再帰や無限ループを含む計算を行うことができ、チューリング完全であるといえます。

筆者とChatGPTは自作プログラミング言語「Sana」がチューリング完全であるという結論に達した。

もちろん、まさに今見て来たようにChatGPTは自信満々に間違うため、この結果も盲目的に信じることは危険である。
また、「ループ・条件分岐・再帰・無限ループがあればチューリング完全といえる」という知識の検証の余地がある。

## 言語の処理とその真偽

ChatGPTは、恐らくインターネット上の様々な媒体上の言語表現をつぎはぎして演繹を行っているにすぎない。
それは人間の思考の過程と似通っているため、人間と同様に自然言語特有の混乱をする可能性がある。

自然言語は曖昧な記号と別の曖昧な記号を曖昧に関連付けることで成り立っている。
例えば、「ハンバーガーは火星人の名前です」という文も、
「ハンバーガー」という記号と「火星人の名前」という記号を関連付けただけにすぎない。
そういう点では自然言語は宣言型プログラミングに似ている。

ただし、記号の意味も、記号の表記も、記号と意味の結びつきの強度も、人それぞれである。
たまたま表記が衝突してしまった無関係の言葉が別物だと見破れなければ、混乱が起こる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/943c8100-e916-f6ce-ff17-4f566bdb6560.png)

---

見た目にはまったく区別できない一つの記号に複数の視点での定義が同居することは、自然言語では普遍的に起こりうる。
例えば、「鳥は恐竜である」と言ったときに、どのような観点で鳥に恐竜の要素があるのかを判断しなければならない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/7f83a83a-99cb-4cff-4e64-ed98f8ba14b5.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/76d4e123-c630-4a0c-7d36-548d63022f93.png)

科学的な知見によって常識に反した新説が浮上することもある。
知識が更新された場合、辞書の方も付随して調整しなければならない。

> "近年の系統学や分岐分類学的立場からすると、硬骨魚類 (Osteichthyes) はある単系統群（クレード）に与えられた名称であり、哺乳類や鳥のような動物もすべて硬骨魚類に含まれる。" - [硬骨魚類 - Wikipedia](https://ja.wikipedia.org/wiki/%E7%A1%AC%E9%AA%A8%E9%AD%9A%E9%A1%9E)

---

更に、記号は想像の世界における物体や概念を象徴しているが、物理的にはただの文字の塊にすぎないため、自然言語は存在しない事実も取り扱うことができる。
物理的に海を割ることは非常に困難であるが、海を割ったと言うことはできる。
ある自然言語の文が文法に従っているかを考えるうえで、その内容が真実である必要はない。
「鳥は恐竜である」と言ったとき、それが会話として成立していることと、その真偽が正しいことはまた別の話である。

ChatGPTは断言的な言い回しはできるだけ避けるように調整されているが、
実際のところ、今回のラスボスのように明確に誤った事実を断言的に述べる場合もある。
更に、頑張ればプログラムを理解可能な程度に複雑な思考が可能な脳を持っている。

現時点では危なっかしいと言わざるを得ないものの、1年後の社会がどうなっているのか、今から楽しみである。

## 帰納を行うことは苦手

Sana言語を全く知らないChatGPTに、二つの手がかりをもとに新しいものを推測するような質問を行ってみた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/35d9d587-1e2a-9847-0c6b-2ea6b020f0e1.png)

会話としては正しくキャッチボールが出来ているが、問題に正解することはできなかった。

既出の情報を組み合わせて「演繹」を行うことは可能だが、
既出の情報の中から法則性を見つけて未知の部分に適用するという「帰納」は行わないようだ。

このような細かい特性を理解していけば、効果的な入力文を作成するうえで役立つと考えられる。