SATySFiの可換図式パッケージの使い方

これは2018 SATySFi advent calendar 11日目の記事です。[] []

SATySFiの標準パッケージの中に可換図式(comutative-diagram)を書くためのcdというパッケージがあるんですが、説明がThe SATySFi Bookや公式ドキュメントの中に見当たらなかったような気がするので使い方を紹介してみます。

1. モジュールインターフェース

cdパッケージのソースcd.satyhを開いてみると以下のようなシグネチャが書いてあります

module CD : sig
  type obj
  val \diagram : [length; length; (|
      obj             : point -> math -> obj;
      draw-obj        : obj -> graphics list;
      draw-arr        : math -> float?-> length ?-> obj -> obj -> graphics list;
      draw-dashed-arr : math -> float?-> length ?-> obj -> obj -> graphics list;
    |) -> graphics list] inline-cmd
end = struct

コマンドは\diagramというインラインコマンドが1つだけあります。 型を見ると、lengthを2つと”何かよくわからんレコードを受け取ってgrahicsのlistを返す関数”を引数に取るコマンドになっていて、この最後の関数はナニ??って話なんですが、これはLisp/Schemeによくあるcall-with-なんとか系の関数に渡す関数、あるいはRubyで言うとFile.openとかのメソッドに渡すブロック引数みたいな感じの関数です。

第1、第2引数で図の領域の幅・高さを指定して、その領域に図式を書き込むための関数をレコードに詰め込んで第3引数の関数に渡し、その結果のgraphicsのリストを(枠線加えて)描画する、という動作になります。 「描画機能を詰め込んだツールボックスを渡すから後は自分で頑張って描いてね」という具合です。(以下の説明ではこの第3引数の関数に渡されるレコードのことを"ツールボックス"と勝手に呼んで行きます。)

もしSATySFi言語の見た目がRuby風だったら↓こんなですかね。

d = CD::diagram(360, 240) {|toolbox|
  a = toolbox.obj(...)
  b = toolbox.obj(...)
 [
   toolbox.draw_obj ... ,
   toolbox.draw_arr ... ,
   ...
  ].flatten
}

draw-objとかdraw-arrとかモジュールから直接使えるようにしないのはナンデ?って話なんですが、

  • SATySFiの図形描画系関数が取る位置指定引数って基本的にページ内の絶対座標だけど、図の入る位置からの相対座標で指定したいよね
  • 図式内で描画する数式のフォント設定だとかは図の入る場所のテキスト処理文脈の物と揃えたいよね

などの事情があるので、この辺をよしなに解決できるよう基点の座標だとかテキスト処理文脈が設定済みのツールボックスをユーザー指定の関数に引き渡すような仕組みになってる物と思われます。

それではこのツールボックスの中身を見ていきましょう。

obj

objツールボックスのフィールドのobj関数とありますが、型objは可換図式の対象(頂点)を表す型です。

  type obj = point * graphics

座標とgraphicsのタプルとなっています。

ツールボックスのフィールドの方は

  obj : point -> math -> obj;

この関数は座標と数式を引数に取り、数式をgraphics化した物と座標をタプルにしてobj型データとして返します。

draw-obj

  draw-obj : obj -> graphics list;

obj型データを引数に取り、graphicsを取り出してlistにして返します。

draw-arr, draw-dashed-arr

  draw-arr        : math -> float -> length -> obj -> obj -> graphics list;
  draw-dashed-arr : math -> float -> length -> obj -> obj -> graphics list;

対象間の射(矢)を書く関数です。draw-arrは実線、draw-dashed-arrは点線の矢印を描きます。 引数の型と内容は共通で、順に以下の通り

  1. math : 射に対応する数式
  2. float : 矢印の始点を0.0 終点を1.0として、数式をどの辺りに表示するか
  3. length : 数式を矢印からどれだけ離して配置するか (正なら進行方向に向かって右、負なら左に配置)
  4. obj : 射の始点となる対象
  5. obj : 射の終点となる対象

返り値はgraphicsのlistです。

ツールボックスのフィールドは以上の4つです。

\diagramコマンドに引数として渡す関数は、渡されたツールボックスobj関数で数式をgraphics化し、そしてdraw-objdraw-arrで得られるgraphicsのlistを結合して返してやればOKなわけです。

2. 実際に描いてみよう

というわけで、まずは簡単な図をベタに書いてみましょう。

@require: stdja
@require: math
@require: cd

let testdiag toolbox =
  let a = toolbox#obj (40pt, 85pt) ${A} in
  let b = toolbox#obj (110pt, 85pt) ${B} in
  let c = toolbox#obj (40pt, 25pt) ${C} in
  let d = toolbox#obj (110pt, 25pt) ${D} in
  let objs = List.map toolbox#draw-obj [a; b; c; d] |> List.concat in
  let f = toolbox#draw-arr ${f} .45 -10pt a b in
  let g = toolbox#draw-arr ${g} .5 5pt a c in
  let h = toolbox#draw-arr ${h} .5 -10pt b d in
  let k = toolbox#draw-arr ${k} .45 10pt c d in
    List.concat [objs; f; g; h; k]

in
document (|
  title = {};
  author = {};
  show-title = false;
  show-toc =false;
|) '<
  +p{ \CD.diagram(150pt)(115pt)(testdiag); }
>

処理結果↓

f:id:youz:20181211001523p:plain
図1.

キレイにできましたね。

3. もうちょっと楽に描きたい

obj, draw-obj, draw-arrをいっぱい書かなきゃだったりdraw-objとdraw-arrの返すgraphicsのlistをまとめるのちょっとダルい…ダルくない? という事で、若干書くのが楽になる(かもしれない)ヘルパーコマンドを作ってみました。

let-inline ctx \cdutil width height objects edges =
  let diagf r =
    let sz = get-font-size ctx in
    let objs = List.map (fun (l, m, p) -> (l, r#obj p m)) objects in
    let objg = List.map (fun (_, o) -> r#draw-obj o) objs in
    let obj label =
      match List.assoc string-same label objs with
      | None    -> r#obj (0pt, 0pt) ${}
      | Some(o) -> o
    in
    let arrg = List.map (fun (s, e, m, t, l) ->
        r#draw-arr m t (0pt -' (sz *' l)) (obj s) (obj e)
      ) edges in
    List.append objg arrg |> List.concat
  in
    read-inline ctx {\CD.diagram(width)(height)(diagf);}

引数width heightは\CD.diagramにそのまま渡します。

objectsは対象を表すリストで、obj型データのリストではなく(string * math * point)のリストを指定します。obj型データにラベルとして文字列を加えた型になっています。

edgesは射のリストで、(string * string * math * float * float)のリストです。 内容は左から順に以下の通り

  1. string : 射の始点の対象を示すラベル文字列
  2. string : 射の終点の対象を示すラベル文字列
  3. math : 射に対応する数式 (draw-arrの第1引数)
  4. float : 矢印の始点を0.0 終点を1.0として、数式をどの辺りに表示するか (draw-arrの第2引数)
  5. float : 数式を矢印からどれだけ離して配置するか (draw-arrの第3引数をフォントサイズに対する倍率指定にした物)

コイツを使うと

  +p{
    \cdutil(305pt)(125pt)
    [(`A`, ${A}, (30pt, 90pt));
     (`B`, ${B}, (90pt, 90pt));
     (`C`, ${C}, (150pt, 90pt));
     (`D`, ${D}, (210pt, 90pt));
     (`E`, ${E}, (270pt, 90pt));
     (`A'`, ${A'}, (30pt, 30pt));
     (`B'`, ${B'}, (90pt, 30pt));
     (`C'`, ${C'}, (150pt, 30pt));
     (`D'`, ${D'}, (210pt, 30pt));
     (`E'`, ${E'}, (270pt, 30pt))
    ]
    [(`A`, `B`, ${f}, 0.4, 1.0);
     (`B`, `C`, ${g}, 0.4, 1.0);
     (`C`, `D`, ${h}, 0.4, 1.0);
     (`D`, `E`, ${j}, 0.4, 1.0);
     (`A`, `A'`, ${l}, 0.4, 1.0);
     (`B`, `B'`, ${m}, 0.4, 1.0);
     (`C`, `C'`, ${n}, 0.4, 1.0);
     (`D`, `D'`, ${p}, 0.4, 1.0);
     (`E`, `E'`, ${q}, 0.4, 1.0);
     (`A'`, `B'`, ${r}, 0.4, 0.-.1.);
     (`B'`, `C'`, ${s}, 0.4, 0.-.1.);
     (`C'`, `D'`, ${t}, 0.4, 0.-.1.);
     (`D'`, `E'`, ${u}, 0.4, 0.-.1.)
    ];
  }

で結果が

f:id:youz:20181211024707p:plain
図2

大きめの図式でも書きやすくなってるんじゃないでしょうか。

4. おまけ

objやdraw-arr関数に渡す数式について。 これ、ガワが数式(math型データ)なら何でも良いので、インラインテキストを引数に取る数式コマンド\textを使えば何でも詰め込めちゃうんですよね。

というわけで以下お遊び。

  +p{
    \cdutil(200pt)(90pt)
     [(`A`, ${A}, (180pt, 20pt));
      (`B`, ${B}, (30pt, 20pt))
     ]
     [(`A`, `B`, ${\text!{\insert-image(50pt)(`genbaneko.jpg`);}}, 0.7, 0.-.0.4)];
  }
  +p{
    \cdutil(200pt)(90pt)
     [(`neko`, ${\text!{\insert-image(80pt)(`genbaneko_.jpg`);}}, (150pt, 10pt));
      (`d`, ${}, (120pt, 50pt));
      (`logo`, ${\text!{\SATySFi;}}, (40pt, 50pt))
     ]
     [(`d`, `logo`, ${\text!{ヨシ!}}, 0.7, 0.-.1.)];
  }

f:id:youz:20181211025557p:plain
ヨシ!


以上cdパッケージの使い方の紹介でした。 今回書いたコードはこのGistにまとめておきましたので参考にしてみてください。

ご安全に!