現行のSATySFiは組版を簡単にするために様々な構文要素を持ちます.その中でもブロックコマンドとインラインコマンドに関連する構文は初学者を混乱させる要素のひとつです.そこでこの記事ではSATySFiのソースコードの見通しを良くすることを目的としてブロックコマンドを一切使わないプログラミングスタイルを紹介します.この記事ではそのようなスタイルを便宜的にBlockless SATySFiと呼ぶことにします.
通常のSATySFi
まずは普通のSATySFiについて復習します.
以下は標準ライブラリにあるstdjareportというクラスファイルを用いて簡単な文書を作成する例です.
@require: stdjareport
document (| title = {My Title}; author = {wasabiz} |) '<
+p {
Hello world.
}
+p {
The quick brown fox \emph{jumps} over the lazy dog.
}
>
三行目以降の式に注目します.
初見だとパースが難しいのですが,これは全体としてはdocument
という関数に(| ... |)
というレコードと'<...>
というブロックテキストを渡しています.ブロックテキストは常に'<...>
という形のリテラルで与えられるわけではなく,場所によっては<...>
という形で与えられます.+p { ... }
はそれぞれ+p
というブロックコマンドに{...}
というインラインテキストを渡しています.インラインテキストのリテラルはブロックテキストとは違い常に{...}
という形で与えられます.インラインテキストはざっくりいえば文字列リテラルのようなものですが中にインラインコマンドの呼び出しを書くことができます.上の例だと\emph{jumps}
がそれに当たるもので,これは\emph
という名前のインラインコマンドに{jumps}
というインラインテキストを引数として渡しています.ブロックテキストはインラインテキストとは異なり生の文字列を記述することはできず,ブロックコマンドの呼び出ししか記述することができません.
…と,このようにSATySFiのブロック/インラインテキスト周りの仕様は少し複雑なものとなっています.この複雑さを軽減することがBlockless SATySFiの目的です.
Blockless SATySFi
Blockless SATySFiでは同じコードが以下のようになります.
@import: lib/typeset/stdjareport-blockless
document (| title = {My Title}; author = {wasabiz} |) [
p {
Hello world.
};
p {
The quick brown fox \emph{jumps} over the lazy dog.
};
]
通常のSATySFiとの違いは以下の通りです.
- ブロックテキスト
'<...>
をリストリテラル[...]
で代用する - ブロックコマンドの呼び出し
+foo ...
は普通の関数呼び出しfoo ...
で代用する
基本的にはこれだけの差ですが,
- 垂直モードに関するSATySFiのパーサーの規則を覚えなくて良くなる.
- リストリテラルも関数呼び出しも通常のプログラミング言語とほぼ同じなので他からの類推が効く
- 組版のためのSATySFi固有の構文要素がインラインコマンド関連のみになるので見通しが良くなる
という良さがあるでしょう.SATySFiの構文はかなり複雑なので(cf. SATySFi構文メモ)構文にまつわる面倒さを少しでも減らすことには意義があると思います.
他にも地味にいい点として
- エディタのハイライトが壊れにくくなる (
<...>
を括弧扱いにするとトラブりやすい) - ローカルなブロックコマンドが定義できない問題から解放される (ただしローカルなインラインコマンドが定義できない問題はそのまま)
- これまではブロックを返す関数を定義する際わざわざ対応するブロックコマンドを別に定義しなければならなかったがそれがなくなる
というのも挙げられます.Blockless SATySFiではブロックテキストに見えるものも全てただの普通の式なので定義済みの関数の呼び出しに限らずどんな複雑な式でもその場で記述することができます.
Blockless SATySFiのはじめ方
Blockless SATySFiの大きな問題点として過去の資産との互換性がないことが挙げられます.基本的にはブロックコマンドの定義やblock-textを引数として取る関数は全て定義し直しになります.
ただし書き換え自体はそんなに難しくありません.
たとえば
let-block ctx +mycmd arg =
...
というコマンドをblockless化する場合は
let mycmd arg ctx =
...
とするだけです.
また,ブロックテキストを引数にとる関数に対しては中でread-blockを呼んでいる場所を以下のように書き換えるだけで良いです.つまり,b
がブロックテキストのとき,
read-block ctx b
を
block-list-to-block-boxes ctx b
のように書き換えます.ただし,このblock-list-to-block-boxes
を事前に以下のように定義しておくこととします.
let block-list-to-block-boxes ctx b =
b |> List.map (fun b -> b ctx)
|> Block.concat
まとめ
Blockless SATySFiを採用することで見た目上の構文要素が減りソースコードの見通しがよくなります.
今回紹介したやり方でstdjareportをblocklessにしたものをsatysfi-libに用意しているので試したくなった方は是非どうぞ.
https://github.com/nyuichi/satysfi-lib/blob/master/typeset/stdjareport-blockless.satyh
ところでこの記事は以下のツイートをしたときに書こうと思っていたものをやっと文書化したものです.技術書典7の開催が1週間後ですがまだ書き始めていない猛者もいると思うのでBlockless SATySFiで記事を書いてみてください.
satysfi,ブロックコマンドの存在意義がわからなくなってきた
— わさびず@技術書典7 け04D (@___yuni) August 27, 2019