自作言語でWebサーバーを作るまで【Pangaea】

「[言語実装 Advent Calendar 2023](https://qiita.com/advent-calendar/2023/lang_dev)」の枠が空いていたので参加させていただきます。本記事は13日目の記事です。

# TL; DR

- ワンライナーに最適化
- 通信機能はホスト言語(Go)のAPIサーバーを使用（[Echo](https://echo.labstack.com/)）
- 利用者向け関数とラッパー関数をモジュール分けしてカプセル化

```bash
# 1行でAPIサーバーを立てる！
pangaea -e 'invite!("http");S.serve(S.get("/"){{"msg":"Hello"}})'
```

```bash
$ curl localhost:8080
{"msg": "Hello"}
```

# はじめに

2020年から「Pangaea」というプログラミング言語を自作しています。コンセプトは「P言語(Perl, Python, PHP, Ruby)の2次創作」[^p]で、特にワンライナー特化のスクリプト言語です。

https://github.com/Syuparn/Pangaea

言語の詳細については[過去の記事](https://qiita.com/tags/pangaea)をご覧ください。

Pangaeaはテキスト処理のための言語として開発しました。しかし、**P言語のパロディを目指すなら避けて通れない目標があります**。

そう、**Webサーバー**です。

というわけで、PangaeaにWebサーバー機能を実現するまでにやったことをまとめました。

# HTTP通信
PangaeaはGo言語で実装しているので、GoのWebフレームワーク「Echo」を使用しています。

https://echo.labstack.com/

ハンドラオブジェクトもサーバオブジェクトも、実態はEchoの構造体のラッパーです。

（実装は長いので割愛）

https://github.com/Syuparn/Pangaea/blob/master/props/modules/http/builtin/handler_object.go#L80C1-L140C2

# インターフェース

Pangaeaが重視するのは「ワンライナーの書きやすさ」です。そこで、ユースケースとして

- モックサーバーが1行で立てられる

ことを目標にしました。

```bash
pangaea -e 'invite!("http");S.serve(S.get("/"){{"msg":"Hello"}})'
```

```bash
$ curl localhost:8080
{"msg": "Hello"}
```

そのため、ある程度汎用性を捨てて以下の仕様にしています。

- APIサーバーの利用を想定
- JSONリクエスト、レスポンスに最適化
- レスポンスは暗黙的にレスポンスオブジェクトへ変換
    - 明示的に指定も可能（例: `Response.new(status: 404, body: {msg: "User not found"}.S)`）
- サーバーの設定はlistenするURL(port)のみ

また、`Server` を `S` 、 `Client` を `C` と書ける等の細かい補助もあります。

```pangaea:ワンライナーを省略せずに書くと...
invite!("http")

# サーバー起動
Server.serve(
    # 引数にハンドラを指定
    # ハンドラはHTTPメソッドと同名のメソッドで作成
    Server.get("/"){|req|
        # レスポンスを返却
        return {"msg":"Hello"}
    },
)
```

ハンドラではパスパラメータやリクエストボディも扱えます。このあたりの機能は完全にEchoに乗っかっています。

```pangaea
S.get("users/:id") {|req|
    # ハンドラのパスで指定したパラメータを req.params.{パラメータ名} で取得可能
    users.find {.id == req.params.id} || Response.new(status: 404, body: {msg: "User not found"}.S)
}
```

# モジュール設計
ここまでの実装をまとめると以下のようになります。

- HTTP処理のオブジェクトはシンプルなEchoのラッパー
  - 低レイヤーの組み込み関数
- インターフェースはユーザーが使いやすいメソッド形式
  - 高レイヤーのネイティブオブジェクト

この2つは役割が異なるため、モジュール自体を階層構造にしました。

- `http`
  - Pangaeaで実装
  - ユーザーが直接利用するため抽象化
- `http/internal`
  - Goで実装
  - `http` 内部でのみ利用

分けたことによって、`http` モジュールはPangaea実装のみになり、オブジェクトとしていかに処理を抽象化するかに専念することができました。

https://github.com/Syuparn/Pangaea/blob/master/native/modules/http.pangaea

# 課題
## インメモリで状態管理しづらい（ほぼ無理）
Pangaeaではすべてのオブジェクトがimmutableです[^immutable]。また、状態変化を表現できるような構文も用意していません。

そのため、CRUDをインメモリDBで実現するのが困難です[^db]。

```bash
$ curl localhost:8081/users
{"users": []}
# ユーザーを新規作成したら...
$ curl -XPOST localhost:8081/users -d '{"name": "Taro"}'
{"id": "user_1", "name": "Taro"}
# 当然取得結果が変わる。でもどうやって表現する？
$ curl localhost:8081/users
{"users": [{"id": "user_1", "name": "Taro"}]}
```

ま、まあ、[The Twelve Factor App](https://12factor.net/ja/) でも**Webアプリはステートレスにしましょう**と言っていますし。
HTTPクライアントもあるので外部のDBにリクエストを投げればいいだけですし。
モックサーバーとして建てるならレスポンスは固定値でも問題ないですし...

...はい、完全に**表現力不足**です。すみません。

### どうすべきか
解決策は1つではないと思いますが、再代入の設計を変えるのが一番手っ取り早いように見えます。
スコープの内側から外側の変数を再代入できるようになれば[^reassign]、以下のようにシンプルに実装できます。
（※現状はシャドーイングされてしまうためスコープの外側には干渉できない）

```pangaea:イメージ
# ※実際にはできない
users := {}

S.serve(
  S.post("users") {|req|
    user := {id: idGen.next, name: req.body.decJSON.name}
    # usersに再代入(※実際のPangaeaではできない)
    users = {user.id.S: user, **users}
    Response.new(status: 201, body: user.S)
  },
  S.get("users") {|req|
    # 参照するだけ
    {users: users.values}
  },
  // ...
}
```

思えば、オブジェクトがimmutableであることと再代入に縛りがあることは関係ありませんでした。
`=` の記号が衝突してしまっているため実装を後回しにしていたツケが回ってきました...

### 実は可能だった：インメモリで状態管理する方法（黒魔術）
最後におまけです。一応、現状でもインメモリで状態管理は可能でした。

~~ここから先の内容は「自作言語の仕様の穴を突く」というあまりにも誰得な内容です。物見遊山で覗いてみたい方だけご覧ください。~~

<details><summary>全体のソースコード</summary><div>

```pangaea
invite!("http")

idGen := <{|i| yield "user_#{i}"; recur(i+1)}>.new(1)

db := <{|users|
  set := {|updated| recur(updated)}
  yield [users, set]
}>.new({})

S.serve(
  S.post("users") {|req|
    db.next.{|users, set|
      user := {id: idGen.next, name: req.body.decJSON.name}
      set({user.id.S: user, **users})
      Response.new(status: 201, body: user.S)
    }
  },
  S.get("users") {|req|
    db.next.{|users, _set|
      {users: users.values}
    }
  },
  S.delete("users/:id") {|req|
    db.next.{|users, set|
      set(users.exclude {|k, v| k == req.params.id}.O)
    }
    Response.new(status: 204)
  },
  url: ":8081",
)
```
</div></details>

### やっていること

はじめに、Iteratorを作成します。
IteratorはPangaea唯一の状態変化するオブジェクトで、`yield` で指定した値を `Iter#next` で返します[^iterator]。

```pangaea
# iteratorは関数の一種だが、 `recur` で次回呼び出された際の引数を指定できる
it := <{|i|
  # nextの戻り値
  yield i
  # 次に呼ばれた際の引数
  recur(i + 1)
}>.new(1) # newで初期値設定

it.next # 1
it.next # 2
it.next # 3
```

`yield` の実体は `return` の仲間で、「戻り値を設定するが呼び出し元に戻らない」という命令です。
戻り値を設定した後に `recur` さえできれば、状態を更新することが可能です。

```pangaea:イメージ
it := <{|i|
  return i
}>.new(1)

it.next
# ※実際にはスコープ外で参照できないのでエラー
recur(9999)
```

そこで、`recur` を呼び出す関数ごと `yield` することで、好きな値に更新が可能になります。

```pangaea
it := <{|i|
  # recurを呼び出し、次回呼び出し時のiをupdatedに更新する関数
  # NOTE: recurは単なる関数（iterator呼び出し時に自動で定義される）なので、レキシカルスコープで参照可能
  set := {|updated| recur(updated)}

  # set関数ごと返す
  return [i, set]
}>.new(1)

ret := it.next # [1, set]
ret[0].p # 1
ret[1](9999)

ret := it.next
ret[0].p # 9999
```

これをハンドラ内で行っているのが上記のソースコードです。

ちなみに、PangaeaのIteratorはJS, Python等のIteratorとかなり構文が異なります（処理の一時停止ではなく、引数が変化する関数呼び出しとして表現）が、このような実装にしたのは以下の2点が理由です。

- 関数の仕組みを使いまわせる
- for文を実装せずにすむ

# おわりに

以上、Pangaeaのwebサーバー機能の紹介でした。
念願の「オレオレ言語でWebアプリ作成」を達成しましたが、同時に過去の設計方針による言語の限界も見えてきました。
すぐ直すかは決めていませんが、他にも同じ原因で詰まることがあれば改良していきたいです。

[^p]: といいつつ、JavaScript, Elixir, Io等からも大きな影響を受けています。
[^immutable]: ~~理由は、単に私がオブジェクトの状態変化を追うのが苦手だからです。~~ また、厳密にはIteratorのみ状態が変化します（後述）
[^db]: immutableであること自体は問題ではありません。
[^reassign]: Goの `=`, Pythonの`global`, `nonlocal` のイメージです。
[^iterator]: 詳細はドキュメント参照: https://github.com/Syuparn/Pangaea/blob/master/docs/reference/iterator.md#iterator