これは「SATySFi Advent Calendar 2020」の15日目の記事です。
(14日目は bd_gfngfn さん、16日目は abenori さんです。)
プログラミング言語に関するニッチな話題の1つに**「その言語の正当なソースコードで最短のものは何か?」というものがあります。この記事ではSATySFiについて「最短コード」を考察**します。
前提
SATySFiの「言語の仕様」は明文化されておらず、また現在はSATySFiの処理系の実装は1つしかないため、「実装が仕様」と見なすのが妥当でしょう。従って、ここでは「SATySFIの正当なソースコード」の定義を**「SATySFiの現時点での最新リリース版である0.0.5版でコンパイルした場合にエラーが起こらずにPDFが出力されるソースコード」**と定めることとし、またソースコードの長さをバイト数で測ることにします。
注意
- PDF出力モードについて考えます。
- 非ASCII文字は結局使わないのでバイト数でも文字数でも同じです。
- 最短コードを構成する際には必須でない改行や空白文字を除去することになりますが、「除去後」のコードは非常に読みづらくなります。本記事では「除去前」と「除去後」のコードの両方を掲載した上で「除去前」のコードを見ながら解説します。「短さ」を考慮する際には常に「除去後」を前提にしていることに注意してください。
標準ライブラリの使用を認める場合
普通にSATySFiを文書作成に利用する場合は“標準ライブラリ”(mathやstdjaなどの、処理系に付随するパッケージ群)の使用が前提となります。そこでまずは、同じ前提で「最短コード」を考えてみましょう。
コンパイルが通るソースコードを書くためには**「document型の値」を作り出す必要があります。実は、標準ライブラリの中に「document型の値を簡単に作る」ためのstandalone**というパッケージが存在します。これを利用するのが最も簡単であり、恐らくこれが最短にもなると思われます。
% 標準ライブラリのstandaloneパッケージを読み込む
@require: standalone
% 以下の型をもつstandalone函数が提供される
% standalone : block-text -> document
standalone '<> % 引数に空のblock-textを渡すとdocumentが得られる
このコードをコンパイルすると、A4サイズの白紙のPDFが得られます。
「除去後」のコードは次の通り1で、33バイトになります。
@require:standalone
standalone'<>
標準ライブラリを使用しない場合
今度は、標準ライブラリも一切使わないこととしてSATySFiのプリミティヴのみを使った場合の「最短コード」を考えてみます。
standaloneパッケージの実装を参照してsource1.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バイト)が「最短」であると思われる。
- length値で一番「短い」のは
- 第4引数の型はblock-boxes型である。source2a.satyでは
read-block ... '<>
となっているが、元のblock-textが空なので、この式の値は「空のblock-boxes」、つまりblock-nil
と等価になる。先述の通り、block-boxesの「最短」はblock-nil
なのでこれに書き換えればよい。-
\math
コマンドの定義は不要になるため削除する。
-
書き換えた後のコードは以下の通りで、実際にこのコードもコンパイルが通ります(出力はやはり白紙)。
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バイトです。
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文字の変数に置き換えます。
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
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変数を定義する際にlet x=1cm let y=block-nil in ...
よりもlet(x,y)=(1cm,block-nil)in ...
の方が短くなることに注意。
このコードで(x, x)
が3回現れています。いろいろ試してみましたが、以下のようにするのが「最短」のようです。
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
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バイトとなりました。
まとめ
今日も一日サティスファイ!(まとめろ)
-
「除去後」のコードにおいては改行文字はLF(1バイト)で表すものとし、またファイルの末尾に改行文字はないものとします。 ↩