SATySFiの「最短コード」

>   これは「[SATySFi Advent Calendar 2020](https://adventar.org/calendars/4965)」の15日目の記事です。
>   （14日目は [bd_gfngfn さん](https://github.com/gfngfn/SATySFi/wiki/%E3%83%9E%E3%82%AF%E3%83%AD%E3%81%A7%E3%82%B3%E3%83%BC%E3%83%89%E4%B8%AD%E3%81%AE%E4%BD%8D%E7%BD%AE%E3%82%92%E5%8F%96%E5%BE%97%E3%83%BB%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B%E6%A9%9F%E8%83%BD)、16日目は [abenori さん](https://abenori.blogspot.com/2020/12/satysfi_16.html)です。）

プログラミング言語に関するニッチな話題の1つに**「その言語の正当なソースコードで最短のものは何か？」**というものがあります。この記事では**[SATySFi][impl]について「最短コード」を考察**します。

## 前提

SATySFiの「言語の仕様」は明文化されておらず、また現在はSATySFiの処理系の実装は1つしかないため、「実装が仕様」と見なすのが妥当でしょう。従って、ここでは「SATySFIの正当なソースコード」の定義を**「SATySFiの現時点での最新リリース版である0.0.5版でコンパイルした場合にエラーが起こらずにPDFが出力されるソースコード」**と定めることとし、またソースコードの長さをバイト数で測ることにします。

[impl]: https://github.com/gfngfn/SATySFi

### 注意

  * PDF出力モードについて考えます。
  * 非ASCII文字は結局使わないのでバイト数でも文字数でも同じです。
  * 最短コードを構成する際には必須でない改行や空白文字を除去することになりますが、「除去後」のコードは非常に読みづらくなります。本記事では「除去前」と「除去後」のコードの両方を掲載した上で「除去前」のコードを見ながら解説します。「短さ」を考慮する際には常に「除去後」を前提にしていることに注意してください。

## 標準ライブラリの使用を認める場合

普通にSATySFiを文書作成に利用する場合は“標準ライブラリ”（mathやstdjaなどの、処理系に付随するパッケージ群）の使用が前提となります。そこでまずは、同じ前提で「最短コード」を考えてみましょう。

コンパイルが通るソースコードを書くためには**「document型の値」**を作り出す必要があります。実は、標準ライブラリの中に「document型の値を簡単に作る」ための**[standalone][]**というパッケージが存在します。これを利用するのが最も簡単であり、恐らくこれが最短にもなると思われます。

[standalone]: https://github.com/gfngfn/SATySFi/blob/master/lib-satysfi/dist/packages/standalone.satyh

```text:source1.saty
% 標準ライブラリのstandaloneパッケージを読み込む
@require: standalone
% 以下の型をもつstandalone函数が提供される
%   standalone : block-text -> document
standalone '<> % 引数に空のblock-textを渡すとdocumentが得られる
```

このコードをコンパイルすると、A4サイズの白紙のPDFが得られます。

「除去後」のコードは次の通り[^lf]で、**33バイト**になります。

[^lf]: 「除去後」のコードにおいては改行文字はLF（1バイト）で表すものとし、またファイルの末尾に改行文字はないものとします。

```text:minimal1.saty(33バイト)
@require:standalone
standalone'<>
```

## 標準ライブラリを使用しない場合

今度は、標準ライブラリも一切使わないこととしてSATySFiのプリミティヴのみを使った場合の「最短コード」を考えてみます。

standaloneパッケージの[実装][standalone]を参照してsource1.satyのコードをプリミティヴのみを使って書き直すと以下のようになります。

```text:source2a.saty
let-inline ctx \math m =
  embed-math ctx m
in
page-break A4Paper
  (fun _ -> (|
    text-origin = (80pt, 100pt);
    text-height = 630pt;
  |))
  (fun _ -> (|
    header-origin  = (0pt, 0pt);
    header-content = block-nil;
    footer-origin  = (0pt, 0pt);
    footer-content = block-nil;
  |))
  (read-block
    (get-initial-context 440pt (command \math)
      |> set-dominant-narrow-script Latin
      |> set-dominant-wide-script Kana)
    '<>)
```

SATySFiのプリミティヴでdocument型の値を作り出す唯一の方法が「`page-break`函数を適用する」ことです。従って、`page-break`の使用は必須であり、あとは「いかにして`page-break`の引数をsource2a.satyより短く書けるか」が問題の核心になります。

  * `page-break`の第1引数の型はpaper型である。paper型のコンストラクタの名前はどれも7バイト以上あるため、`A4Paper`は「最短」になっている。
  * 第2・第3引数の型はともに「特定のレコード型の値を別の特定のレコード型の値に変換する函数」である。source2a.satyは定数のレコードリテラルを返す函数を渡している。当該の型の値を返すプリミティヴ函数はないので、結局同様の方針をとるしかなく、あとはレコードの中の基本型の値を「短く」するしかなさそう。
      * length値で一番「短い」のは`1cm`などの3バイトの即値である。なのでlength値を全て`1cm`に置き換える。
      * block-boxes値は恐らく`block-nil`（9バイト）が「最短」であると思われる。
  * 第4引数の型はblock-boxes型である。source2a.satyでは<br>`read-block ... '<>`<br>となっているが、元のblock-textが空なので、この式の値は「空のblock-boxes」、つまり`block-nil`と等価になる。先述の通り、block-boxesの「最短」は`block-nil`なのでこれに書き換えればよい。
      * `\math`コマンドの定義は不要になるため削除する。

書き換えた後のコードは以下の通りで、実際にこのコードもコンパイルが通ります（出力はやはり白紙）。

```text:source2b.saty
page-break A4Paper
  (fun _ -> (|
    text-origin = (1cm, 1cm);
    text-height = 1cm;
  |))
  (fun _ -> (|
    header-origin  = (1cm, 1cm);
    header-content = block-nil;
    footer-origin  = (1cm, 1cm);
    footer-content = block-nil;
  |))
  block-nil
```

この時点での「除去後」のコードは**185バイト**です。

```text:minimal2b.saty(185バイト)
page-break A4Paper(fun_->(|text-origin=(1cm,1cm);text-height=1cm|))(fun_->(|header-origin=(1cm,1cm);header-content=block-nil;footer-origin=(1cm,1cm);footer-content=block-nil|))block-nil
```

さらに細かい「最適化」をしていきます。コード中に`1cm`と`block-nil`が複数回現れるので、これを1文字の変数に置き換えます。

```text:source2c.saty
let (x, y) = (1cm, block-nil)
in
page-break A4Paper
  (fun _ -> (|
    text-origin = (x, x);
    text-height = x;
  |))
  (fun _ -> (|
    header-origin  = (x, x);
    header-content = y;
    footer-origin  = (x, x);
    footer-content = y;
  |))
  y
```

```text:minimal2c.saty(174バイト)
let(x,y)=(1cm,block-nil)in page-break A4Paper(fun_->(|text-origin=(x,x);text-height=x|))(fun_->(|header-origin=(x,x);header-content=y;footer-origin=(x,x);footer-content=y|))y
```

※`x`と`y`の2変数を定義する際に<br>`let x=1cm let y=block-nil in ...`<br>よりも<br>`let(x,y)=(1cm,block-nil)in ...`<br>の方が短くなることに注意。

このコードで`(x, x)`が3回現れています。いろいろ試してみましたが、以下のようにするのが「最短」のようです。

```text:source2.saty
let (x, y) = (1cm, block-nil)
let z = (x, x)
in
page-break A4Paper
  (fun _ -> (|
    text-origin = z;
    text-height = x;
  |))
  (fun _ -> (|
    header-origin  = z;
    header-content = y;
    footer-origin  = z;
    footer-content = y;
  |))
  y
```

```text:minimal2.saty(173バイト)
let(x,y)=(1cm,block-nil)let z=(x,x)in page-break A4Paper(fun_->(|text-origin=z;text-height=x|))(fun_->(|header-origin=z;header-content=y;footer-origin=z;footer-content=y|))y
```

というわけで、現状の「最短記録」は**173バイト**となりました。

## まとめ

**今日も一日サティスファイ！**:smiley:（まとめろ）