⛄️

SATySFi v0.1.0 のリリース後、SLyDIFi がどう発展するかについて

2022/12/19に公開

はじめに

SLyDIFi は SATySFi でスライドを作成するためのクラスファイル(に相当するパッケージ)です。

https://github.com/monaqa/slydifi

Satyrographos 経由でインストールでき、SATySFi の文法さえ知っていれば、以下のようなマークアップで誰でも簡単にスライドを作ることができます。




マークアップ
slide.saty
% SLyDIFi と直接関係のないインポートや関数定義は省略
@require: class-slydifi/theme/arctic

document '<

  % +make-title でタイトル用のフレームを作れる
  +make-title(|
    title = {|\SLyDIFi; で|らくらくスライド作成|};
    author = {|monaqa|GitHub: \link(`https://github.com/monaqa`);|};
    date = {| 2021年6月6日 |};
  |);

  % +section で複数のフレームをセクションにまとめることができる
  +section{|セクションスライドの|具体例|}<

    % スライドのページ1枚(フレーム)は +frame で作る
    +frame{フレーム作成 in \SLyDIFi;}<
      +listing{
        * フレーム:スライド資料の1ページ1ページに値するもの
        * \SLyDIFi; では3種類のフレームを区別する
          ** 見出し:スライド全体の題目、発表者名などを載せるフレーム
          ** セクション見出し:セクションのタイトルを載せる
          ** 本文:通常のフレーム
      }
    >

    +frame{テキストの記述}<
      +p{以下のようなコマンドを用いてテキストを記述できる。}
      +listing{
        * `+p{}`: 段落
        * `+listing{}`: 番号のない箇条書き
        * `+enumerate{}`: 番号付きの箇条書き
      }
      +p{さらに、インラインテキストの中では以下のマークアップが使える。}
      +listing{
        * `\emph{}`: \emph{強調}
        * `\text-color(){}`: \text-color(Color.of-css `#42883B`){文字色変更}
      }
    >
  >

>

より詳しいコードと出力の例は以下を参照してください。

https://github.com/monaqa/slydifi/blob/master/example

SLyDIFi のインストール方法や使い方については去年のアドベントカレンダーで投稿した解説記事をご覧ください。

https://zenn.dev/monaqa/articles/2021-12-17-satysfi-slydifi-create-slide

世の中には様々な見た目のスライドがあります。仮に同一の内容を扱うスライドであっても、文字の書体や大きさ、配色などといったレイアウト(テーマ)によって大きく印象が変わるものです。自分の目的や好みに合ったテーマを選びたい、さらに既存のテーマをより自分好みにカスタマイズしたい、これらはスライドを作成するなら自然に生まれる要望といえるでしょう。

本記事では、SATySFi v0.1.0 のリリース後に SLyDIFi がどのような形でスライドテーマの枠組みを提供するか、その構想をまとめます。

SATySFi v0.0.x における SLyDIFi のテーマの枠組み

SATySFi の現行の最新バージョンは v0.0.8 であり、互換性のない大規模な変更である v0.1.0 が来年頃に正式リリース予定となっています。ここでは前者 (0.0.8) を「現行 SATySFi」、後者を(0.1.0)「新 SATySFi」と呼ぶこととしましょう。

実は、現行 SATySFi で動く SLyDIFi(現行 SLyDIFi)にもすでにスライドテーマを管理する枠組みが備わっています。実際、以下のディレクトリには現行 SATySFi でコンパイル可能ないくつかのスライド例があります。文書ファイル (*.saty) の中身はほぼ同一でも、インポートするテーマを変えることで出力結果 (*.pdf) のレイアウトが大きく異なることが分かります。

https://github.com/monaqa/slydifi/tree/master/example

現在は以下のような枠組みとなっています。

  • スライドテーマはパッケージとして提供する(テーマパッケージ)。
    • class-slydifi パッケージにデフォルトでいくつか入っている。たとえば class-slydifi/theme/plain パッケージを使えば plain テーマが使える。
    • 自分で作ることもできる。
  • 各テーマパッケージは設定項目 (config 型) を定義する。設定項目のフィールドはテーマにより異なる。
    • たとえば plain テーマであれば以下のような感じ。

      type plain-config = (|
        font-normal : context -> context;  % 通常のテキストのフォント設定
        font-emph : context -> context;    % 強調テキストのフォント設定
        color-bg : color;                  % 背景色
        ...
      |)
      
    • テーマパッケージは config 型のデフォルト値を提供する。

    • +set-config コマンド等を本文中で用いると、そのテーマを用いるユーザ自身の手で設定値を変更できる。

  • テーマパッケージはスライドのマークアップに必要となる以下のようなコマンドを提供する。
    • +make-title: タイトルスライド
    • +frame: 1枚のフレーム
    • \emph: 強調表示
    • その他、テーマにより追加コマンドを提供することもある

つまり、ユーザはテーマを選び、さらに各テーマ内の設定をいじることでカスタマイズできるようにしたのです。このような構造にすることで、以下の恩恵を受けることができます。

  • ライトユーザにとっては、複雑な設定をせずとも複数の選択肢からテーマを選べる。
  • ヘビーユーザにとっては、設定をカスタマイズすることで自分好みのレイアウトを実現しやすい。
  • 定義されているコマンドの名前や型を揃えることで、マークアップがある程度共通化される。
  • コマンドの拡張を許すことで、リッチなテーマと簡素なテーマを両立できる。

一方で、以下のような問題点があります。

  • 「各テーマがどのような関数やコマンドを提供する必要があるか、設定値はどのように渡す必要があるか」について、現状不文律となってしまっている。
    • スライドテーマは「自分で作ることもできる」と書いたが、従来のスライドテーマのシグニチャを真似て作る必要があり、面倒。
    • SLyDIFi のテーマを作成する上で何が必須の関数なのかすぐに判断できない。
    • テーマ作成者用にドキュメントを書け、というのはその通りだが、型システムで「実装しなければならないコマンド」などの条件を付けられると便利。
  • 各テーマの設定値 (config) を変更する方法が分かりにくい。
    • 実装の都合上、可変参照を用いて config を取り回すような形になっており、関数型言語のパラダイムと相性が良くない。

これらは現行 SLyDIFi では根本的な解決が難しい課題でした。

新 SLyDIFi のテーマの枠組み

上で述べた課題は、 v0.1.0 で大きく解決されることとなります。鍵は 新 SATySFi で新たに取り入れられることとなった f-ing modules というモジュールシステムにあります。

f-ing module の採用により、新 SATySFi ではファンクタが定義できるようになります。ファンクタとは「モジュールを受け取りモジュールを返す関数のようなもの」であり、「スライドテーマ」を抽象化するのに重要な役割を果たします。f-ing module を採用した SATySFi の具体的な解説は別の記事に譲りますので、SATySFi 作者の gfn さんによる以下のスライドなどを参照してください。

https://gfngfn.github.io/ja/posts/2022-09-25-slides-satysficonf2022/

新 SLyDIFi においても「ユーザはテーマを選び、さらに各テーマ内の設定をいじることでカスタマイズできるようにする」という方針は変わりません。「テーマ」や「設定」といった要素をより適切に抽象化できるようにした点がポイントです。

設定からスライド生成用のクラスファイルが作られるまでの、大まかな流れは以下の図のようになります。


大まかなモジュールや機能の流れ

テーマの枠組みを構成する3要素

SLyDIFi を用いてスライドを作成するとき、大きく分けて3種類の役割を持つモジュールやファイルが存在することになります。

  • Slydifi パッケージ: 全てのテーマの共通処理を記述するパッケージ
    • SLyDIFi 本体である class-slydifi パッケージに含まれています。
    • 以下の役割を果たします。
      • テーマシグネチャを定義。
      • テーマからクラスファイルを作成するファンクタを実装。
  • 各テーマ生成ファイル: SLyDIFi のテーマを記述するファイル・モジュール群
    • デフォルトのテーマは class-slydifi パッケージに含まれていますが、同じ要領で第三者が自由に定義できます。
    • 以下の役割を果たします。
      • テーマごとに設定シグネチャを定義。
      • デフォルトの設定を実装。
      • 設定からクラスファイルを作成するファンクタを実装。
  • 文書ファイル: テーマを用いて実際に文書を記述するファイル
    • 当然、文書を書くユーザが作成します。
    • 以下の役割を果たします。
      • テーマを選び、そのテーマの Config を実装(省略可能)。
      • クラスファイルの I/F に沿って文書を記述。

これらが果たすそれぞれの役割を見てみましょう。

Slydifi モジュール


Slydifi モジュールの責任範囲

Slydifi モジュールの重要な仕事は2つあり、1つは Theme シグネチャを定義することです。現時点で Theme シグネチャは以下のように定義されています。

signature Theme = sig
  type title-content :: o
  % レイアウト
  val layout: layout
  val init-ctxf: context -> context

  val frame-normal: frame (| title: inline-text, body: block-text |)
  val frame-title: frame title-content
  val \emph: inline [?(cond: condition) inline-text]
end

このコードの意味を日本語で述べるなら、以下のようになります。

  • 全てのテーマには title-content という型が必要。
    • title-content はタイトルフレームに載せるコンテンツを表す型。
      • たとえば通常のスライドなら「スライドのタイトル」「発表者名」「日付」を表示することが多いが、その場合は以下のようなレコード型がふさわしい。
        type title-content = (|
          title: inline-text,
          author: inline-text,
          date: inline-text,
        |)
        
      • もし学術発表用のテーマなら、「所属 (affiliation)」フィールドや「会議名」フィールドなどを追加することも考えられる。
  • 全てのテーマには layout という名前の値が必要。
    • layoutlayout 型を持ち、紙面サイズや本文のテキスト幅など、テーマに関わらず必須となる設定項目。

      type layout = (|
        paper-width: length,
        paper-height: length,
        text-width: length,
        text-height: length,
        text-horizontal-margin: length,
        text-vertical-margin: length,
      |)
      
  • 全てのテーマには frame-normalframe-title という frame 型の値が必要。
    • frame 型とはフレームを作成する方法を記述する関数型。
      • コンテンツとテキスト処理文脈が与えられたとき、「本文」と「背面に配置するグラフィックス」「前面に配置するグラフィックス」を返す関数。
      • frame-normal は通常のフレームを作成する方法を表す。
      • frame-title はタイトルフレームを作成する方法を表す。
  • 全てのテーマには \emph コマンドが必要。
    • インラインテキストを強調する方法を示すもの。

Slydifi モジュールが担うもう1つの重要な仕事は Slydifi.Make ファンクタです。これは Theme シグニチャを有するモジュールを受け取って、SLyDIFi のクラスファイル(に相当するモジュール)を返すものであり、以下のような構造となっています。

module Make = fun (T: Theme) -> struct
  % T で定義されたレイアウト設定などを用いて `document` 関数を定義
  val document bt = ...

  % T で定義された関数などを用いて `+frame` コマンドを定義
  val block +frame ?(n-layer = n-layer-opt) it-title bt-body = ...

  % T で定義された関数などを用いて `+make-title` コマンドを定義
  val block +make-title content = ...

  % T に入っているコマンド群が使えるようにする
  include T
end

Theme シグニチャを持つモジュール Tが与えられた下で、クラスファイルとして最低限必要な関数とコマンドを生成する方法を定義しています。T にはクラスファイルとして成立させるのに必要な document+frame といったコマンドが定義されていませんが、これらは T の情報を下に Slydifi.Make 内で生成してくれるため問題ないという塩梅です。
このように各テーマで共通の処理をまとめることで、テーマを開発する人は document 関数などを直接定義する必要がなく、テーマ本体のロジックに集中できるのです。

テーマファイル


各テーマの責任範囲

テーマファイルは、テーマの設定方法を定義し、実際にユーザが使えるようなクラスファイルを提供するところまでを責務としています。具体的には以下の役割を果たします。

  • Config シグニチャを定義
  • Config シグニチャを満たすモジュール(デフォルトの設定値)を定義
  • Make シグニチャを定義
    • 内部で Config から Theme モジュールを生成し、Slydifi.Make を用いてクラスファイルを生成

テーマファイルの実装の一つである Plain モジュールを例に挙げてみましょう。Plain モジュールのコードは以下のようになっています。

module Plain = struct

  signature Font = sig
    val normal: context -> context
    val emph: context -> context
    ...
  end
  signature Color = sig
    val fg: color
    val bg: color
  end
  signature Length = sig
    val margin-bot-frame-title: length
  end

  signature Config = sig
    module Font: Font
    module Color: Color
    module Length: Length
  end

  % デフォルトの設定値の定義
  module DefaultConfig :> Config = struct
    module Font = struct
      val normal = ...
      val emph = ...
    end

    module Color = struct
      val fg = Color.black
      val bg = Color.gray 0.8
    end

    module Length = struct
      val margin-bot-frame-title = 20pt
    end
  end

  module Make = fun (C: Config) -> struct
    module Theme = struct
      type title-content = (| title: inline-text |)
      val layout = (| ... |)
      val init-ctxf ctx = ctx |> C.Font.normal
      val frame-title ctx content = ...
      val frame-normal ctx content = ...
      val inline ctx \emph ?(cond = cond-opt) it = ...
    end

    include Slydifi.Make Theme

    % Plain モジュール固有のコマンド
    val block +frame-dummy it-title bt-body =
      let () = Slydifi.increment-page-num 2 in
      '<
        +genframe(1)(frame-normal)(|title = it-title, body = bt-body|);
      >
  end

  % デフォルト設定を用いて生成したクラスファイル
  module DefaultClass = Make DefaultConfig

end

ほとんど上で説明した通りの実装となっています。
なお、Config モジュールは内部で更に Font/Color/Length の3つのサブモジュールに分かれています。このように構造を細分化することで、ユーザにとって設定の見通しが良くなります。

文書ファイル


ユーザから見たテーマ

文書ファイルの仕事は「テーマファイルを選び、そのテーマファイルのマークアップに従って組版する」ことです。仰々しく書いていますが、やることは通常の文書ファイルと同様、クラスファイルを読み込み、定義されたコマンドで組版を行うだけです。

このとき、Plain テーマを用いるユーザは2通りの選択をすることができます。

  • ライトユーザ: Plain.DefaultClass をそのまま用いる。
    • モジュールを定義する必要が一切ないため、楽に書き始めることができる。
  • ヘビーユーザ: Plain.Config シグニチャを持つモジュールを生成し、Plain.Make で新しいクラスファイルを作る。
    • 決められたフィールドを埋めるだけで、自由度の高い設定ができる。
    • ユーザが行った設定により生成されるクラスファイルそのものが変わるため、可変参照などを使う必要もない。

現状と今後の展望

現在は grammar-v0_1_0 ブランチにて新 SLyDIFi の実装を進めています。ここで紹介したコードも、このブランチの実装から抜粋したものです。

https://github.com/monaqa/slydifi/tree/grammar-v0_1_0

すでに概念実証は済んでおり最低限のスライドは問題なく生成できたため、ひとまず上記の方針で実装を進めようと思います。

ただし今の所、それ以上の実装は手が止まっている状況です。というのも元々の SLyDIFi の実装が satysfi-base に強く依存しており、 v0.1.0 にはまだ satysfi-base の実装がないからです。satysfi-base の機能が揃うのを待ってから SLyDIFi の実装を進めたいと思います。

https://adventar.org/calendars/7515

12/9 bd_gfngfn
satysfi-baseをSATySFi v0.1.0に移植してみる

この記事が待ち遠しいですね!

終わりに

f-ing modules の話を最初に SATySFiConf で聞いたとき、なんて夢の広がる機能なんだ、と思いました。実際にSLyDIFi に適用してみて、確かな手応えを感じています。スライドテーマに限らず、細かいカスタマイズが求められる組版と全体的に相性が良いように思いました。

SATySFi v0.1.0 には他にも様々な機能が搭載されます。新機能をフルで使いこなせるように今後もキャッチアップを続けたいと思います。正式にリリースされた暁には、皆さんも是非新しい方の SATySFi と SLyDIFi を使ってみてください!

Discussion