webpackのローダの設定を混ぜるな

概要

小ネタです。webpackには、動作を拡張できる「ローダ」という仕組みがあるのですが、その動作を設定ファイル(webpack.config.js)で変更することができます。今回はここの書き方でハマったという話です。

対応バージョン

$ webpack --version
4.8.1

ローダでのオプション設定、2つの方法

css-loaderというローダがあります。これを使うと、複数のCSSファイルをまとめたり、JavaScriptの中でCSSのStyleを適用できたりします(CSS modules)。今回、このCSS modulesを利用している中でCSSのクラス名の命名について、kebab-caseからcamelCaseに変更したいと思った時にハマりました。

css-loaderをwebpackに組み込む最もシンプルなコードは以下です。

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['css-loader'],
      },
    ],
  },
};

これで、.cssで終わるファイルに関して、css-loaderが動作するようになります。これらの動作を変更するためのオプションが用意されているのだが、その書き方には大きく2種類あって、1つ目は以下のように、ローダ名文字列をoption属性を持つオブジェクトに置き換える方法です。

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: 'css-loader',
            options: {
              modules: true
            }
          }
        ]
      }
    ]
  }
};

そしてもう一つ、「インライン」方式があって、URLのクエリパラメータと同様の形式の文字列を与えることができます。上記と同様の内容をインライン形式で記述すると、以下のようになります。

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['css-loader?modules=true'],
      },
    ],
  },
};

2つの指定方法を混ぜてはいけない

本題に入ります。css-loaderにはlocalsConventionというオプションがあります。

少し説明します。(少なくとも個人的には)CSSファイルに記述するクラス名は通常kebab-caseで、JavaScriptでの属性名はcamelCaseで記述するのですが、css-loaderのデフォルトではCSSクラス名からJS属性名へ何の変換も行いません。したがって、どちらかのケースに統一する必要があります。でもCSSにcamelCaseが書いてあるのも、JSにkebab-caseが堂々と出てくるのもあまり好きではないので、名称の変換をしたいわけです。

今回、希望の動作を叶えるためには、localsConventiondashesを指定します。

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: 'css-loader?modules',
            options: {
              localsConvention: 'dashes'
            }
          }
        ]
      }
    ]
  }
};

しかしながら、実は上記の設定は正しく動作しません。実際には、該当するCSSクラスが全て効かなくなってしまいました。2つの方式を混ぜて書いてしまっているからです。しかもタチの悪いことに、ビルド時エラーにもなりません。

正しい指定方法

2つの方式は共存できないので、以下のどちらかで統一しましょう。 ちなみにインライン方式ではmodule=trueではなく、moduleになっています。オプション名だけだと=trueを付けたのと同じことになるみたいですね。初歩的な内容かもしれません。あんまり詳しくなくてごめんなさい。

オブジェクトでの指定

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: 'css-loader',
            options: {
              modules: true
              localsConvention: 'dashes'
            }
          }
        ]
      }
    ]
  }
};

インライン方式での指定

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['css-loader?modules&localsConvention=dashes'],
      },
    ],
  },
};

圏論シンポジウムに参加した

昨日、圏論シンポジウムに参加してきました。約100人の席がほぼ埋まってました。参加者として記名するタイミングで、そこに「大学名」「役職」という欄を見つけて、ああ、部外者(=研究者以外)向けではないんだな、と感じた(他意はなく、新鮮だっただけ)。

 

全体としては、30分のセッションが9つ。急遽予定を入れたので、最後の1つだけ聴くことができなかった。詳しいタイトルなどは以下のページを。

http://www.inter.ipc.i.u-tokyo.ac.jp/symposium.html

 

端的に、全体を通して、めっちゃおもしろかった。

https://twitter.com/ironoir/status/1220962339853238272?s=21

 

ざっくり言ってしまうと、物理学・生物学・哲学・美学・プログラミング・論理学などの分野が、主に分野ごとの圏の間の関手とみなすことができる、という例示の場所だったように思う。そこで、2つほど気になったところがある。

 

まず、それは他の代数的構造ではなく、圏でないといけないのか?という点。短い時間の発表ではわかるはずもないが、関手の「構造を保ったまま写す」特性を活かせているのか疑問に思った内容もあった(事実、その発表も最初は群を説明に使っていた)。もちろん、圏論からはあくまでもインスピレーションを受けただけで、精緻な形式化までは目指していないのかもしれない。専門家でないので偉そうなことは私も言えないし、「異文化交流のための共通言語」を目指すのであれば、相当柔軟な態度が求められるだろう。マサカリいくない。その点では、八面体定理の同じ図式が、全く異なる発表内で複数使われていたのはすごくいい演出だなと思った(と言いつつ、実は三角圏も八面体定理もそんなによくわかっていないけども)。

 

次に、数は少なかったが、発表の中に圏論の話がほぼ全く出なかった発表があったので、少しでいいので絡めてほしかった。絡みがないと「一見似てないものが似ている」程度のふわっとした話の印象になってしまって、もったいない気がしてしまった。

 

プログラマとしては、Haskellの話は楽しかったが、盛りだくさんだったので、消化するにはもうちょっと時間が必要かも(今後、資料をリファレンス的に使うのはとても便利そう)。あと、結び目の話は知ってる話だった(というか発表者の方の同内容のブログを読んでいた)ので、安心感があった。

 

というわけで、遠路をかけて行く価値のあるシンポジウムだった。分析哲学をかじったものとしては、分析美学の発表と、類推(比喩表現)に関する発表については突っ込んで考察したいところであるが、それは気が向けば次回に…

Python.hの場所を取得する

丸一年、ブログ書いてないですね。気にせず小ネタ。

Pythonには拡張モジュールというものがあって、CやC++などの言語でPythonのモジュールを作ることができます。

docs.python.org

そしてそのために必須なCヘッダとして、 Python.h がありますが、このヘッダのパスを取るためにはどうするか、という話です。

結論から言うとsysconfigモジュールを使います。 get_config_varsメソッドを使うと、Pythonの構成情報にアクセスすることができます。

import sysconfig
import pprint
pprint.pprint(sysconfig.get_config_vars())
{'ABIFLAGS': 'm',
 'AC_APPLE_UNIVERSAL_BUILD': 0,
 'AIX_GENUINE_CPLUSPLUS': 0,
 'ANDROID_API_LEVEL': 0,
 'AR': 'ar',
 'ARFLAGS': 'rc',
 'BASECFLAGS': '-Wno-unused-result -Wsign-compare -Wunreachable-code',
 'BASECPPFLAGS': '',
 'BASEMODLIBS': '',
(以下略)

私の環境ではこの結果が749行ありましたが、このうちPython.hへのパスを持っているのはINCLUDEPYCONFINCLUDEPYというキーの値でした。ちなみにそれぞれの違いはわかりません、どなたかご存知の方がいれば教えてください。

先ほどのsysconfig.get_config_varsには引数を与えることもできて、先ほどわかったキーを渡せば、無事にその値が取れます。

import sysconfig
sysconfig.get_config_vars('INCLUDEPY')

私の環境ではanyenvを入れていてPythonは3.6.6なので、 ${HOME}/.anyenv/envs/pyenv/versions/3.6.6/include/python3.6mが出ました。

ちなみに、python -m sysconfigと書けば、スクリプトとして上記の情報を取得できます。 シェルスクリプトなどでこの情報を使いたい時には、こっちの方が便利な時もあるかもです。

この情報を使って簡単なツールを作ろうとしているので、気が向いたらその話もします。

2018年を振り返る

普段はこういった、一年を振り返るような記録を残すことはない。ただ、今年は区切りをつけたい理由があるので書く。

2018年は長かった。一言で言えば、仕事に追われた一年だった。2017年に技術者として行き詰まりを感じて長期の休暇を取り、ひょんなことから、それまで無縁だったデータ分析業界に入ることになった。ただし、データサイエンティストとしてではない。会社唯一のソフトウェアエンジニアとして、データ分析用の新規プロダクトを開発するためだった。

元々、独自サービスの立ち上げはいつか挑んでみたいことではあった。また、個人事業主ができる範囲には(少なくとも私の能力では)限界があると感じていたので、会社員に戻るという不自由さを差し引いても、これからの時間を新規事業立ち上げに投資するのは悪くないという判断でもあった。

ただ、ある程度覚悟はしていたものの、なかなか環境としては大変だった。まず、ソフトウェアエンジニアが一人もいない職場は初めてだった。フリーランスでも確かに一人だが、それとはわけが違う。仕事上の判断をするためのキーパーソンが数人の役員レベルしかおらず、非技術者かつ普段の事業で時間が取れない。これがシステム開発を生業とする会社なら、たとえ現場の技術者ではなくとも、システムやプロダクトに関するざっくりした肌感覚を持っていることが多いのだが、弊社はそうではなく、システム開発に関してはかなり経験が少ない会社だった。プログラミングをする人間が多いと聞いていたので、その点は誤算だった。

入社して待っていたのは、オフショア開発のRailsアプリ、それからReact+Flask(Python)という2つのプロトタイプだった。しかし私からすれば、それは「コンセプトなしの実装」というプロダクト開発におけるアンチパターンだった。巷のデータ分析用ソフトウェアを真似ても、そんなものは売れない。だからコンセプトを決めて、要件定義をしたい。それなのに、社内でのコミュニケーションは(多忙などの理由で)ほぼ皆無であり、たまにある機会も、システム開発の基礎的な前提を伝えている間に消費され、肝心の「中身」が曖昧なまま時間が過ぎていった。

とはいえ、私もWebプログラマとしては経験が浅かったので、まずはRailsのソースを読み、PythonもReactも勉強しながら技術スタックを決定していく必要がある。特にフロントエンドは必要な知識が多すぎたので、やることはいくらでもあるので退屈はしなかった。それにしてもゴールがなかなか見えずに苦しかったが……。

問題は、人員が増えないにもかかわらず、弊社の戦略上2018年に一定の成果を出すことが求められていたこと。人員補充はずっと計画にはあったものの、全く増えない。必要な機能もコンセプトも技術も固まらない状態で思いついたのは、「人員がいつ来ても、機能がいつ決まっても、コンセプトがいつ決まってもなるべくダメージを下げる」ということ。

ここまで書いたところで、時間がないのでいったんあげます。自分の作戦がなんだったのか、というのは年明けすぐに続きを書こうと思う。みなさま、よいお年を。

Rustで圏論(6) 小圏・関手・自然変換

0, 1, 2, 3, 小圏

0(対象も射もない圏)、 1(対象が1つと恒等射のみの圏)、 2(対象が2つと、それぞれの恒等射・その間の1つの射からなる圏)、 3(対象が3つとそれぞれの恒等射、射も3つで1つの射が残り2つの射の合成である圏)

をそれぞれRustで表現していこうとしていたのですが、3をやろうとして、ちょっと面倒だな、と感じました。 0/1/2はそれぞれ別の構造体で、それぞれ別のトレイト実装を作っていたわけなのですが、3だと内容の割にコード量が増えたからです。 というわけで、3以外も表現できる一般的な圏を表す構造体を作ることにしました。

#[derive(Default)]
struct SmallCat<Object, Arrow> {
    objects: Vec<Object>,
    arrows: Vec<Arrow>,
}

この機会に、ジェネリックにして、対象も射も好きな型を与えられるようにしました。 こうしたのは、3を作っていて、対象や射に名前を与えたくなったからです。 今まで使っていたCategoryObject/CategoryArrowというUnitな構造体では情報が足りないので、 NamedCategoryObject/NamedCategoryArrowを作り、それをSmallCatに与えましょう。 ついでに型エイリアスも宣言します。

#[derive(PartialEq, Default)]
struct NamedCategoryObject(&'static str);

#[derive(PartialEq, Default)]
struct NamedCategoryArrow {
    name: &'static str,
    codomain: usize,
    domain: usize,
    equals: Vec<Vec<usize>>,
}

type SmallCat_ = SmallCat::<NamedCategoryObject, NamedCategoryArrow>;

NamedCategoryArrowは、射の名前以外にも、codomain/domain/equalsがいます。 codomain/domainがあるのは、初回に作った構造体(Category)と似ています。 equalsは、3で出てくる射の結合を表現するためのものです。

Categoryトレイトを実装します。

impl<O: PartialEq> Category for SmallCat<O, NamedCategoryArrow> {
    type Object = O;
    type Arrow = NamedCategoryArrow;

    fn domain(&self, f: &Self::Arrow) -> &Self::Object { &self.objects[f.domain] }
    fn codomain(&self, f: &Self::Arrow) -> &Self::Object { &self.objects[f.codomain] }
    fn identity(&self, o: &Self::Object) -> &Self::Arrow {
        let idx = self.objects.iter().position(|x| x == o).unwrap();
        for a in self.arrows.iter() {
            if a.codomain == idx && a.domain == idx {
                return a
            }
        }
        panic!("");
    }
    fn composition_internal<'a>(&'a self, f: &'a Self::Arrow, g: &'a Self::Arrow) -> &'a Self::Arrow {
        let f_idx = self.arrows.iter().position(|x| x == f).unwrap();
        let g_idx = self.arrows.iter().position(|x| x == g).unwrap();
        for arrow in self.arrows.iter() {
            for eq in arrow.equals.iter() {
                if eq[0] == f_idx && eq[1] == g_idx {
                    return arrow;
                }
            }
        }
        panic!("")
    }
    fn objects<'a>(&'a self) -> Box<Iterator<Item=&'a Self::Object> + 'a> {
        Box::new(self.objects.iter())
    }
    fn arrows<'a>(&'a self) -> Box<Iterator<Item=&'a Self::Arrow> + 'a> {
        Box::new(self.arrows.iter())
    }
}

対象や射を区別するのは、Vecのインデックスという仕様にしました。参照にうまくLifetimeを与えられませんでした。 射はNamedCategoryArrow固定ですが、対象はジェネリックに与えられます。

これを使って3を表現したいわけですが、表記が冗長になるので、以前作ったcatマクロを改良して使いましょう。

macro_rules! cat {
    ([ $( $object:ident )* ]
     [ $( $arrow_name:ident : $codomain:ident -> $domain:ident )* ]
     [ $( $f1:ident;$f2:ident = $f3:ident )* ]) => ({
        let mut objects = vec![];
        let mut arrows = vec![];

        $(
            objects.push(NamedCategoryObject(stringify!($object)));
            let idx = objects.iter().count() - 1;
            arrows.push(NamedCategoryArrow {
                name: stringify!($object), 
                codomain: idx, 
                domain: idx, 
                equals: vec![]
            });
        )*

        let mut equals_list = vec![];
        $( equals_list.push( (stringify!($f3),
                              vec![stringify!($f1), stringify!($f2)]) ); )*

        $(
            let cod_idx = objects.iter()
                                 .position(|x| x.0 == stringify!($codomain))
                                 .unwrap();
            let dom_idx = objects.iter()
                                 .position(|x| x.0 == stringify!($domain))
                                 .unwrap();
            let mut equals = vec![];

            for eqs in equals_list.iter() {
                if eqs.0 == stringify!($arrow_name) {
                    let before_idx = arrows.iter()
                                           .position(|x| x.name == eqs.1[0])
                                           .unwrap();
                    let after_idx = arrows.iter()
                                          .position(|x| x.name == eqs.1[1])
                                          .unwrap();
                    equals.push(vec![before_idx, after_idx]);
                }
            }
            arrows.push(NamedCategoryArrow {
                name: stringify!($arrow_name),
                codomain: cod_idx,
                domain: dom_idx,
                equals: equals,
            });
        )*
        SmallCat_ { objects, arrows }
    })
}

これを使えば、3は以下のように表現できます。 3以外の小圏も同様に定義できます。

    let _small_cat = cat!(
        [X Y Z]
        [f: X -> Y
         g: Y -> Z
         h: X -> Z]
        [f;g = h]
    );

関手・自然変換

続いて、関手と自然変換もトレイトを作ってみました。

trait Functor {
    type DOM: Category;
    type COD: Category;

    fn send_objects(object: <Self::DOM as Category>::Object) -> <Self::COD as Category>::Object;
    fn send_arrows(arrow: <Self::DOM as Category>::Arrow) -> <Self::COD as Category>::Arrow;
}

trait NaturalTransformation {
    type C: Category;
    type D: Category;
    type F: Functor<DOM=Self::C, COD=Self::D>;
    type G: Functor<DOM=Self::C, COD=Self::D>;

    fn components() -> Box<Iterator<Item=<Self::D as Category>::Arrow>>;
}

比較的、定義通りに書ける感じがします。 ただ、やはりそれぞれの満たすべき条件を書き表すことはできません。 トレイトの実装者が満たすことになりますね。 まだ何か物足りない気もしますが、具体的に作っていくことで見えてくるでしょう。

Rustで圏論(5) イチ圏がなかなか作れない

イチ圏

前回はゼロ圏(対象も射も持たない圏)をRustで表現してみました。 次は、1(対象が1つと、その恒等射のみを持つ圏)を定義してみようと思います。 少しダサいですが、勝手に「イチ圏」と表記します。

構造体を作る

#[derive(PartialEq)]
struct CategoryObject;

#[derive(PartialEq)]
struct CategoryArrow;

struct One {
    object: CategoryObject,
    arrow: CategoryArrow
}

ゼロ圏はZeroでしたので、イチ圏はOneにしてみました。 対象も射も1つずつしか持たないので、それらを直接フィールドとして定義しました。 また、CategoryObjectCategoryArrowが新しく登場しました。それぞれ、対象・射として振る舞うためだけの構造体です。 ゼロ圏の時はそれぞれ()で問題ありませんでしたが、今回はそういうわけにもいかないので、便宜的に作成しました。

トレイトの実装部分

関連型に上述のCategoryObjectCategoryArrowを使っています。

impl Category for One {
    type Object = CategoryObject;
    type Arrow = CategoryArrow;

    fn domain(&self, _f: &CategoryArrow) -> &CategoryObject { &self.object }
    fn codomain(&self, _f: &CategoryArrow) -> &CategoryObject { &self.object }
    fn identity(&self, _a: &CategoryObject) -> &CategoryArrow { &self.arrow }
    fn composition_internal(&self, _f: &CategoryArrow, _g: &CategoryArrow) -> &CategoryArrow {
        &self.arrow
    }
    fn objects<'a>(&'a self) -> Box<Iterator<Item=&'a CategoryObject> + 'a> {
        Box::new(once(&self.object))
    }
    fn arrows<'a>(&'a self) -> Box<Iterator<Item=&'a CategoryArrow> + 'a> {
        Box::new(once(&self.arrow))
    }
}

domain/codomain/identity/composition_internal

上記4つのメソッドは、実は前回から返り値を変更しました。 元々は値をそのまま返していたんですが、対象も射も所有権は圏にあると考えているので、参照を返すように変更しました。 ゼロ圏では()を返そうが&()を返そうが構わない(コンパイラに怒られない)ので見落としていました。 実装の中身は、それぞれ唯一の対象や射を返しているだけです。

objects/ arrows

この2つのメソッドの実装に思いの外、てこずりました。最初は以下の実装だったんです。

trait Category {
    ...
    fn objects(&self) -> Box<Iterator<Item=&CategoryObject>>
    ...
}

impl Category for One {
    ...
    fn objects(&self) -> Box<Iterator<Item=&CategoryObject>> {
        Box::new(once(&self.object))
    }
    ...
}

std::iter::onceを使って、唯一の対象をイテレータに入れて返しているつもりですが、このままだとコンパイラに以下のように怒られます。

error[E0495]: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
  --> src/main.rs:79:76
   |
79 |     fn objects(&self) -> Box<Iterator<Item=&Self::Object>> { Box::new(once(&self.object)) }
   |                                                                            ^^^^^^^^^^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the method body at 79:5...
  --> src/main.rs:79:5
   |
79 |     fn objects(&self) -> Box<Iterator<Item=&Self::Object>> { Box::new(once(&self.object)) }
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...so that reference does not outlive borrowed content
  --> src/main.rs:79:76

   |
79 |     fn objects(&self) -> Box<Iterator<Item=&Self::Object>> { Box::new(once(&self.object)) }
   |                                                                            ^^^^^^^^^^^^
   = note: but, the lifetime must be valid for the static lifetime...
   = note: ...so that the expression is assignable:
           expected std::boxed::Box<(dyn std::iter::Iterator<Item=&CategoryObject> + 'static)>
              found std::boxed::Box<dyn std::iter::Iterator<Item=&CategoryObject>>

出たなLifetimeと叫びたくなりますが、丁寧な解説がエラーメッセージに出てくるので、素直に読むことにします。 最後の部分が端的に間違いを指摘してくれています。

           expected std::boxed::Box<(dyn std::iter::Iterator<Item=&CategoryObject> + 'static)>
              found std::boxed::Box<dyn std::iter::Iterator<Item=&CategoryObject>>

メソッドの宣言では'staticが付いているのに、実際に返している値にはそれがない、ということのようです。しかし、自分ではそんなことをした覚えはない。

宣言を見直してみる

元々の宣言はLifetimeが省略されているのですが、ルール通りにこれを補えば

(省略アリ)fn objects(&self) -> Box<Iterator<Item=&CategoryObject>>
(省略ナシ)fn objects(&'a self) -> Box<Iterator<Item=&'a CategoryObject>>

となるはずです。どこにも'staticとか出てこないじゃんよ、なに言ってんだよrustc、という気持ちになったわけです。 しかししばらく経ってから気づいたのですが、この'staticは参照に付いているのではなく、Iteratorに付いているのです。 参照以外の値にLifetimeを設定することに慣れていなかったので、完全に盲点でした。

つまり、正しくLifetimeを補うと以下のようになります。 IteratorはデフォルトでstaticなLifetimeと見なされるということでした。

(省略アリ)fn objects(&self) -> Box<Iterator<Item=&CategoryObject>>
(省略ナシ)fn objects(&'a self) -> Box<Iterator<Item=&'a CategoryObject> + 'static>

振り返れば、ここでのBoxはトレイトオブジェクトのようなものだし、Lifetimeを気にしてもおかしくはない気もします。 ただし、それ自体は参照ではないので、暗黙のLifetimeは推論されない。 更に付け加えるなら、「参照は必ずLifetimeを持つ」のですが、「参照以外の値でもLifetimeを持つ」のは当たり前です。 参照は参照先の値と同じだけ生存するので。

コンパイルが通りました

というわけで、'staticだと見なされていた部分に明示的にLifetimeを指定して、無事にコンパイルが通るようになりました。

trait Category {
    ...
    fn objects<'a>(&'a self) -> Box<Iterator<Item=&'a CategoryObject> + 'a>
    ...
}

impl Category for One {
    ...
    fn objects<'a>(&'a self) -> Box<Iterator<Item=&'a CategoryObject> + 'a> {
        Box::new(once(&self.object))
    }
    ...
}

もう少し簡単だと思っていたんですけど、イチ圏を実装するだけで結構苦労している気がします。引き続き、少しずつ、がんばります。

Rustで圏論(4) ゼロ圏を作る

対象の族と射の族の型を変更した

今回定義している圏のトレイトCategoryの中には、対象のHashSet(C0)と射のHashSet(C1)を返すメソッドが定義されていました。今回、思うところもあってHashSetからIteratorに変更しました。気分の問題といえばそこまでですが、C0もC1も集合になるとは限らないので。Iteratorであれば、色々便利そうだな、という目論見も含めての変更です。

トレイト内でimpl traitは使えない

impl traitが安定化してから初めてコードを書いているので、嬉しくてさっそく、圏のトレイトの定義に使ってみようとしたのですが、inherentな関数じゃないと使えません、とコンパイラに怒られてしまいました。しかたなく、今まで通りBox<Iterator<Item>>にすることで落ち着きました。

trait Category {
    type Object: PartialEq;
    type Arrow: PartialEq;

    fn domain(&self, f: &Self::Arrow) -> Self::Object;
    fn codomain(&self, f: &Self::Arrow) -> Self::Object;
    fn identity(&self, a: Self::Object) -> Self::Arrow;
    fn composition(&self, f: Self::Arrow, g: Self::Arrow) -> Option<Self::Arrow> {
        if self.codomain(&f) != self.domain(&g) {
            None
        } else {
            Some(self.composition_internal(&f, &g))
        }
    }
    fn composition_internal(&self, f: &Self::Arrow, g: &Self::Arrow) -> Self::Arrow;
    fn ci(&self, f: &Self::Arrow, g: &Self::Arrow) -> Self::Arrow {
        self.composition_internal(f, g)
    }

    fn objects() -> Box<Iterator<Item=Self::Object>>;
    fn arrows() -> Box<Iterator<Item=Self::Arrow>>;

    fn associativity(&self, f: Self::Arrow, g: Self::Arrow, k: Self::Arrow) -> bool {
        self.ci(&self.ci(&f, &g), &k) == self.ci(&f, &self.ci(&g, &k))
    }
    fn unit_law_domain(&self, o: Self::Object, f: Self::Arrow) -> bool {
        self.ci(&self.identity(o), &f) == f
    }
    fn unit_law_codmain(&self, o: Self::Object, f: Self::Arrow) -> bool {
        self.ci(&f, &self.identity(o)) == f
    }
}

まずはゼロ圏を作ってみる

「ゼロ圏」という呼び方が正しいのか自信がないです(書籍にはローマ数字の0がそのまま記号として使われている印象)。ともかく、implするのが簡単そうなので、ここから始めてみることにします。

impl Category for Zero {
    type Object = ();
    type Arrow = ();

    fn domain(&self, _f: &()) {}
    fn codomain(&self, _f: &()) {}
    fn identity(&self, _a: ()) {}
    fn composition_internal(&self, _f: &(), _g: &()) {}
    fn objects() -> Box<Iterator<Item=()>> { Box::new(empty()) }
    fn arrows() -> Box<Iterator<Item=()>> { Box::new(empty()) }
}

std::iter::emptyを初めて使ってみました。空のイテレータって中身がないのでこう書けるのは煩雑にならなくて便利ですね。ObjectArrow()にしたのは、実装の見た目がスッキリするから以上の理由はありません。Unit自体はぜろというより1(対象が1つと恒等射のみからなる圏)ですけどね、イメージとしては。まあいっか。