【Common Lispと数学】
圏論的設計を考える
(パイプ指向とラムダ計算)

  • 圏論は「ものの変換」に注目する数学で、Functor・Monoid・Monadという概念でmap・reduce・Promiseといった設計パターンを統一的に説明できる
  • 手続き型やOOPは状態管理の複雑さを抱えるが、関数型設計はIOを端に寄せて純粋な変換関数だけを合成することでテストを容易にする
  • 圏論的設計ではパイプラインを値として持つことで「どう組み立てるか」と「いつ実行するか」が分離され、部品の差し替えや拡張が既存コードを変えずにできる
  • さらに自由モナド的設計ではIOの記述自体をデータ化し、本番・テスト・ロギングをインタープリタの切り替えだけで実現できる

関連記事

1. 数学は「思考の道具」でもある

数学の問題を解くためだけの道具ではありません。

1. 数学は「思考の道具」でもある

たとえば、数学で文字式の x を習うと、未知のものをいったん記号として扱えるようになります。
集合や命題で  や  を習うと、「すべてに当てはまる」のか「少なくとも一つ存在する」のかを区別できるようになります。

数学の言葉は、日常生活でも曖昧になりがちな話を厳密に考えるために使えます。

圏論の用語も同じです。
Functor(関手)、Monoid(モノイド)、Monad(モナド)といった言葉を知ると、自然言語では曖昧になりがちな違いを、よりはっきり区別できるようになります。

1.1. 圏論(category theory)とは?

圏論(category theory)」は、1945年に数学者 Samuel Eilenberg と Saunders Mac Lane が代数的位相幾何の研究から生み出した理論1です。

1.1. 圏論(category theory)とは?

「圏(Category)」とは、隣あったエッジの合成が定義されているマルチエッジ有向グラフのことです。

  • ノード(頂点)をオブジェクト(object)、有向エッジを射(arrow、あるいはmorphism)という
  • 各オブジェクト X には恒等射(identity)と呼ばれる自分自身への射 1ₓ (恒等射)があり、
  • また、射の合成規則は結合則を満たす(隣り合った射 f と g の合成を g・f と書き、(h・g)・f = h・(g・f))

集合論では、集合内の要素を中心に考えましたが、圏論では、対象のあいだの変換操作(射)を中心に考えます。
合成(composition)は、射をつなぐ操作2、つまりプログラムも合成の一つです。

出発点は「異なる数学の分野に、同じ形の対応が繰り返し現れる」という観察3で、中心にある問いはシンプルです。
「ものそのものより、ものの間の変換に注目したら、何が見えるか?」
とくに、代数的位相幾何学で使われていた「自然な変換」とは何か、を厳密に扱うためのツールとして始まりました。

1.2. プログラミングではどう役に立つの?

プログラミングにおける圏論の有用性は、ちょっと「デザインパターン」に似ています。

1.2. プログラミングではどう役に立つの?

圏論の用語である、Functor(関手), Monoid(単系), Monad(単子) という名前と法則を知っていると、関数設計の判断が言語化しやすくなります。
たとえば、mapreduce・Promiseの.then()・パイプラインといった別々の設計テクニックが、Functor・Monoid・Monadという3つの構造で整理でき、バラバラに見えていたものが同じ形として見えるようになるのです。

圏論と親和性の高いプログラミング言語としては、型クラスで Functor・Monad などを直接表現できる Haskell や、圏論の命題を証明として記述できる Coq があります。
ただ、Common Lisp でも、型システムによる保証はありませんが、合成・持ち上げ・IO の分離という圏論的な設計意図は活かせます。

たとえば、数学で文字式の x を習ったり、集合や命題の∀ や ∃ を習うと、数学の問題だけでなく、日常生活でも厳密に考えるために使えます。
圏論の用語も同様です。
自然言語だと、曖昧になりがちな違いをはっきり区別できるようになります。

1.3. ドーナツとコーヒーカップの共通性(同相と同型)

位相幾何学(topology)では、たとえば「ドーナツとコーヒーカップには共通性」がある、と説明されることがあります。
これは、「穴のある理想化された形」として同一視する話です。

1.3. ドーナツとコーヒーカップの共通性(同相と同型)

トポロジーでは、長さや角度など細かな量ではなく、連続変形しても変わらない性質に着目して考えます。
このときに重要なのが「同相(homeomorphism)」で、これは、ある形を別の形へ連続的に変形でき、しかもその対応を連続的に元に戻せる、という関係です。

圏論では、このような関係を図形以外にも広げて考えます。
トポロジーでの「同相」の話は、圏の言葉で言い換えることができます。
位相空間を「対象」、連続写像を「射」とする圏 Top を考えると、「コーヒーカップとドーナツは、位相空間の圏 Top において 同型(isomorphism)である」と言えるからです。

1.4. 変換問題を考える有効な視点

圏論での「同型」は、日常的に言えば、「互いに連続的に変換でき、しかもその対応を連続的に戻せる」、ということです。

この考え方を単語に当てはめると、たとえば「アナグラム」も似た形で考えられます。
アナグラムは、文字列の文字を並べ替える操作です。
たとえば、listen と silent は、同じ文字を使っていて、文字の順序だけが違います。
「並べ替え」を射として考えるなら、元に戻す並べ替えもあります。

つまり、同じ長さの文字列を対象、文字の置換を射とする圏では、「listenとsilentは同型である」と言えます。

プログラミングでは、暗号化や可逆的なデータ圧縮も、「同型」に近い考え方で捉えられます。
暗号化には復号化があり、可逆圧縮には展開があります。
つまり、あるデータを別の形に変換しても、対応する逆向きの操作によって元に戻せる場合、その変換は圏論的には同型として整理できます。

2. 【問題】データベース的な処理

同じ問題を4つの設計スタイルで解きながら、「どこで読むか」「内部でどう持つか」「どこで出力するか」という差を見ていきます。

入力:
1行目 ユーザー数 N(整数)
2〜N+1行目 名前と年齢をスペース区切りで1行ずつ
出力:
18歳以上の名前をカンマとスペースで区切って1行

入力例:        出力例:
4               Alice, Carol
Alice 30
Bob 15
Carol 22
Dave 17

2.1. どのように変わるのか?

状態入出力の扱い合成の見え方変換の扱い
手続き型明示的に変わるロジックと混在ループの中に埋まる処理ステップ
OOPスロットに隠蔽オブジェクト生成と結合メソッド呼び出しの連鎖型に紐付いた操作
関数型なしread-users / format t で分離ネストまたはパイプ第一級の値
UNIXパイプなしstdin / stdout が共通型| で一直線小さなコマンドの連鎖
圏論的なしread-input / write-outputパイプラインとして組み立て組み立て可能な部品
自由モナド的なしインタープリタが注入プログラムをデータとして構築記述と実行の完全分離

3. 状態を更新していく考え方

3. 状態を更新していく考え方

3.1. a. 手続き型の解

もっともシンプルに考えると、読み込み・判定・蓄積・出力をすべて1つのループに書けばよいことになります。

(defun solve/proc ()
  (let* ((n      (parse-integer (read-line)))
         (result '()))
    (dotimes (_ n)
      (let* ((line  (read-line))
             (space (position #\Space line))
             (name  (subseq line 0 space))
             (age   (parse-integer (subseq line (1+ space)))))
        (when (>= age 18)
          (push name result))))
    (format t "~{~a~^, ~}~%" (nreverse result))))

(solve/proc)Code language: Lisp (lisp)

result という変数は、 push のたびに変わります。
また、繰り返し処理の中に、読み込みと判定と蓄積が混在しているため、動作を読むには、処理の途中でどんな状態にあるかを追う必要があります。
とくに、バグを再現するには、その時点の result の内容と、何行目まで読んだかを両方再現しなければいけません。

3.2. b. オブジェクト指向の解

オブジェクト指向では、データとその処理を「オブジェクト」としてまとめ、クラスとメソッドで責務を分けます4

(defclass user ()
  ((name :initarg :name :reader user-name)
   (age  :initarg :age  :reader user-age)))

(defgeneric adult-p (entity))
(defmethod adult-p ((u user))
  (>= (user-age u) 18))

;;; 読み込みとオブジェクト生成が結合している
(defun read-user ()
  (let* ((line  (read-line))
         (space (position #\Space line)))
    (make-instance 'user
      :name (subseq line 0 space)
      :age  (parse-integer (subseq line (1+ space))))))

(defun solve/oop ()
  (let* ((n     (parse-integer (read-line)))
         (users (loop repeat n collect (read-user))))
    (format t "~{~a~^, ~}~%"
            (mapcar #'user-name
                    (remove-if-not #'adult-p users)))))

(solve/oop)Code language: Lisp (lisp)

adult-puser-name は責務として分離できました。
ただし、 read-user が読み込みとオブジェクト生成を一体化しているため、テストするには標準入力を用意しなければいけません。
「フィルタしてから名前を取ってからフォーマットする」という合成の構造も、solve/oop の中に埋まったままです。

3.3. バグになる状態を探す大変さ

a と b は、見た目が違っても同じ問いを抱えています。
「状態をどう管理するか」です。

手続き型では result という変数が push のたびに変わります。
オブジェクト指向ではスロットに隠蔽されましたが、状態そのものは残ります。
つまり、solve/oop が正しく動くかどうかは、user オブジェクトが「正しい状態にある」ことが前提です。

この「状態」があると、バグの再現が難しくなります。
ある時点の状態を再現しないと、問題が起きた条件を再現できないからです。
同じ関数でも、呼ばれる順序によって結果が変わります。

4. 状態を持たない「関数」を組み合わせる

そこで、「時間の中で変化する状態をどう整理するか」から「状態なしでどう処理をつなぐか」への発想の転換が生まれます。

4. 状態を持たない「関数」を組み合わせる

状態を管理するのではなく、状態を持たない関数を合成して、データを一方向に流す、という設計です。

純粋な「関数」は、「同じ入力には必ず同じ出力を返す」という性質を持ちます。
これを参照透明性(referential transparency)と言います。

計算機工学で生まれた「つなぐ」という発想を、圏論は、形式化する理論です。

4.1. c. 関数型の解

変換を小さい関数に分けて、直接合成する設計を考えます5

;;; 純粋な関数(IO なし)
(defun parse-user (line)
  (let* ((space (position #\Space line))
         (name  (subseq line 0 space))
         (age   (parse-integer (subseq line (1+ space)))))
    (list :name name :age age)))

(defun adult-p (user)
  (>= (getf user :age) 18))

(defun user-name (user)
  (getf user :name))

;;; IO(端に寄せる)
(defun read-users ()
  (let ((n (parse-integer (read-line))))
    (loop repeat n collect (parse-user (read-line)))))

(defun solve/fp ()
  (format t "~{~a~^, ~}~%"
          (mapcar #'user-name
                  (remove-if-not #'adult-p (read-users)))))

(solve/fp)Code language: Lisp (lisp)

たとえば、parse-user は IO を持たない純粋な関数です。
文字列を渡せばテストできます。

;; テストが簡単
(parse-user "Alice 30")  ;; => (:NAME "Alice" :AGE 30)
(adult-p '(:name "Bob" :age 15))  ;; => NILCode language: Lisp (lisp)

ただし、solve/fp の処理の流れはまだ見えにくいです。
というのも、関数が入れ子構造(ネスト)になっているので、内側から外側へと読む形になっているからです。

4.2. c’. パイプ指向とUNIX哲学

小さな処理を組み合わせる、という発想は、1970年代のUNIXでも同じ結論に到達していました6
それが「パイプ(pipe)」です。

awk 'NR==1{next} $2>=18{print $1}' | paste -sd ', ' -Code language: Bash (bash)

Douglas McIlroyの設計哲学は、2点に集約されます。
「一つのことをうまくやる小さなプログラムを作れ」と「出力が別のプログラムの入力になることを前提にせよ」です7

これは、工学的な設計原則で、関数型プログラミングの理論とは独立して生まれた。
しかし、共通するのは、大きなデータ構造(オブジェクト、データベース)を共有するのではなく、「テキストストリーム」という最小限の共通インターフェースを使い回したことです。

圏論の言葉を使えば、テキストストリームは対象(object)で、各コマンドは射(morphism)です。

awk は、名前と年齢の詳細を知りません。
paste は、年齢の意味を知りません。
それでも合成できる。

射の出力型と入力型が合えば合成できる、という性質をUNIXは実践しています。
関数型プログラミングでは、これを「関数合成」として形式化し、圏論がさらに「合成できるもの一般」として抽象化します。
つまり、ここに数学と計算機工学の落ち合うポイントがあるわけです。

5. 順序の管理から構造の保持へ

関数型には、まだ一つ問いが残っています。
それは、「関数の合成順序をどう管理するか」です。

5. 順序の管理から構造の保持へ

手続き型では状態の管理がやっかいでした。
関数型はその問いを手放しましたが、代わりに「どの関数をどの順序で合成するか」という管理が必要になります。

solve/fp の中のネストを見ると、処理の順序が内から外に読む形で埋まっています。
関数の数が増えるほど、その順序の見通しは悪くなります。

圏論的には、この問いを構造として取り出します。
「どう合成するか」をラムダとして先に値に落としておき、適用は後回しにします。

;; c: 呼ぶたびに「フィルタ→名前→フォーマット」という手順が走る
(defun solve/fp () ...)

;; d: 「フィルタ→名前」という構造を値として持つ
(defparameter *adult-name-pipeline* (pipe *filter-adults* *extract-names*))
;; 適用はいつでも、どんなデータに対しても
(funcall *adult-name-pipeline* data)Code language: Lisp (lisp)

パイプラインが値として存在するということは、「どう組み立てるか」と「いつ実行するか」が切り離されています。

もとの対象 A から求める答え C へ至る変換では、「どの道を通ってもよい」のです。
圏論で言えば、可換図式が成り立つなら A から C へ行く経路が複数あっても、結果は同じになります。

5.1. d. 圏論的な解

「圏論」は、複雑なプログラムの問題を解くのに役立ちます。
問題の構造を「部品」に分解し、それを「つなぐ」というときに、「射が合成できるか」という問いを持つことで、テストしやすく、差し替えやすい関数設計にできます。

圏論的な設計では、関数を合成する順序管理を「パイプラインという値」に閉じ込めることで、使う側は組み立て済みの構造を渡すだけにします8

;;; 合成の道具
(defun pipe (&rest fns)
  (lambda (x)
    (reduce (lambda (v f) (funcall f v)) fns :initial-value x)))

(defun map-with (f)
  (lambda (xs) (mapcar f xs)))

(defun filter-by (pred)
  (lambda (xs) (remove-if-not pred xs)))

;;; 純粋な変換部品(IO なし)
(defparameter *filter-adults*
  (filter-by (lambda (u) (>= (getf u :age) 18))))

(defparameter *extract-names*
  (map-with (lambda (u) (getf u :name))))

;;; パイプライン(IO を含まない変換の構造)
(defparameter *adult-name-pipeline*
  (pipe *filter-adults* *extract-names*))

;;; IO は端に寄せる
(defun parse-user (line)
  (let* ((space (position #\Space line))
         (name  (subseq line 0 space))
         (age   (parse-integer (subseq line (1+ space)))))
    (list :name name :age age)))

(defun read-input ()
  (let ((n (parse-integer (read-line))))
    (loop repeat n collect (parse-user (read-line)))))

(defun write-output (names)
  (format t "~{~a~^, ~}~%" names))

;;; 実行(IO → 変換 → IO)
(defun solve/categorical ()
  (write-output (funcall *adult-name-pipeline* 
                         (read-input))))

(solve/categorical)Code language: Lisp (lisp)

*adult-name-pipeline* は IO を一切持ちません。
read-inputwrite-output が外側にあり、中央の変換は純粋なデータの流れです。

5.2. 「持ち上げる」とは何か(Functor)

map-with が行っていることを図で見ると、こうなります。

5.2. 「持ち上げる」とは何か(Functor)
単体への変換:  User  ──────────────►  Name
               Alice                  "Alice"

                          ↕  map-with が持ち上げる

リストへの変換:[User] ─────────────►  [Name]
               [Alice, Bob, Carol]     ["Alice", "Bob", "Carol"]Code language: CSS (css)

一人への変換と、リスト全体への変換が、同じ形で平行しています。
これが Functor(関手)の直感9です。
「中身を変換する関数を、箱ごと変換する関数に育てる」と読んでいいでしょう。

map-with が Functor 的なのは、「単体への変換をリスト全体への変換に持ち上げる」からです10
(lambda (u) (getf u :name)) は一人のユーザーへの変換で、map-with がそれをリスト全体への変換に変えます。
圏論はこの「持ち上げ」という操作を形式化しています。

5.3. pipe と結合法則(Monoid)

高校数学で合成関数 g(f(x)) を習います11
(pipe f g h) は、それを左から右の順に書いたものです。

5.3. pipe と結合法則(Monoid)
数学での書き方:  h(g(f(x)))     (内側から外側へ読む)
pipe での書き方: (pipe f g h)   (左から右へ読む)

圏論の「射の合成」は、この「つなげられる」という性質を出発点にしています。

pipe でつないだ関数は、どこで区切っても最終結果が変わりません。

;; どちらも同じ変換になる
(pipe (pipe *filter-adults* *extract-names*) another-step)
(pipe *filter-adults* (pipe *extract-names* another-step))Code language: Lisp (lisp)

これはモノイドの結合法則 (a + b) + c = a + (b + c) と同じ構造です。
モノイド(Monoid)とは、結合的な二項演算と単位元を持つ代数的構造です12
さらに、何もしない恒等関数 (lambda (x) x) が単位元に対応します。

(pipe *filter-adults* (lambda (x) x))  ; *filter-adults* と同じ
(pipe (lambda (x) x) *filter-adults*)  ; *filter-adults* と同じCode language: Lisp (lisp)

圏論が「合成できるもの」に注目するのは、結合法則と単位元の存在が、どこで区切っても全体の構造を保証するからです。

6. IOもデータにできる

6. IOもデータにできる

6.1. e. 自由モナド的設計

さらに、一歩進んでIO を含む操作の記述ごとデータにしてしまう設計も考えられます。

ここで示すのは厳密な意味での自由モナドそのものではありませんが、IO をすぐに実行せず、まず『何をしたいか』を命令列としてデータ化し、その意味を後からインタープリタで与える、という考え方です。

read-inputwrite-output を「どう実行するか」をインタープリタとして後から差し込みます。

;;; 操作の記述(実行ではない。ただのデータ)
(defun adult-p (u) (>= (getf u :age) 18))

(defparameter *adult-name-program*
  '((:read-input)
    (:filter  adult-p)   ; 何でフィルタするか
    (:extract :name)     ; 何を取り出すか
    (:write-output)))    ; どこへ出すか(まだ決まっていない)

;;; インタープリタ1:本番(stdin / stdout)
(defun run/production (program)
  (reduce (lambda (data op)
            (ecase (first op)
              (:read-input   (read-input))
              (:filter       (remove-if-not (symbol-function (second op)) data))
              (:extract      (mapcar (lambda (x) (getf x (second op))) data))
              (:write-output (write-output data) data)))
          program
          :initial-value nil))

;;; インタープリタ2:テスト(IO なし)
(defun make-test-runner (test-data)
  (lambda (program)
    (let (output)
      (reduce (lambda (data op)
                (ecase (first op)
                  (:read-input   test-data)
                  (:filter       (remove-if-not (symbol-function (second op)) data))
                  (:extract      (mapcar (lambda (x) (getf x (second op))) data))
                  (:write-output (setf output data) data)))
              program
              :initial-value nil)
      output)))

;;; インタープリタ3:ロギング付き本番
(defun run/logging (program)
  (reduce (lambda (data op)
            (let ((result
                    (ecase (first op)
                      (:read-input   (read-input))
                      (:filter       (remove-if-not (symbol-function (second op)) data))
                      (:extract      (mapcar (lambda (x) (getf x (second op))) data))
                      (:write-output (write-output data) data))))
              (format *error-output* "[LOG] ~a => ~a~%" (first op) result)
              result))
          program
          :initial-value nil))Code language: Lisp (lisp)

*adult-name-program* はただのリストです。
:read-input が標準入力を意味するのか、テストデータを意味するのかは、インタープリタが決めます。

;;; 本番(stdin から読んで stdout へ)
(run/production *adult-name-program*)

;;; テスト(IO なし。同じプログラムをそのまま渡す)
(funcall (make-test-runner
           '((:name "Eve" :age 25)
             (:name "Frank" :age 16)
             (:name "Grace" :age 19)))
         *adult-name-program*)
;; => ("Eve" "Grace")

;;; ロギング付き本番(同じプログラムをそのまま渡す)
(run/logging *adult-name-program*)Code language: Lisp (lisp)

ここでの違いは、 :read-input:write-output はプログラムの記述の一部で、「実際に何をするか」はインタープリタが注入していることです。

たとえば、Tauri や Electron などのGUIプログラムを作るなら、「ファイルを読む」「設定を保存する」「通知を出す」という記述はプログラムとして書いておき、本番では Tauri のコマンドを呼ぶインタープリタ、テストではメモリ上で動かすインタープリタを差し込む、という設計がそのまま対応します。

つまり、「IO の意味まで値に閉じ込めた」段階の設計も可能なのです。

6.2. 圏論的設計の核心

*adult-name-pipeline* は IO を含まない純粋な変換構造です。
read-inputwrite-output を変えることなく、パイプラインだけ差し替えられます。
また、IO を挟まずにテストデータを直接渡せます。

;; テストは IO なしで完結する
(let ((test-data '((:name "Eve" :age 25)
                   (:name "Frank" :age 16)
                   (:name "Grace" :age 19))))
  (funcall *adult-name-pipeline* test-data))
;; => ("Eve" "Grace")Code language: Lisp (lisp)

段階を追加するときも、既存の部品は変えません。

;; 名前に件数を付けて出力するパイプラインに拡張する
(defparameter *adult-name-with-count*
  (pipe *filter-adults*
        *extract-names*
        (lambda (names)
          (cons (format nil "~d件" (length names)) names))))

(write-output (funcall *adult-name-with-count* (read-input)))
;; => 2件, Alice, CarolCode language: Lisp (lisp)

変換を組み立てる順序が、読む順序と一致しています。
各変換は独立してテストでき、差し替えられます。
AIに「フィルタ条件を変えてください」と指示するとき、*filter-adults* だけ指せばよくなります。

6.3. Functor, Monoid, Monad

圏論での構造を表す言葉は、関数型設計で使えます。

  • Functor(関手)は「中身を変換する関数を、箱ごと変換する関数に育てる」構造に名前をつけたものです。
    mapcar でやっていることがそれですが、「同じパターンがリスト以外にも現れる」と気づいたときに、その共通構造を指す言葉として使えます。
  • Monoid(モノイド)は「結合できて、単位元がある」構造の名前です。
    pipe がどこで区切っても同じ結果になる理由がこれです。
    セグメント木や累積処理も同じ構造として見えます。
  • Monad(モナド)は「失敗・非同期・副作用といった文脈つきの計算を、順番につなぐ」構造の名前です。
    Promiseの.then()でやっていることと、エラーハンドリングの連鎖と、状態の引き回しが、実は同じ形をしていると見えるようになります。
  1. 元論文は “General Theory of Natural Equivalences”(1945)として Transactions of the American Mathematical Society に掲載されました。圏(category)、関手(functor)、自然変換(natural transformation)の概念がこの論文で初めて体系的に導入されています。 – General Theory of Natural Equivalences – nLab
  2. 型をプログラムの命題、プログラムをその証明と対応させる「Curry-Howard対応」をさらに圏論に拡張したものが「Curry-Howard-Lambek対応」です。型=圏の対象、プログラム=射、という対応はこの理論的背景に基づいています。 – Curry-Howard-Lambek correspondence – John D. Cook
  3. 具体的には代数的位相幾何(algebraic topology)における、位相空間に代数的構造(群など)を対応させる操作が繰り返し現れることがきっかけでした。Eilenbergらは1942年の “Group Extensions and Homology” の時点でこの発想を使っており、1945年論文はその抽象化です。 – Category Theory – Stanford Encyclopedia of Philosophy
  4. CLOSはCommon Lisp Object Systemの略で、1980年代後半にANSI標準化の一環として設計されました。多重ディスパッチと多重継承を持ち、クラス定義とメソッド定義が分離しているという点でC++やJavaのOOPシステムとは設計思想が大きく異なります。 – CLOS – Common Lisp Cookbook
  5. 関数を第一級オブジェクトとして扱う(他のデータと同様に渡したり返したりできる)という性質は、Alonzo Churchが1930年代に開発したラムダ計算を理論的な基盤としています。Common Lisp、Scheme、Haskell、OCamlなどの関数型言語はこの系譜に連なります。 – Lambda Calculus: The Bedrock of Functional Programming – Medium
  6. UNIXパイプを最初に提案したのはBell LabsのDouglas McIlroyで、1964年に「プログラムをホースのようにつなげるべき」というメモを書き残しています。実際にパイプがUNIXに実装されたのは1973年で、Ken Thompsonが一晩でpipe()システムコールとシェル統合を実装しました。 – Pipeline (Unix) – Wikipedia
  7. McIlroyはUNIX哲学として「プログラムは一つのことをうまくやれ。出力が別のプログラムへの入力になることを前提にせよ」と1978年のBell System Technical Journalに記しています。 – Unix Philosophy – Klara Systems
  8. 関数を第一級の値として扱い、変換を組み合わせて大きな処理を構成するこのスタイルは、関数型プログラミングの実践と圏論の設計思想が最も近づく点です。HaskellのFunctor・Applicative・Monadといった型クラスも同じ発想を型システムレベルで保証しています。 – Functor (functional programming) – Wikipedia
  9. HaskellではFunctorは型クラスとして定義されており、fmap :: (a -> b) -> f a -> f b というシグネチャを持ちます。Functorは「構造を壊さずに中身を変換できる型」を表し、リスト・Maybe・IO・Promiseなど広範な型がこれに相当します。 – Functor – HaskellWiki
  10. Functorが満たすべき法則は2つあります。恒等関数をfmapしても構造が変わらないこと(identity law)と、2つのfmapの連続が合成したfmapと等しいこと(composition law)です。map-with はこれらを自然に満たします。 – Haskell/The Functor class – Wikibooks
  11. 関数合成はラムダ計算の核心的な操作です。圏論の「射の合成」はこの概念を一般化したもので、対象と射さえ定義されれば、関数以外のもの(型変換、データ変換ルール、パーサーなど)も合成の対象として扱えます。 – Stanford Encyclopedia of Philosophy – Category Theory
  12. 整数の加算(単位元は0)、文字列の連結(単位元は空文字列)、リストの連結(単位元は空リスト)、そして関数合成(単位元は恒等関数)はすべてモノイドです。 – Monoid – Wikipedia