TypeScriptで型レベルScheme

2024/01/02

実家近くから撮影した初日の入りです。日の出はタイミングを逃してうまく撮れませんでした。

TypeScriptの型システムでSchemeインタプリタを作りました。

できること

趣味のために作ったので実用性は全く考えていませんが、type-challengesをいくつか解くくらいはできます。

Reverse(難易度:Medium)

import type { Scheme } from "ts-type-level-scheme";

type Reverse<array extends unknown[]> = [
  "begin",

  [
    "define",
    ["reverse", "rest", "result"],

    [
      "if",
      ["null?", "rest"],
      "result",
      ["reverse", ["cdr", "rest"], ["cons", ["car", "rest"], "result"]]
    ]
  ],

  ["reverse", ["quote", array], ["quote", []]]
];

type res = Scheme<Reverse<[1, "a", true, null, undefined]>>;
//   ^? type res = [undefined, null, true, "a", 1]

Slice(難易度:Extreme)

import type { Scheme } from "ts-type-level-scheme";

type Slice<
  array extends unknown[],
  start extends number,
  end extends number
> = [
  "begin",

  [
    "define",
    ["slice", "array", "start", "end"],

    [
      "define",
      ["iter", "rest", "result", "idx"],

      [
        "if",
        ["null?", "rest"],
        "result",
        [
          "if",
          ["and", ["<=", "start", "idx"], ["<", "idx", "end"]],
          [
            "iter",
            ["cdr", "rest"],
            ["append", "result", ["list", ["car", "rest"]]],
            ["+", "idx", 1]
          ],
          ["iter", ["cdr", "rest"], "result", ["+", "idx", 1]]
        ]
      ]
    ],

    ["iter", "array", ["quote", []], 0]
  ],

  ["slice", ["quote", array], start, end]
];

type res = Scheme<Slice<[1, 2, 3, 4, 5], 2, 4>>;
//   ^? type res = [3, 4]

Sort(難易度:Extreme)

挿入ソートです。

import type { Scheme } from "ts-type-level-scheme";

type Sort<array extends unknown[], desc extends boolean = false> = [
  "begin",

  [
    "define",
    ["sort", "array", "desc"],

    [
      "define",
      ["insert", "array", "val"],

      [
        "define",
        ["iter", "rest", "result"],

        [
          "cond",
          [["null?", "rest"], ["append", "result", ["list", "val"]]],
          [
            [["if", "desc", ">", "<"], "val", ["car", "rest"]],
            ["append", ["append", "result", ["list", "val"]], "rest"]
          ],
          [
            "else",
            [
              "iter",
              ["cdr", "rest"],
              ["append", "result", ["list", ["car", "rest"]]]
            ]
          ]
        ]
      ],

      ["iter", "array", ["quote", []]]
    ],

    [
      "define",
      ["iter", "rest", "result"],

      [
        "if",
        ["null?", "rest"],
        "result",
        ["iter", ["cdr", "rest"], ["insert", "result", ["car", "rest"]]]
      ]
    ],

    ["iter", "array", ["quote", []]]
  ],

  ["sort", ["quote", array], desc]
];

type res1 = Scheme<Sort<[]>>;
//   ^? type res1 = []

type res2 = Scheme<Sort<[1]>>;
//   ^? type res2 = [1]

type res3 = Scheme<Sort<[2, 4, 7, 6, 6, 6, 5, 8, 9]>>;
//   ^? type res3 = [2, 4, 5, 6, 6, 6, 7, 8, 9]

type res4 = Scheme<Sort<[3, 2, 1], true>>;
//   ^? type res4 = [3, 2, 1]

type res5 = Scheme<Sort<[3, 2, 0, 1, 0, 0, 0], true>>;
//   ^? type res5 = [3, 2, 1, 0, 0, 0, 0]

このように

複雑な型演算であってもSchemeでお手軽に実現できます。

実行時エラー

コードがおかしい場合はそれなりにエラーが返ります。

import type { Scheme } from "ts-type-level-scheme";

type error1 = Scheme<"foo">;
//   ^? type error1 = "Unbound variable: foo"

type error2 = Scheme<["begin", ["define", "a", 1], ["a"]]>;
//   ^? type error2 = "Apply: unknown procedure type"

type error3 = Scheme<undefined>;
//   ^? type error3 = "Eval: unknown expression type"

type error4 = Scheme<["cond"]>;
//   ^? type error4 = "CondToIf: clauses are required"

あくまでそれなりなので、変なエラーや型が返る場合もあります。しょうがないね。

良かった点

まず、自動フォーマットが良かったです。先程のコードはすべてPrettierがフォーマットした結果だったのですが、Schemeのコードとして見てもわりと読みやすいと思います。そのため、コーディングの体験は意外に良かったです。シンボルをいちいちダブルクォートして,を打つのは面倒でしたが。

次に、レキシカルスコープによるブロック構造のありがたみを(改めて)実感しました。ローカルな手続きをブロック内に定義できるのは、素の型パズルと比べるとだいぶ体験が良かったです。

制限

以下の特殊形式のみ使用できます。

  • quote
  • define
  • if
  • lambda
  • cond
  • begin

特に、set!set-car!set-cdr!といった、代入を行う特殊形式はサポートしていません。実装が大変そうだったのと、あまり必要になる感じがしなかったからです。

数値は、0を含む自然数のみ使用できます。TypeScriptの型で素数を求めたいで作ったやつを組み込みました。TypeScriptの型で全加算器から浮動小数点数,そして√2では、負の数や小数も作りましたが、そこまでは必要ないだろうと思って組み込みませんでした。

以下の基本手続きのみ使用できます。

  • +
  • -
  • *
  • /
  • remainder
  • =
  • <
  • <=
  • >
  • >=
  • and
  • or
  • null?
  • cons
  • car
  • cdr
  • list
  • append

TS2589(Type instantiation is excessively deep and possibly infinite.)により、あまり重い計算はできません。

どのくらい重い計算ができるのかやってみます

フィボナッチ数を計算してみます。

import type { Scheme } from "ts-type-level-scheme";

type Fib<n extends number> = [
  "begin",

  [
    "define",
    ["fib", "n"],

    [
      "define",
      ["iter", "a", "b", "cnt"],

      [
        "if",
        ["=", "cnt", 0],
        "b",
        ["iter", ["+", "a", "b"], "a", ["-", "cnt", 1]]
      ]
    ],

    ["iter", 1, 0, "n"]
  ],

  ["fib", n]
];

type fib_19 = Scheme<Fib<19>>;
//   ^? type fib_19 = 4181

19番目のフィボナッチ数までは求められますが、20番目を求めようとするとエラーになります。

カンですが、主に内部の数値表現がネックになっているっぽいので、2進表現や10進表現を実装すればもっと計算できるようになると思います。カンですが。

実装

SICP(計算機プログラムの構造と解釈)のやつを愚直に実装しただけなので、あまり書くことがありません。

それでも工夫が必要だったのは、環境の参照を実現する部分でした。例えば、クロージャは外側の環境への参照を保持する必要があります。しかし、TSの型システムには型への参照というものがありません(そらそう)。

したがって、すべての環境を配列として保持し、その添字を参照として扱うことにしました。この配列をevalとapplyで持ち回り、必要なときに参照することにしました。

これは代入操作を実装しなかった理由にも関係します。例えばset-cdr!を導入してしまうと、これを以下のように使うことで循環リストを作れてしまいます。

(define
  (last-pair x)
  (if (null? (cdr x))
    x
    (last-pair (cdr x))
  )
)

(define
  (make-cycle x)
  (set-cdr! (last-pair x) x)
  x
)

出典:SICP(計算機プログラムの構造と解釈)、練習問題3.12および3.13

しかし前述の通り、内部的に参照を表現するのはちょっと面倒です。おそらくペアを保存するメモリみたいなものを自前で作ってeval、applyで持ち回ればいけると思うのですがヤバいです。

まぁ代入なんて無くてもプログラミングできるよねってことでサボりました。

感想

2023年12月31日16時くらいからやんわりと書き始め、2024年1月1日15時くらいに完成しました。evalを書きながら年を越したのは初めての体験でしたが、それなりにコードが動いて満足です。

毎度のことですが、TS2589の限界を突破したりエラーを抑制するために、kgtkrさんのハックsusisuさんのハックを使わせていただきました。また、ビルドの設定周りでsusisu/typefuckを参考にさせていただきました。なんか記事のタイトルまで似てしまいましたが、これは偶然です。

あまり脳を使わずに書いたのでたぶんバグがあります。見つけたらissueとかで教えてもらえると嬉しいです。

ありがとうございました。

続けて読む…

TypeScriptの型で素数を求めたい

2021/01/19

よわよわエンジニアがTAPL(型システム入門)を読んだら

2022/05/02

TypeScriptにおける配列の共変性

2022/12/15

SICPの感想文

2023/07/29

TypeScriptにおける代入可能関係の推移性

2023/08/10

Idrisでふんわり眺める依存型

2022/07/04

書いた人

sititou70のアイコン画像
sititou70

都内の社会人エンジニア3年生。Web技術、3DCG、映像制作が好き。