これは「SATySFi Advent Calendar 2019」の5日目の記事です。
(4日目は monaqa さん です。)
(6日目は amutake さん です。)
できた!!
let hex str =
let len = string-length str in
let dval digit = match digit with
| `0` -> 0 | `1` -> 1 | `2` -> 2 | `3` -> 3 | `4` -> 4
| `5` -> 5 | `6` -> 6 | `7` -> 7 | `8` -> 8 | `9` -> 9
| `A` -> 10 | `B` -> 11 | `C` -> 12
| `D` -> 13 | `E` -> 14 | `F` -> 15
| `a` -> 10 | `b` -> 11 | `c` -> 12
| `d` -> 13 | `e` -> 14 | `f` -> 15
| _ -> abort-with-message `bad number format` in
let-rec iter k a =
if k < len then
iter (k + 1) (a * 16 + (dval (string-sub str k 1)))
else a in
if len == 0 then abort-with-message `bad number format`
else iter 0 0
さっそく使ってみましょう!
@require: stdjareport
%↓さっきのhex.satygを読み込む
@import: hex
let-inline \show-int n = embed-string (arabic n)
in
document (|
title = {\SATySFi;コード中で整数を16進数で書きたい};
author = {某ZR};
|) '<
+chapter {hex函数を使ってみる} <
+p {\show-int(hex `2603`);} % 0x2603 = 9731
+p {\show-int(hex `1F986`);} % 0x1F986 = 129414
>
>
完璧!!
(追記)最初にお詫び
以下の話は「SATySFiは16進数表記をサポートしない」という前提で書かれていますが、これは私の単純な勘違いであり、実際にはSATySFiは普通に0x
で始まる16進数表記をサポートしています(参照)。ただし実は「16進数表記の実装」自体は本題ではないので、以下での話は「仮に未サポートだった」と思って読んでください
ちゃんと説明しろ
※本記事の内容はSATySFiのmaster版を前提とします。(最新リリース版のv0.0.3ではない)
(現状では)SATySFiのコードにおいて整数値は10進表記(129414
など)で書くことになっていて、他のプログラム言語とは異なり16進表記(1F986
など)はサポートされていません。(←勘違い)
もちろんSATySFiは「組版を目的とする」言語であるため、16進数が必要となる場面は少ないでしょう。しかし、例えばUnicodeの符号値は16進数で指定するのが普通なので、16進数表記の需要が全くないわけではないはずです。
hexパッケージは以下の函数hex
を(グローバルに)定義します。
-
hex
: string → int
hex str は str が16進表現として表す整数を表す。A~Fの文字は大文字でも小文字でもよい。符号はサポートしない。str の書式が不正の場合は実行時エラーになる。
hexを利用したもう少し実用的(?)な例として、Unicodeの符号値を16進数で指定する例を挙げます。
@require: stdjareport
%↓hexパッケージを利用する
@import: hex
%% \u(uc) : [int] inline-cmd
% 整数 uc をUnicode符号値とする文字を出力する.
let-inline \u uc = embed-string (string-unexplode [uc])
%% +wagahai(n) : [int] block-cmd
% "吾輩は猫である。"の段落をn回出力する.
let-block ctx +wagahai n =
% 評価時に端末にメッセージを表示する
let () = display-message `+wagahai is called` in
let ib-w = read-block ctx '< +p{吾輩は猫である。} > in
let-rec iter | 0 ib = ib
| n ib = iter (n - 1) (ib +++ ib-w) in
iter n block-nil
in
document (|
title = {\SATySFi;コード中で整数を16進数で書きたい};
author = {某ZR};
|) '<
+chapter {「吾輩は猫である」と3回言ってみる} <
+wagahai(3);
>
+chapter {「吾輩は猫である」の作者は?} <
+p {森\u(hex `9DD7`);外(違う)}
>
>
やっぱり完璧
本当に完璧なのか?
さて、ここからが本題です。
このhexパッケージは、確かに「16進表記を解釈する」機能としては完璧なのですが、ひとつ大きな問題があります。それは「引数の文字列の書式が不正である場合に実行時にエラーが発生する」ということです。
何が問題であるかを、先ほどのUnicode符号値を読む例を用いて確かめてみましょう。test-2.satyのコードの一部を次のように変えてみました。
- 16進表記
9DD7
の中のD
をÐ
(U+00D0)に“書き間違えて”みる。Ð
は正当な16進数字でないので当然hexでエラーが発生する。 - 「吾輩は猫である」を3回ではなく20000回(870ページ分)出力する。
@require: stdjareport
%↓hexパッケージを利用する
@import: hex
let-inline \u uc = embed-string (string-unexplode [uc])
let-block ctx +wagahai n =
% 評価時に端末にメッセージを表示する
let () = display-message `+wagahai is called` in
let ib-w = read-block ctx '< +p{吾輩は猫である。} > in
let-rec iter | 0 ib = ib
| n ib = iter (n - 1) (ib +++ ib-w) in
iter n block-nil
in
document (|
title = {\SATySFi;コード中で整数を16進数で書きたい};
author = {某ZR};
|) '<
+chapter {「吾輩は猫である」と2万回言ってみる} <
+wagahai(20000); % 3→20000に変更
>
+chapter {「吾輩は猫である」の作者は?} <
% ↓マチガッテル
+p {森\u(hex `9ÐÐ7`);外(違う)}
>
>
このマチガッテル文書をコンパイルしてみましょう。
$ satysfi test-3.saty
---- ---- ---- ----
target file: 'test-3.pdf'
...(略)...
---- ---- ---- ----
evaluating texts ...
+wagahai is called
! [Error during Evaluation] bad number format
おっと、「bad number format
」のエラー1が発生しました。それは予定通りなのですが、ここで重要なのは、このエラーが出る直前に十数秒くらい端末表示が止まっていることです。この停止の原因は「吾輩は猫である」2万回が処理された後に実行時エラーが発生したからだと推測されます。
実は、この推測が正しいことはエラーの直前にある端末出力を見れば確認できます。
+wagahai is called
この端末出力は+wagahai
命令の実装コードの先頭行にあるdisplay-message
によるものです。つまり、+wagahai(20000);
の処理は完了していて、その後にエラーが発生したわけです。
後からエラーが出る、それは大問題
hexパッケージを作ったそもそもの動機は「SATySFiで16進の整数表記を可能にすること」でした。普通のプログラミング言語で9ÐÐ7
のような不正な16進の整数表記があれば「文法エラー」として即座に実行は中断される2はずです。それと比べると、870ページ分の文書データを処理して初めてエラーが出るというのは随分不便なことです。
『The SATySFibook』でLaTeXをディスっての問題点を指摘している節(3ページ)に以下のような記述があります。
エラー報告は不親切なだけでなく提示されるまでに時間を要することも多い。やはり「組版処理中に初めて不整合が生じた」ときにエラーが報告されるのが要因で、例えば500ページの文書をLaTeXで組む際に490ページ目にあたる部分に不適格な記述を書いてしまった場合は490ページ分の組版処理の後にエラーが提示されるため、ユーザは入力ミスの存在を知らされるまで短めに見積もっても数十秒から数分程度は待つことになる。
そうです。数百ページの文章を処理した後でエラーが起こるというのは、まさにアレアレなLaTeXの世界を象徴する事象なのです。“LaTeXを倒すため”に生まれてきたSATySFiとしては何とも許しがたいことです。
やっぱり、今のhexパッケージは完璧からは程遠いものでした。
完璧なhexのために必要なもの
このアレアレな“LaTeX的事象”を解決する術はあるのでしょうか。実はこれに関する発表が、先日催された「Regex Festa」で行われたようです。
発表は一発芸なんですが、やってることはSATySFiに多段階計算が導入されたモチベーションそのもの(DSLの正当性を静的に(stage 0で)保証したい)なので曲芸ではないです。むしろ非自明で最も簡単なmotivatingなexampleって言っていいんじゃないかくらいの勢い。
— κeen (@blackenedgold) October 18, 2019
どうやら「多段階計算」とかいうやつを使うと“LaTeX的事象”が解決できるようです。
SATySFiの多段階計算
では、その多段階計算とは一体どんなものなのでしょうか。さっそく当該の発表資料を見てみましょう。
……なるほど、わからん
SATySFi Wikiにgfn氏自身による解説があるので、それも参照してみましょう。
- SATySFiに多段階計算を入れる構想(SATySFi Wiki)
なるほど、サッパリわからん
それでも頑張ってみる
絶望しかないわけですが、それでもκeen氏の実装コードを穴の開くほど見つめたり、時には情熱のSATySFiダンスを踊ったりしながら、結局「全面的に直観に頼る」という最終手段に訴えて「多段階計算版のhexパッケージ」(hex-msパッケージ)をつくってみました。
@stage: 0
let hex str =
let len = string-length str in
let dval digit = match digit with
| `0` -> &0 | `1` -> &1 | `2` -> &2 | `3` -> &3 | `4` -> &4
| `5` -> &5 | `6` -> &6 | `7` -> &7 | `8` -> &8 | `9` -> &9
| `A` -> &10 | `B` -> &11 | `C` -> &12
| `D` -> &13 | `E` -> &14 | `F` -> &15
| `a` -> &10 | `b` -> &11 | `c` -> &12
| `d` -> &13 | `e` -> &14 | `f` -> &15
| _ -> abort-with-message `bad number format` in
let-rec iter k a =
if k < len then
iter (k + 1) &(~a * 16 + ~(dval (string-sub str k 1)))
else a in
if len == 0 then abort-with-message `bad number format`
else iter 0 &0
多段階計算を使った函数を実際に使う場合、典型的には函数適用の結果に~
を適用した形を書くことになるようです。つまり、hex `…`
の代わりに~(hex `…`)
と書く必要があります。
@require: stdjareport
%↓多段階計算版のhex-msを読み込む
@import: hex-ms
let-inline \show-int n = embed-string (arabic n)
in
document (|
title = {\SATySFi;の整数を16進数にしたい};
author = {某ZR};
|) '<
+chapter {多段階なhexを使ってみる} <
% "~(hex …)" という形で呼び出す
+p {\show-int(~(hex `2603`));} % 0x2603 = 9731
+p {\show-int(~(hex `1F986`));} % 0x1F986 = 129414
>
>
恐る恐るコンパイルしてみると……。
もしかして:できた?
マチガッテルやつを試してみる
多段階計算版のhex-msパッケージができたようなフインキも若干感じるので、さっそくhex-msを使って「870ページ分処理した後にマチガッテル文書」を試してみましょう!
@require: stdjareport
%↓多段階計算版のhex-msを読み込む
@import: hex-ms
let-inline \u uc = embed-string (string-unexplode [uc])
let-block ctx +wagahai n =
% 評価時に端末にメッセージを表示する
let () = display-message `+wagahai is called` in
let ib-w = read-block ctx '< +p{吾輩は猫である。} > in
let-rec iter | 0 ib = ib
| n ib = iter (n - 1) (ib +++ ib-w) in
iter n block-nil
in
document (|
title = {\SATySFi;の整数を16進数にしたい};
author = {某ZR};
|) '<
+chapter {「吾輩は猫である」と2万回言ってみる} <
+wagahai(20000);
>
+chapter {「吾輩は猫である」の作者は?} <
% ↓マチガッテル
+p {森\u(~(hex `9ÐÐ7`));外(違う)}
>
>
このファイルをコンパイルしてみると……。
---- ---- ---- ----
target file: 'test-5.pdf'
...(略)...
preprocessing 'stdjareport.satyh' ...
preprocessing 'test-5.saty' ...
! [Error during Evaluation] bad number format
今度は+wagahai is called
が表示されずに(つまり「我輩は猫である」2万回を処理する前に)すぐにエラーが出て終了しました。ユーザに優しいSATySFiの世界が帰ってきたわけです!
まとめ
SATySFiの多段階計算はスゴイ、だけど、なにもわからない