⛄️

【入門記事】 SATySFi に登場する代表的な型まとめ

2020/12/03に公開

この記事は SATySFi advent calendar 2020 の3日目の記事です。
昨日は matsud224 さんによる記事「パッケージ一覧サイト “Satyrographos Package Index” の宣伝」でした。
明日は na4zagin3 さんによる記事「2020 年 Satyrographos 周辺の動きと次メジャーアップデートに向けた展望」です。

monaqa です。もう12月ですね。12月といったらクリスマス、クリスマスといったら SATySFi です。楽しく SATySFi でマークアップしましょう!

SATySFi でマークアップするには、コマンドを使いこなすことが必要不可欠です。逆に、コマンドさえ完璧に使いこなせればあなたはもうすでに立派な SATySFier、かもしれません。そこで、アドベントカレンダーという折角の舞台を活かして「SATySFi のコマンドを使いこなす上でもしかすると役に立つかもしれない3つのこと」をまとめたいと思います。

  1. 【12/3】SATySFi の型について (本記事!
  2. 【12/10】SATySFi のコマンドを書く方法
  3. 【12/17】SATySFi のコマンドを定義する方法

ここで説明する内容は基本的に The SATySFibook に記載されています。詳しい説明はそちらを参照してください。ただし、この記事に関して誤りを発見された場合は記事下のコメントまたは Twitter の @mo_naqa などにご連絡いただけると助かります。
また、以下の解説は SATySFi v0.0.5 を想定しています。将来のバージョンで仕様が変わる可能性があるためご注意ください。

はじめに

冒頭で述べたとおり、本記事では SATySFi における型 (type) について説明します。

SATySFi は静的型付け言語であり、コマンドにも型が付いています。SATySFi ではコマンドの引数に好き勝手な引数を渡すことはできません。これは初めのうちこそ戸惑いもあるものの、慣れれば意図せぬ入力ミスを未然に防ぐ頼もしい味方となってくれます。SATySFi の型について詳しくなっておけば、コマンドの書き方や定義の仕方を理解する上で大きく役立つでしょう。

SATySFi には様々な型がありますが、大きく分けるとすれば

  • 基本型(数値、文字列、真偽値など)
  • 複合型(タプル、リスト、レコード、代数的データ型)
  • 文書に関連する型(インラインテキスト、ブロックボックス、数式など)
  • グラフィックス周りの型(パス、グラフィックス、括弧、デコレーションなど)
  • etc.

といったところでしょうか。今回はこのうちコマンドと深く関係する上3種類、基本型、複合型、文書に関連する型について説明します。

要約だけ読みたい人のための目次

基本型(数値、文字列など)

数値や文字列といった、プログラミングを行う上で基本となる型について述べます。

数値型

SATySFi にはデフォルトで以下の型が用意されています。

  • int
  • float
  • length

整数型 (int)

SATySFi において、整数型はリストの要素数やページ数など、様々な所に用いられます。以下のようなリテラルは整数型として扱われます。

42
-3
0
0x23AF

整数型は普通のプログラミング言語とさほど変わりません。最後の例のように頭に 0x を付けて16進数表示もできます。HTML の Hex color を書きたいときなどに便利です。一点だけ、16進数表示する際に使える文字は 0-9A-F に制限されており、アルファベット小文字は使えないことに注意しましょう [1]

浮動小数点数型 (float)

SATySFi には当然浮動小数点数もあり、フォントサイズの拡大率といった連続値を表現する際に用いられます。以下のようなリテラルは浮動小数点数として扱われます。

2.3
2.
.3

2.2.0 に、 .30.3 に等しいものとして扱われます。なお、-3.0 のようには書けないことに注意してください。 SATySFi が OCaml と同様に演算子のオーバーロードを認めない文法であり、単項演算子のマイナス - は整数型にのみ適用できるからです。

長さ型 (length)

length 型はその名の通り「長さ」を表す型であり、テキストの横幅やフォントサイズなどを表すのに用いられます。通常のプログラミング言語なら浮動小数点数を用いて表すところですが、言語仕様として別に用意されているのはいかにも組版用言語ならではですね。以下のようなリテラルは length 型として扱われます。

2pt
2.3cm
-2.3mm

このように、長さのリテラルは数字と単位がくっついた形をしています。現時点では pt / inch / cm / mm の4種類の単位に対応しているようです。それぞれの厳密な単位換算は The SATySFibook をご覧ください。なお、数字と単位の間に空白を入れてはいけません。2.3␣pt としてしまうと、浮動小数点数の 2.3pt という変数が2つ並んだものとして扱われてしまうためです。

数値型の基本演算

SATySFi は OCaml と似た文法を採用しており、演算子のオーバーロードを許しません。たとえば 2 + 3 とは書けますが、 2.0 + 3.0 と書くことはできません。 + という記号は両辺が整数である場合のみに定義された演算子であり、そうでない場合は対応できないためです。この場合は 2.0 +. 3.0 と書く必要があります。
数値型の四則演算では、以下の表のように型に応じて演算子を使い分けましょう。

左辺 演算 右辺 記法
int 足す int a + b
int 引く int a - b
int 掛ける int a * b
int 割る int a / b
float 足す float a +. b
float 引く float a -. b
float 掛ける float a *. b
float 割る float a /. b
length 足す length a +' b
length 引く length a +' b
length 掛ける float a *' b
length 割る length a /' b
数値型同士の四則演算

length 割る float」 などの演算子はデフォルトで用意されていませんが、a *' (1.0 /. b) で代用できるため特に大きく困ることはないでしょう。OCaml のように自分で新たな二項演算子を定義することもできます。

数値型の型変換

型が自動的にキャストされることもないため、整数と浮動小数点数を足すなどしたい場合は片方の型を明示的に変換する必要があります。代表的な型変換を行う方法は以下の通りです。

変換元 変換先 方法
int float float x
float int round x (切り捨て)
float length 1pt *' x など [2]
length float x /' 1pt など
数値型の変換

文字列型

SATySFi では、文字列を扱う string 型があります。SATySFi の文字列リテラルは1つ以上のバッククオーテーションの並びを合図として始まり、それと同じ数のバッククオーテーションの並びを合図として終わります。「同じ数のバッククオーテーション」が出現するまでは、それ以外のどんな文字も文字列リテラルの一部として扱われます。たとえば以下のようなリテラルは string 型となります。

`hoge fuga`
``hoge fuga``
`hoge
 fuga`  % 途中で改行を挟んでもOK

注意すべきは、この記法だと「文字列開始時・終了時に付いた空白は自動でトリムされる」という点です。たとえば `␣␣hoge fuga␣␣` のように書くと、実際には hoge fuga という中身を持った文字列リテラルとなります。最初が空白から始まる文字列を書きたい場合は、外側に # を付ける必要があります。#`␣␣hoge fuga␣␣`# のように書くと、␣␣hoge fuga␣␣ という中身を持った文字列リテラルが得られます。
SATySFi の文字列リテラルにはエスケープ記号がありませんが、これらの記法を駆使することで任意の文字列リテラルを表現することができます。詳しくは The SATySFibook を参照してください。

SATySFi のマークアップには、基本的に string 型ではなく後述の inline-text 型が用いられます。そのため、ユーザがマークアップを行うときに string 型が主役となることはさほど多くありませんが

  • string-same 関数で等価性を判定できる
  • string リテラルを使えば、任意の文字列をエスケープ文字なしでコード上に書ける

といった性質から、下記のようなものを表現する際によく用いられます。

  • 相互参照にて参照先を紐付けるラベル
  • 書体を指定する際に用いるフォント名
  • コードブロックや LaTeX の verbatim 環境のように、書いた文字列をそのまま出力させたいとき

その他の基本型

真偽値 (bool) 型と unit 型があります。bool 型は truefalse の2値を取る型です(メジャーなプログラミング言語を何か書いたことがあれば、詳しい説明は不要でしょう)。unit 型は () という値だけを取りうる型です。SATySFi ではそこまで頻繁には見かけないものの、副作用を持つ関数などでは戻り値が unit 型となることがあります。

基本型のまとめ

  • SATySFi では数値型として int, float, length がある。
    • 数値型の演算で用いる演算子は、型ごとに異なる。
    • 数値型の暗黙の型変換は行われないため、明示的な型変換が必要。
  • SATySFi では文字列型として string が用意されている。
  • その他、bool 型や unit 型がある。

複合型

SATySFi では(他の多くのプログラミング言語と同様に)様々な型を組み合わせて、より複雑な型を作成することができます。複合型は種類も多く網羅的に解説するのは大変なので、今回は SATySFi を用いたマークアップに頻出しそうな型を簡単に紹介するにとどめます。詳細は The SATySFibook の解説を参照してください。

タプル

複数の式を単にひとまとめにしたものがタプル (tuple) です。例えば (2pt, 3pt) という式は length 型を2つ並べたタプル型であり、これを length * length と表現します。数学的には直積を表すから掛け算で表現するわけですね。他の多くのプログラミング言語と同様、異なる型を組み合わせてタプルを作ることもできます。例えば (2, 3.5, `hoge`)int * float * string 型を持ちます。
タプルは様々な用途で用いられます。

  • 多次元の情報を格納したいとき。例えば紙面上の座標は (100pt, 200pt) のように length * length 型で表すのが便利です。
  • 複数の値を返り値に持つ関数を定義したいとき。SATySFi の関数は複数の戻り値を指定することができませんが、問題はありません。返したい値のタプルを戻り値とすることによって、複数の戻り値を簡単に実現できるからです。

レコード

多次元の情報を格納する最も単純な方法はタプルを用いることですが、個々の構成要素に名前を付けたほうが管理しやすい場合もあります。CやRustなどの言語では構造体 (struct) を用いるのが一般的ですが、 SATySFi ではレコード (record) 型を用います。
以下のような式はレコード型として取り扱われます。

(| path = `/path/to/image`; width = 200pt; pos = (100pt, 300pt); |)

上に示した例のように、レコード型は label = expr; というペアを (||) の中にいくつか書くことによって表現されます(最後のセミコロンはあってもなくても構いません)。上の例で述べたレコードは (| path: string; width: length; pos: length * length |) という型を持ちます。

SATySFi ではキーワード引数の代替としてよく用いられます。SATySFi の文法では関数やコマンドにキーワード引数を与えることができませんが、レコードを渡すことは可能です。設定値などを格納したレコードを関数の引数として渡すよう関数を設計すれば、キーワード引数の代わりになるというわけです。たとえば標準の stdja パッケージの StdJa.document 関数の第1引数は以下のようなレコード型となっています。

(|
  title: inline-text;
  author: inline-text;
  show-title: bool;
  show-toc: bool;
|)

SATySFi のレコードに関する型は実際にはもう少し複雑です。詳しくは The SATySFibook 第1版の 3.2.12 節を参照してください。

リスト

式を何個か1列に並べたものがリスト (list) です。タプルとは異なり要素数が事前に決まっている必要はありませんが、格納する値はすべて同じ型を持つ必要があります。たとえば以下のような式はリストとして扱われます。

[1; 2; 3; 4; 5]
[(10pt, 20pt); (10pt, 30pt); (40pt, -10pt);]
[[1]; [2; 3]; [4; 5; 6]]

1番目の例は int list 型を、2番目の例は (length * length) list 型を、3番目の例は (int list) list 型を持ちます。例を見れば分かる通り、 リストは [] の間に ; で区切られた要素を並べることによって表現されます。3番目のようにリストをネストして多次元配列のような構造にすることも可能です。

一般的なプログラミング言語における配列やベクタが様々なところで用いられるように、SATySFi のリストにも多様な用途があります。たとえば複数行の数式を記述できる \align コマンドでは、引数に数式のリスト(より正確には (math list) list 型)を取ります。

オプション

オプション (option) 型は「あるかもしれないし、無いかもしれないもの」を格納するのに用いられます。 SATySFi では関数やコマンドのオプション引数(省略可能な引数)を実装するときに特によく用いられます。
たとえば「基本的にはフォントサイズを倍にするが、オプションで倍率を指定することも出来るコマンド \enlarge」を定義したくなったとします。

{
  ここは普通のテキスト。
  \enlarge{ここだけ2倍になる。}
  ここは普通のテキスト。
  \enlarge?:(3.5){ここだけ3.5倍になる。}
  ここは普通のテキスト。
}

という具合です。このようなコマンドは以下のように定義することができます。

let-inline ctx \enlarge ?:expand-rate-opt text =
  let expand-rate = match expand-rate-opt with
    | Some(rate) -> rate
    | None       -> 2.0
  in
  let fontsize = get-font-size ctx in
  let ctx2 = set-font-size (fontsize *' expand-rate) ctx in
  read-inline ctx2 text

詳しい説明は長くなってしまうため割愛しますが、上の定義の expand-rate-opt という変数が float option 型を持っています。これがオプション型です。オプション型の中身は、上のようにmatch式によるパターンマッチを用いて取り出すことができます。

複合型のまとめ

SATySFi で頻繁に登場する代表的な複合型は以下のとおりです。

  • タプル
  • レコード
  • リスト
  • オプション

なお、SATySFi では更に複雑なデータ構造を表現するために、独自の型を定義することもできます。OCaml などの関数型言語でよく見かける代数的データ型もあります(option 型はデフォルトで用意されている代数的データ型の一種です)。

文書に関連する型

SATySFi の文書は、

  1. インライン方向(横方向)にグリフを並べてグリフの列を作る
  2. ブロック方向(縦方向)に段落を並べて文書を作る

という処理を経て作成されます。ここでは横書きの場合を想定して説明しています[3]
このとき、

  1. 「横方向に並んだグリフの列」のことをインラインボックス列と呼び、SATySFi では inline-boxes という型で表現します [4]
  2. 「縦方向に並んだボックスの列」のことをブロックボックス列と呼び、SATySFi では block-boxes という型で表現します。

SATySFi で生成される PDF に印字されているグリフも図版も数式も、それらは基本的に全て inline-boxes であり、段落は基本的に全て block-boxes です。インラインボックス列やブロックボックス列をどこにどのように配置するか、はまさにSATySFiを用いた組版処理における要なのです。

しかし、inline-boxesblock-boxes 型は書体やサイズ、色といったレイアウトに関する情報を全て含むため、ユーザが直接 inline-boxes 型などを指定して組むのはやや扱いづらいものがあります。SATySFi では組版の内容とスタイルの指定を分離するため、

  • コンテンツに関する情報を取り出した インラインテキスト型 (inline-text) 及び ブロックテキスト型 (block-text) 型
  • 書体、サイズ、色といった情報を保持する テキスト処理文脈 (context) 型

が別に用意されています。HTML + CSS 組版でいうと、前者は HTML ファイルの所有するデータに、後者は CSS ファイルの所有するデータに近いと言えます。

まとめると、 SATySFi では文書に関する型として以下の表の4つの型 + context 型が登場します。

並び順 テキスト ボックス列
インライン inline-text inline-boxes
ブロック block-text block-boxes

うーむ、ややこしいですね。

上で紹介した4つの型は、具体的にどのような形で最終的な PDF となるのでしょうか。
以下の図は、ユーザが入力したインラインテキストを SATySFi が段落として組み上げるまでに経る工程を示しています。


ユーザの入力から段落が生成されるまでの流れ

各工程について、もう少し詳しく見てみましょう。

インラインテキスト→インラインボックス列

一番最初の インラインテキスト は、最終的に組むことになるコンテンツの内容です。たとえば SATySFi では

+p{The quick brown fox jumps over the lazy dog.}

のように書くことで段落を作成できますが、このとき +p コマンドの引数に渡している {The quick ...}inline-text 型です。インラインテキストのリテラルは

{The quick brown fox jumps over the lazy dog.}

のように波括弧 { } で挟んで表現します。

インラインテキストは、read-inline という関数を用いてインラインボックス列 (inline-boxes 型) へと変換されます。

let ib = read-inline ctx {The quick brown fox jumps over the lazy dog.}

このように、read-inline に以下の2つの引数を指定することで inline-boxes 型の値 ib を得ることができます。

  1. context 型 (上の例では ctx という名前の変数)
  2. inline-text 型(上の例では {The quick ...} というリテラル)

context 型の引数 ctx には、先程述べた通り書体やフォントサイズといった情報が格納されています。read-inline はインラインテキストを眺め、 ctx に格納された情報に従ってテキストをグリフへと変換していきます。その結果、The quick ... という内容を持ち、テキスト処理文脈 ctx に従って組まれたグリフの列が返り値 ib として得られることになります。

インラインボックス列→ブロックボックス列

これで PDF に出力されるグリフの並びが得られましたが、処理はここで終わりではありません。
実際の紙面は横方向の長さに限りがありますから、グリフの並びをいくつかの行に分ける行分割処理を行う必要があります。
この行分割処理を行って初めてテキストは「段落」となることができます。 SATySFi でこの行分割処理を担ってくれるのが line-break 関数です。

let bb = line-break true true ctx ib

このように、line-break に以下の4つの引数を指定することで block-boxes 型の値 bb を得ることができます。

  1. bool 型の変数(上の例では true リテラル)
  2. bool 型の変数(上の例では true リテラル)
  3. context 型 (上の例では ctx という名前の変数)
  4. inline-boxes 型(上の例では ib という名前の変数)

第1引数、第2引数はそれぞれ作成する段落の上下での改ページを許可するかどうかを表します。より注目すべきは第3引数と第4引数で、行分割処理がテキスト処理文脈とインラインボックス列という2つの情報に基づいて行われるということを示しています。

context 型の引数 ctx には、書体やフォントサイズだけでなく、行送りや段落間空白、単語間空白といった「行分割の際に必要な情報」も格納されています。line-break はインラインボックス列を適切に分割し、ctx に格納された情報に従って可能な限り自然な形で段落を作成します。その結果、The quick ... という内容を持ち、テキスト処理文脈 ctx に従って組まれた段落(ブロックボックス列)が返り値 bb として得られることになります。

inline-textstring の違い

さて、ここまで読んだ方は「inline-text 型と string 型ってどう違うの」と思われるかもしれません。たしかに inline-textstring の立ち位置は似ていますね。記法は異なるものの、どちらもユーザが直接好きな文字を書くことができます。

let it = {The quick brown fox jumps ...} in
let str = `The quick brown fox jumps ...` in

inline-textstring の一番大きな違いは、「inline-text のリテラルはコマンドを埋め込むことが出来る」という点でしょう。inline-text は単なる文字だけではなく、中に インラインコマンド を埋め込むことができます。

let it = {The quick \textcolor-brown{brown} fox jumps ...} in

さらに、インラインコマンドは「文字を太字にする」「フォントサイズを倍にする」「文字色をブラウンにする」のように、テキスト処理文脈に関する指定を行うこともできるのです
(LaTeX の \textbf コマンド等に相当するものを実現しようと思えば、そういった処理は当然できてほしいものです)。


インラインテキストにコマンドを埋め込むことで、テキスト処理文脈を部分的に変更できる

inline-text はテキストの内容を格納したものだ、と表現しましたが、体裁に関する情報を指定できないわけではありません。その意味で、inline-text は「テキスト処理文脈を渡されてインラインボックス列になるのを待っている」型である、と言ったほうが近いかもしれません。

ブロックテキスト→ブロックボックス列

さて、今までの説明では inline-text, inline-boxes, block-boxes の3つの型が登場しました。まだ block-text 型が登場していません。
block-text 型も inline-text 型同様にユーザが内容を直接記述することを想定した型であり、 block-text のリテラル記法が存在します。たとえば、以下のようなものは block-text 型です。

let bt = '<
  +p{Hello, \SATySFi;!}
  +p{The quick brown fox jumps over the lazy dog.}
>

block-text のリテラルは '< という括弧で始まり、 > という括弧で終わります。block-textinline-text と異なり、中に好きな文字を直接書くことはできません。 '<あああ> などと書けば構文エラーとなります。block-text の中には、代わりに ブロックコマンド を記述します。上の例で +p{ ... } と書かれているものは全てブロックコマンドです。

inline-text 型は「インライン方向のコンテンツ(テキストなど)」を指定するための型でしたが、block-text 型は「ブロック方向のコンテンツ(段落など)」を指定するための型です。上のリテラルは「Hello, ... で始まる段落」と「The quick ... で始まる段落」を順に並べる、というコンテンツを表しています。

block-textblock-boxes に変換するには read-block 関数を用います。

let bb = read-block ctx bt

ここまでをまとめると、4つの型の関係は以下のようになります。


4つの型 + context 型と、それらがどんな関数で変換されるか

数式型

忘れそうになりましたが、文書に関連する型としてもう一つ、数式型 (math) があります。

数式型はその名の通り、数式を組むための型です。ユーザが数式の内容を指定することを想定しており、以下のようにリテラルを書くことができます。

let m = ${E = mc^2}

math 型は ${} という括弧で囲みます。 $ は LaTeX のドル記法からきていると考えれば覚えやすいですね。

math 型は inline-text 型と似ており、context 型を用いてインラインボックス列に変換することができます。変換には embed-math 関数を用います。

let ib = embed-math ctx m

インラインテキストの中に直接数式を埋め込むこともできます。

+p{アインシュタイン曰く、${E = mc^2} である。}

文書に関連する型のまとめ

文書に関連する型は以下のようなものがある。

  • context : PDF に印字する際の書体、フォントサイズ、色といった情報を格納する型。
  • inline-text : インライン方向に並べるテキストの内容。inline-text リテラルを記述するための記法が用意されており、ユーザが直接値を指定することができる。
  • block-text : ブロック方向に並べるテキスト(見出しや段落など)の内容。block-text リテラルを記述するための記法が用意されており、ユーザが直接値を指定することができる。
  • inline-boxes : 主に context 型の情報を用いて inline-text 型の内容を「組む」ことによって得られる、インライン方向のボックス列。
  • block-boxes : 主に context 型の情報を用いて block-text 型の内容を「組む」ことによって得られる、ブロック方向のボックス列。
  • math : インライン方向に並べる数式。 math リテラルを記述するための記法が用意されている。

終わりに

コマンドについて説明するための事前知識のような立ち位置でしたが、かなり長くなってしまいました。来週はいよいよ、コマンドの使い方について本格的に説明します。

脚注
  1. もし 0xC0xc と書いてよいことにすると、後に登場する length 型と紛らわしくなるためだと考えられます。 ↩︎

  2. float -> length の変換は単位によりますが、そのときは上の 1pt の部分を適当に変更するだけです。 ↩︎

  3. 実のところ SATySFi で縦書きの文書はまだサポートされていません。 ↩︎

  4. SATySFi のインラインボックス列は文字のグリフだけでなく、グラフィックスや外部から読み込んだ画像なども含まれます。 ↩︎

Discussion