HN 日本語サマリー

← 一覧へ戻る
プログラミング

Arrows to arrows, categories to queries (2025)

Arrows to arrows, categories to queries (2025) (reasonablypolymorphic.com)

68 pointsby firstbase6 コメント

要約

著者は、カテゴリ理論の抽象概念を中間言語として利用し、SQLにコンパイルされるプログラミング言語 `catlang` を開発しました。この言語は、関数適用ではなく矢印(arrows)の合成に焦点を当てており、積(product)や余積(coproduct)といった概念をSQLのクエリに変換します。特に、ループ処理は再帰CTE(Common Table Expression)を用いて実装されており、その生成されるSQLコードの複雑さと動作原理が解説されています。

全文翻訳

Arrows to arrows, categories to queries

最近、少し仕事から離れる時間があったので、いつものように賢明とは言えない方法で過ごしていました。特に、SQLにコンパイルされる小さなプログラミング言語 を書きました。私はそれを catlang と呼んでいます。新しいクエリ言語を書いたということではありません。これはプログラミング言語であり、そのコンパイラは巨大な SELECT 文を出力します。そのクエリをPostgresで実行すると、プログラムの出力が得られます。

なぜこんなことをしたのか?それは、言語の実際の機能、つまり中間言語が抽象的なカテゴリ理論のナンセンスの集まりであることをテストするための、面白いコンパイルターゲットが必要だったからです。それについては後述します。しかし、まずこのすごいものを実際に動いているところを見たいと思うでしょう。

どんな入力が与えられても100を返す関数をご覧ください。しかし、それはwhileループに相当する方法で行われます。

count : Int -> Int
count =
  x ->
    loop x
      i ->
        n <- join id id -< i
        z <- abs . (-) -< (n, 100)
        case z of
          inl _ -> inr . (+) -< (n, 1)
          inr _ -> inl -< n

もしarrow notation に慣れているなら、上記が一つの大きな proc ブロックのように見えることに気づくでしょう。これは偶然ではありません(何も偶然ではないからです)。この作業をすべて行うのであれば、アローのデシュガー(desugarer)も一緒に手に入れれば良いと考えました。しかし、話が逸れました。それはまた別の機会の話です。

さて、ここで何が起こっているかというと、count というアローがあり、単一の引数 x を取ります。次に、x の値からループを開始します。ループの中では、n(ループ変数の現在の値)を計算するために、いくつかのマジックを行います。そして、n から100を引いて絶対値を取ります。ここでの abs 関数は少し奇妙です。入力が負の場合 Left (abs x) を返し、そうでなければ Right x を返します。次に、abs の出力に基づいて分岐します。ここで Left と Right はそれぞれ inl と inr にリネームされています。もし n - 100 がゼロ未満であれば、inl ケースに入り、n に1を加えて全体を inr でラップします。これはループによって「この新しい値でループを続ける」と解釈されます。そうでなければ、n - 100 は非負であり、n を直接返すことができます。

回りくどいですか?もちろんです!この分かりにくさは直接的な特徴ではありません。カテゴリ理論的なものに簡単にデシュガーできる、概念的に単純なことをやりたかっただけです。そこで中間言語の話になります。上記の count のソース構文をデシュガーした後、IL表現は以下のようになります。

  id △ id
⨟ cochoice
    ( undist
    ⨟   ( (prj₁ ⨟ id ▽ id) △ id
          ⨟   ( prj₁ △ 100
              ⨟ (-) 
              ⨟ abs
              )
            △ id
          ⨟ prj₁ △ id
          ⨟ dist
          ⨟   ( (prj₂ ⨟ prj₂ ⨟ prj₁) △ 1
              ⨟ (+)
              ⨟ inr
              )
            ▽ ( prj₂
              ⨟ prj₂
              ⨟ prj₁
              ⨟ inl
              )
        )
      △ prj₂
    ⨟ dist
    )
⨟ prj₁

これらすべてについて後で議論しますが、今は美しいUnicodeを眺めてください。

ここでの基本的な考え方は、これらの残りのシンボルがそれぞれ非常にシンプルで特定の代数的意味論を持っているということです。例えば、A ⨟ B は「A を実行し、その結果を B にパイプする」ことを意味します。このカテゴリ理論的なILから他のドメインへの変換を与えることで、catlang をさまざまな奇妙なコンパイルターゲットにコンパイルすることが容易になります。SQLのように。

生成されるSQLがどのようなものか疑問に思っているでしょう。もし勇気があれば覗いてみてください。

冒涜的なコンパイル済みSQL

SELECT
f0 AS f0
FROM
(SELECT
 f0 AS f0, f1 AS f1
 FROM
 (SELECT *
  FROM
  (WITH t0 AS
   (SELECT * 
    FROM
    (WITH RECURSIVE recursion AS
     (SELECT
      clock_timestamp() as step
      , * 
      FROM
      (WITH t1 AS
       (SELECT * 
        FROM
        (SELECT
         f0 AS f0, f1 AS f1, NULL::integer AS f2, NULL::integer AS f3
         FROM
         (WITH t2 AS
          (SELECT * FROM (SELECT 0 as f0) AS _) 
          SELECT * 
          FROM
          (SELECT * FROM (SELECT f0 AS f0 FROM t2 AS _) AS _ 
           CROSS JOIN 
           (SELECT f0 AS f1 FROM t2 AS _))
          AS _) 
         AS _) 
        AS _) 
       SELECT * 
       FROM
       (WITH t3 AS
        (SELECT * 
         FROM
         (-- undist
          SELECT * 
          FROM
          (SELECT
           f0 AS f0, NULL::integer AS f1, f1 AS f2
           FROM
           (-- undist1
            SELECT * FROM t1 AS _ WHERE "f0" IS NOT NULL)
           AS _) 
          AS _
          UNION
          SELECT * 
          FROM
          (SELECT
           NULL::integer AS f0, f2 AS f1, f3 AS f2
           FROM
           (-- dist2
            SELECT * FROM t1 AS _ WHERE "f2" IS NOT NULL)
           AS _) 
          AS _) 
         AS _) 
        SELECT * 
        FROM
        (WITH t4 AS
         (SELECT * 
          FROM
          (SELECT * 
           FROM
           (SELECT
            f0 AS f0, f1 AS f1
            FROM
            (WITH t5 AS
             (SELECT * FROM t3 AS _) 
             SELECT * 
             FROM
             (WITH t6 AS
              (SELECT * 
               FROM
               (SELECT * 
                FROM
                (SELECT
                 f0 AS f0
                 FROM
                 (WITH t7 AS
                  (SELECT * FROM (SELECT f0 AS f0, f1 AS f1 FROM t5 AS _) AS _) 
                  SELECT * 
                  FROM
                  (SELECT * 
                   FROM
                   (SELECT
                    f0 AS f0
                    FROM
                    (-- join1
                     SELECT * FROM t7 AS _ WHERE "f0" IS NOT NULL)
                    AS _) 
                   AS _
                   UNION
                   SELECT * 
                   FROM
                   (SELECT
                    f1 AS f0
                    FROM
                    (-- join2
                     SELECT * FROM t7 AS _ WHERE "f1" IS NOT NULL)
                    AS _) 
                   AS _) 
                  AS _) 
                 AS _) 
                AS _
                CROSS JOIN 
                (SELECT f0 AS f1, f1 AS f2, f2 AS f3 FROM t5 AS _))
               AS _) 
              SELECT * 
              FROM
              (WITH t8 AS
               (SELECT * 
                FROM
                (SELECT * 
                 FROM
                 (SELECT
                  f0 AS f0, f1 AS f1
                  FROM
                  (WITH t9 AS
                   (SELECT * 
                    FROM
                    (SELECT
                     f0 - f1 AS f0
                     FROM
                     (WITH t10 AS
                      (SELECT * FROM t6 AS _) 
                      SELECT * 
                      FROM
                      (SELECT * 
                       FROM
                       (SELECT f0 AS f0 FROM (SELECT f0 AS f0 FROM t10 AS _) AS _) 
                       AS _
                       CROSS JOIN 
                       (SELECT f0 AS f1 FROM (SELECT 100 as f0 FROM t10 AS _) AS _))
                      AS _) 
                     AS _) 
                    AS _) 
                   SELECT * 
                   FROM
                   (SELECT * 
                    FROM
                    (SELECT
                     abs(f0) as f0, NULL::integer as f1
                     FROM
                     t9
                     AS _
                     WHERE
                     f0 < 0)
                    AS _
                    UNION
                    SELECT * 
                    FROM
                    (SELECT NULL::integer as f0, f0 as f1 FROM t9 AS _ WHERE f0 >= 0)
                    AS _) 
                   AS _) 
                  AS _) 
                 AS _
                 CROSS JOIN 
                 (SELECT f0 AS f2, f1 AS f3, f2 AS f4, f3 AS f5 FROM t6 AS _))
                AS _) 
               SELECT * 
               FROM
               (WITH t11 AS
                (SELECT * 
                 FROM
                 (SELECT * 
                  FROM
                  (SELECT
                   f0 AS f0, f1 AS f1
                   FROM
                   (SELECT f0 AS f0, f1 AS f1 FROM t8 AS _) 
                   AS _) 
                  AS _
                  CROSS JOIN 
                  (SELECT
                   f0 AS f2, f1 AS f3, f2 AS f4, f3 AS f5, f4 AS f6, f5 AS f7
                   FROM
                   t8
                   AS _))
                 AS _) 
                SELECT * 
                FROM
                (WITH t12 AS
                 (SELECT * 
                  FROM
                  (-- dist
                   SELECT * 
                   FROM
                   (SELECT
                    f0 AS f0, f2 AS f1, NULL::integer AS f10, NULL::integer AS f11, NULL::integer AS f12, NULL::integer AS f13, f3 AS f2, f4 AS f3, f5 AS f4, f6 AS f5, f7 AS f6, NULL::integer AS f7, NULL::integer AS f8, NULL::integer AS f9
                    FROM
                    (-- dist1
                     SELECT * FROM t11 AS _ WHERE "f0" IS NOT NULL)
                    AS _) 
                   AS _
                   UNION
                   SELECT * 
                   FROM
                   (SELECT
                    NULL::integer AS f0, NULL::integer AS f1, f4 AS f10, f5 AS f11, f6 AS f12, f7 AS f13, NULL::integer AS f2, NULL::integer AS f3, NULL::integer AS f4, NULL::integer AS f5, NULL::integer AS f6, f1 AS f7, f2 AS f8, f3 AS f9
                    FROM
                    (-- dist2
                     SELECT * FROM t11 AS _ WHERE "f1" IS NOT NULL)
                    AS _) 
                   AS _) 
                  AS _) 
                 SELECT * 
                 FROM
                 (SELECT * 
                  FROM
                  (SELECT
                   NULL::integer AS f0, f0 AS f1
                   FROM
                   (SELECT
                    f0 + f1 AS f0
                    FROM
                    (WITH t13 AS
                     (SELECT * 
                      FROM
                      (SELECT
                       f0 AS f0, f1 AS f1, f2 AS f2, f3 AS f3, f4 AS f4, f5 AS f5, f6 AS f6
                       FROM
                       (-- join1
                        SELECT * FROM t12 AS _ 
                        WHERE 
                        ("f0" IS NOT NULL) AND ((("f1" IS NOT NULL) OR ("f2" IS NOT NULL)) AND (("f3" IS NOT NULL) AND ((("f4" IS NOT NULL) OR ("f5" IS NOT NULL)) AND ("f6" IS NOT NULL))))) 
                       AS _) 
                      AS _) 
                     SELECT * 
                     FROM
                     (SELECT * 
                      FROM
                      (SELECT
                       f0 AS f0
                       FROM
                       (SELECT
                        f0 AS f0
                        FROM
                        (SELECT
                         f2 AS f0, f3 AS f1, f4 AS f2, f5 AS f3
                         FROM
                         (SELECT
                          f1 AS f0, f2 AS f1, f3 AS f2, f4 AS f3, f5 AS f4, f6 AS f5
                          FROM
                          t13
                          AS _) 
                         AS _) 
                        AS _) 
                       AS _) 
                      AS _
                      CROSS JOIN 
                      (SELECT f0 AS f1 FROM (SELECT 1 as f0 FROM t13 AS _) AS _))
                     AS _) 
                    AS _) 
                   AS _) 
                  AS _
                  UNION
                  SELECT * 
                  FROM
                  (SELECT
                   f0 AS f0, NULL::integer AS f1
                   FROM
                   (SELECT
                    f0 AS f0
                    FROM
                    (SELECT
                     f2 AS f0, f3 AS f1, f4 AS f2, f5 AS f3
                     FROM
                     (SELECT
                      f1 AS f0, f2 AS f1, f3 AS f2, f4 AS f3, f5 AS f4, f6 AS f5
                      FROM
                      (SELECT
                       f7 AS f0, f8 AS f1, f9 AS f2, f10 AS f3, f11 AS f4, f12 AS f5, f13 AS f6
                       FROM
                       (-- join2
                        SELECT * FROM t12 AS _ 
                        WHERE 
                        ("f7" IS NOT NULL) AND ((("f8" IS NOT NULL) OR ("f9" IS NOT NULL)) AND (("f10" IS NOT NULL) AND ((("f11" IS NOT NULL) OR ("f12" IS NOT NULL)) AND ("f13" IS NOT NULL))))) 
                       AS _) 
                      AS _) 
                     AS _) 
                    AS _) 
                   AS _) 
                  AS _) 
                 AS _) 
                AS _) 
               AS _) 
              AS _) 
             AS _) 
            AS _
            CROSS JOIN 
            (SELECT f0 AS f2 FROM (SELECT f2 AS f0 FROM t3 AS _) AS _))
           AS _) 
          SELECT * 
          FROM
          (-- dist
           SELECT * 
           FROM
           (SELECT
            f0 AS f0, f2 AS f1, NULL::integer AS f2, NULL::integer AS f3
            FROM
            (-- dist1
             SELECT * FROM t4 AS _ WHERE "f0" IS NOT NULL)
           AS _) 
           AS _
           UNION
           SELECT * 
           FROM
           (SELECT
            NULL::integer AS f0, NULL::integer AS f1, f1 AS f2, f2 AS f3
            FROM
            (-- dist2
             SELECT * FROM t4 AS _ WHERE "f1" IS NOT NULL)
           AS _) 
           AS _) 
          AS _) 
         AS _) 
        AS _) 
       AS _
       UNION ALL
       SELECT
       clock_timestamp() as step
       , * 
       FROM
       (SELECT * 
        FROM
        (WITH t14 AS
         (SELECT * FROM recursion AS _) 
         SELECT * 
         FROM
         (WITH t15 AS
          (SELECT * 
           FROM
           (-- undist
            SELECT * 
            FROM
            (SELECT
             f0 AS f0, NULL::integer AS f1, f1 AS f2
             FROM
             (-- undist1
              SELECT * FROM t14 AS _ WHERE "f0" IS NOT NULL)
             AS _) 
            AS _
            UNION
            SELECT * 
            FROM
            (SELECT
             NULL::integer AS f0, f2 AS f1, f3 AS f2
             FROM
             (-- dist2
              SELECT * FROM t14 AS _ WHERE "f2" IS NOT NULL)
             AS _) 
            AS _) 
           AS _) 
          SELECT * 
          FROM
          (WITH t16 AS
           (SELECT * 
            FROM
            (SELECT * 
             FROM
             (SELECT
              f0 AS f0, f1 AS f1
              FROM
              (WITH t17 AS
               (SELECT * FROM t15 AS _) 
               SELECT * 
               FROM
               (WITH t18 AS
                (SELECT * 
                 FROM
                 (SELECT * 
                  FROM
                  (SELECT
                   f0 AS f0
                   FROM
                   (WITH t19 AS
                    (SELECT * FROM (SELECT f0 AS f0, f1 AS f1 FROM t17 AS _) AS _) 
                    SELECT * 
                    FROM
                    (SELECT * 
                     FROM
                     (SELECT
                      f0 AS f0
                      FROM
                      (-- join1
                       SELECT * FROM t19 AS _ WHERE "f0" IS NOT NULL)
                      AS _) 
                     AS _
                     UNION
                     SELECT * 
                     FROM
                     (SELECT
                      f1 AS f0
                      FROM
                      (-- join2
                       SELECT * FROM t19 AS _ WHERE "f1" IS NOT NULL)
                      AS _) 
                     AS _) 
                    AS _) 
                   AS _) 
                  AS _
                  CROSS JOIN 
                  (SELECT f0 AS f1, f1 AS f2, f2 AS f3 FROM t17 AS _))
                 AS _) 
                SELECT * 
                FROM
                (WITH t20 AS
                 (SELECT * 
                  FROM
                  (SELECT * 
                   FROM
                   (SELECT
                    f0 AS f0, f1 AS f1
                    FROM
                    (WITH t21 AS
                     (SELECT * 
                      FROM
                      (SELECT
                       f0 - f1 AS f0
                       FROM
                       (WITH t22 AS
                        (SELECT * FROM t18 AS _) 
                        SELECT * 
                        FROM
                        (SELECT * 
                         FROM
                         (SELECT f0 AS f0 FROM (SELECT f0 AS f0 FROM t22 AS _) AS _) 
                         AS _
                         CROSS JOIN 
                         (SELECT f0 AS f1 FROM (SELECT 100 as f0 FROM t22 AS _) AS _))
                        AS _) 
                       AS _) 
                      AS _) 
                     SELECT * 
                     FROM
                     (SELECT * 
                      FROM
                      (SELECT
                       abs(f0) as f0, NULL::integer as f1
                       FROM
                       t21
                       AS _
                       WHERE
                       f0 < 0)
                      AS _
                      UNION
                      SELECT * 
                      FROM
                      (SELECT NULL::integer as f0, f0 as f1 FROM t21 AS _ WHERE f0 >= 0)
                      AS _) 
                     AS _) 
                    AS _) 
                   AS _
                   CROSS JOIN 
                   (SELECT f0 AS f2, f1 AS f3, f2 AS f4, f3 AS f5 FROM t18 AS _))
                  AS _) 
                 SELECT * 
                 FROM
                 (WITH t23 AS
                  (SELECT * 
                   FROM
                   (SELECT * 
                    FROM
                    (SELECT
                     f0 AS f0, f1 AS f1
                     FROM
                     (SELECT f0 AS f0, f1 AS f1 FROM t20 AS _) 
                     AS _) 
                    AS _
                    CROSS JOIN 
                    (SELECT
                     f0 AS f2, f1 AS f3, f2 AS f4, f3 AS f5, f4 AS f6, f5 AS f7
                     FROM
                     t20
                     AS _))
                  AS _) 
                 SELECT * 
                 FROM
                 (WITH t24 AS
                  (SELECT * 
                   FROM
                   (-- dist
                    SELECT * 
                    FROM
                    (SELECT
                     f0 AS f0, f2 AS f1, NULL::integer AS f10, NULL::integer AS f11, NULL::integer AS f12, NULL::integer AS f13, f3 AS f2, f4 AS f3, f5 AS f4, f6 AS f5, f7 AS f6, NULL::integer AS f7, NULL::integer AS f8, NULL::integer AS f9
                     FROM
                     (-- dist1
                      SELECT * FROM t23 AS _ WHERE "f0" IS NOT NULL)
                    AS _) 
                   AS _
                   UNION
                   SELECT * 
                   FROM
                   (SELECT
                    NULL::integer AS f0, NULL::integer AS f1, f4 AS f10, f5 AS f11, f6 AS f12, f7 AS f13, NULL::integer AS f2, NULL::integer AS f3, NULL::integer AS f4, NULL::integer AS f5, NULL::integer AS f6, f1 AS f7, f2 AS f8, f3 AS f9
                    FROM
                    (-- dist2
                     SELECT * FROM t23 AS _ WHERE "f1" IS NOT NULL)
                    AS _) 
                   AS _) 
                  AS _) 
                 SELECT * 
                 FROM
                 (SELECT * 
                  FROM
                  (SELECT
                   NULL::integer AS f0, f0 AS f1
                   FROM
                   (SELECT
                    f0 + f1 AS f0
                    FROM
                    (WITH t25 AS
                     (SELECT * 
                      FROM
                      (SELECT
                       f0 AS f0, f1 AS f1, f2 AS f2, f3 AS f3, f4 AS f4, f5 AS f5, f6 AS f6
                       FROM
                       (-- join1
                        SELECT * FROM t24 AS _ 
                        WHERE 
                        ("f0" IS NOT NULL) AND ((("f1" IS NOT NULL) OR ("f2" IS NOT NULL)) AND (("f3" IS NOT NULL) AND ((("f4" IS NOT NULL) OR ("f5" IS NOT NULL)) AND ("f6" IS NOT NULL))))) 
                       AS _) 
                      AS _) 
                     SELECT * 
                     FROM
                     (SELECT * 
                      FROM
                      (SELECT
                       f0 AS f0
                       FROM
                       (SELECT
                        f0 AS f0
                        FROM
                        (SELECT
                         f2 AS f0, f3 AS f1, f4 AS f2, f5 AS f3
                         FROM
                         (SELECT
                          f1 AS f0, f2 AS f1, f3 AS f2, f4 AS f3, f5 AS f4, f6 AS f5
                          FROM
                          t25
                          AS _) 
                         AS _) 
                        AS _) 
                       AS _) 
                      AS _
                      CROSS JOIN 
                      (SELECT f0 AS f1 FROM (SELECT 1 as f0 FROM t25 AS _) AS _))
                     AS _) 
                    AS _) 
                   AS _) 
                  AS _
                  UNION
                  SELECT * 
                  FROM
                  (SELECT
                   f0 AS f0, NULL::integer AS f1
                   FROM
                   (SELECT
                    f0 AS f0
                    FROM
                    (SELECT
                     f2 AS f0, f3 AS f1, f4 AS f2, f5 AS f3
                     FROM
                     (SELECT
                      f1 AS f0, f2 AS f1, f3 AS f2, f4 AS f3, f5 AS f4, f6 AS f5
                      FROM
                      (SELECT
                       f7 AS f0, f8 AS f1, f9 AS f2, f10 AS f3, f11 AS f4, f12 AS f5, f13 AS f6
                       FROM
                       (-- join2
                        SELECT * FROM t24 AS _ 
                        WHERE 
                        ("f7" IS NOT NULL) AND ((("f8" IS NOT NULL) OR ("f9" IS NOT NULL)) AND (("f10" IS NOT NULL) AND ((("f11" IS NOT NULL) OR ("f12" IS NOT NULL)) AND ("f13" IS NOT NULL))))) 
                       AS _) 
                      AS _) 
                     AS _) 
                    AS _) 
                   AS _) 
                  AS _) 
                 AS _) 
                AS _) 
               AS _) 
              AS _) 
             AS _) 
            AS _
            CROSS JOIN 
            (SELECT f0 AS f2 FROM (SELECT f2 AS f0 FROM t15 AS _) AS _))
           AS _) 
          SELECT * 
          FROM
          (-- dist
           SELECT * 
           FROM
           (SELECT
            f0 AS f0, f2 AS f1, NULL::integer AS f2, NULL::integer AS f3
            FROM
            (-- dist1
             SELECT * FROM t16 AS _ WHERE "f0" IS NOT NULL)
           AS _) 
           AS _
           UNION
           SELECT * 
           FROM
           (SELECT
            NULL::integer AS f0, NULL::integer AS f1, f1 AS f2, f2 AS f3
            FROM
            (-- dist2
             SELECT * FROM t16 AS _ WHERE "f1" IS NOT NULL)
           AS _) 
           AS _) 
          AS _) 
         AS _) 
        AS _) 
       AS _
       WHERE 
       ("f2" IS NOT NULL) AND ("f3" IS NOT NULL)) 
      AS _) 
     SELECT * FROM recursion ORDER BY step DESC LIMIT 1)
    AS _) 
   SELECT * 
   FROM
   (WITH t26 AS
    (SELECT * 
     FROM
     (-- undist
      SELECT * 
      FROM
      (SELECT
       f0 AS f0, NULL::integer AS f1, f1 AS f2
       FROM
       (-- undist1
        SELECT * FROM t0 AS _ WHERE "f0" IS NOT NULL)
      AS _) 
      AS _
      UNION
      SELECT * 
      FROM
      (SELECT
       NULL::integer AS f0, f2 AS f1, f3 AS f2
       FROM
       (-- dist2
        SELECT * FROM t0 AS _ WHERE "f2" IS NOT NULL)
      AS _) 
      AS _) 
     AS _) 
    SELECT * 
    FROM
    (WITH t27 AS
     (SELECT * 
      FROM
      (SELECT * 
       FROM
       (SELECT
        f0 AS f0, f1 AS f1
        FROM
        (WITH t28 AS
         (SELECT * FROM t26 AS _) 
         SELECT * 
         FROM
         (WITH t29 AS
          (SELECT * 
           FROM
           (SELECT * 
            FROM
            (SELECT
             f0 AS f0
             FROM
             (WITH t30 AS
              (SELECT * FROM (SELECT f0 AS f0, f1 AS f1 FROM t28 AS _) AS _) 
              SELECT * 
              FROM
              (SELECT * 
               FROM
               (SELECT
                f0 AS f0
                FROM
                (-- join1
                 SELECT * FROM t30 AS _ WHERE "f0" IS NOT NULL)
                AS _) 
               AS _
               UNION
               SELECT * 
               FROM
               (SELECT
                f1 AS f0
                FROM
                (-- join2
                 SELECT * FROM t30 AS _ WHERE "f1" IS NOT NULL)
                AS _) 
               AS _) 
              AS _) 
             AS _) 
            AS _
            CROSS JOIN 
            (SELECT f0 AS f1, f1 AS f2, f2 AS f3 FROM t28 AS _))
           AS _) 
          SELECT * 
          FROM
          (WITH t31 AS
           (SELECT * 
            FROM
            (SELECT * 
             FROM
             (SELECT
              f0 AS f0, f1 AS f1
              FROM
              (WITH t32 AS
               (SELECT * 
                FROM
                (SELECT
                 f0 - f1 AS f0
                 FROM
                 (WITH t33 AS
                  (SELECT * FROM t29 AS _) 
                  SELECT * 
                  FROM
                  (SELECT * 
                   FROM
                   (SELECT f0 AS f0 FROM (SELECT f0 AS f0 FROM t33 AS _) AS _) 
                   AS _
                   CROSS JOIN 
                   (SELECT f0 AS f1 FROM (SELECT 100 as f0 FROM t33 AS _) AS _))
                  AS _) 
                 AS _) 
                AS _) 
               SELECT * 
               FROM
               (SELECT * 
                FROM
                (SELECT
                 abs(f0) as f0, NULL::integer as f1
                 FROM
                 t32
                 AS _
                 WHERE
                 f0 < 0)
                AS _
                UNION
                SELECT * 
                FROM
                (SELECT NULL::integer as f0, f0 as f1 FROM t32 AS _ WHERE f0 >= 0)
                AS _) 
               AS _) 
              AS _) 
             AS _
             CROSS JOIN 
             (SELECT f0 AS f2, f1 AS f3, f2 AS f4, f3 AS f5 FROM t29 AS _))
            AS _) 
           SELECT * 
           FROM
           (WITH t34 AS
            (SELECT * 
             FROM
             (SELECT * 
              FROM
              (SELECT
               f0 AS f0, f1 AS f1
               FROM
               (SELECT f0 AS f0, f1 AS f1 FROM t31 AS _) 
               AS _) 
              AS _
              CROSS JOIN 
              (SELECT
               f0 AS f2, f1 AS f3, f2 AS f4, f3 AS f5, f4 AS f6, f5 AS f7
               FROM
               t31
               AS _))
             AS _) 
            SELECT * 
            FROM
            (WITH t35 AS
             (SELECT * 
              FROM
              (-- dist
               SELECT * 
               FROM
               (SELECT
                f0 AS f0, f2 AS f1, NULL::integer AS f10, NULL::integer AS f11, NULL::integer AS f12, NULL::integer AS f13, f3 AS f2, f4 AS f3, f5 AS f4, f6 AS f5, f7 AS f6, NULL::integer AS f7, NULL::integer AS f8, NULL::integer AS f9
                FROM
                (-- dist1
                 SELECT * FROM t34 AS _ WHERE "f0" IS NOT NULL)
               AS _) 
               AS _
               UNION
               SELECT * 
               FROM
               (SELECT
                NULL::integer AS f0, NULL::integer AS f1, f4 AS f10, f5 AS f11, f6 AS f12, f7 AS f13, NULL::integer AS f2, NULL::integer AS f3, NULL::integer AS f4, NULL::integer AS f5, NULL::integer AS f6, f1 AS f7, f2 AS f8, f3 AS f9
                FROM
                (-- dist2
                 SELECT * FROM t34 AS _ WHERE "f1" IS NOT NULL)
               AS _) 
               AS _) 
              AS _) 
             SELECT * 
             FROM
             (SELECT * 
              FROM
              (SELECT
               NULL::integer AS f0, f0 AS f1
               FROM
               (SELECT
                f0 + f1 AS f0
                FROM
                (WITH t36 AS
                 (SELECT * 
                  FROM
                  (SELECT
                   f0 AS f0, f1 AS f1, f2 AS f2, f3 AS f3, f4 AS f4, f5 AS f5, f6 AS f6
                   FROM
                   (-- join1
                    SELECT * FROM t35 AS _ 
                    WHERE 
                    ("f0" IS NOT NULL) AND ((("f1" IS NOT NULL) OR ("f2" IS NOT NULL)) AND (("f3" IS NOT NULL) AND ((("f4" IS NOT NULL) OR ("f5" IS NOT NULL)) AND ("f6" IS NOT NULL))))) 
                   AS _) 
                  AS _) 
                 SELECT * 
                 FROM
                 (SELECT * 
                  FROM
                  (SELECT
                   f0 AS f0
                   FROM
                   (SELECT
                    f0 AS f0
                    FROM
                    (SELECT
                     f2 AS f0, f3 AS f1, f4 AS f2, f5 AS f3
                     FROM
                     (SELECT
                      f1 AS f0, f2 AS f1, f3 AS f2, f4 AS f3, f5 AS f4, f6 AS f5
                      FROM
                      t36
                      AS _) 
                     AS _) 
                    AS _) 
                   AS _) 
                  AS _
                  CROSS JOIN 
                  (SELECT f0 AS f1 FROM (SELECT 1 as f0 FROM t36 AS _) AS _))
                 AS _) 
                AS _) 
               AS _) 
              AS _
              UNION
              SELECT * 
              FROM
              (SELECT
               f0 AS f0, NULL::integer AS f1
               FROM
               (SELECT
                f0 AS f0
                FROM
                (SELECT
                 f2 AS f0, f3 AS f1, f4 AS f2, f5 AS f3
                 FROM
                 (SELECT
                  f1 AS f0, f2 AS f1, f3 AS f2, f4 AS f3, f5 AS f4, f6 AS f5
                  FROM
                  (SELECT
                   f7 AS f0, f8 AS f1, f9 AS f2, f10 AS f3, f11 AS f4, f12 AS f5, f13 AS f6
                   FROM
                   (-- join2
                    SELECT * FROM t35 AS _ 
                    WHERE 
                    ("f7" IS NOT NULL) AND ((("f8" IS NOT NULL) OR ("f9" IS NOT NULL)) AND (("f10" IS NOT NULL) AND ((("f11" IS NOT NULL) OR ("f12" IS NOT NULL)) AND ("f13" IS NOT NULL))))) 
                   AS _) 
                  AS _) 
                 AS _) 
                AS _) 
               AS _) 
              AS _) 
             AS _) 
            AS _) 
           AS _) 
          AS _) 
         AS _) 
        AS _
        CROSS JOIN 
        (SELECT f0 AS f2 FROM (SELECT f2 AS f0 FROM t26 AS _) AS _))
       AS _) 
      SELECT * 
      FROM
      (-- dist
       SELECT * 
       FROM
       (SELECT
        f0 AS f0, f2 AS f1, NULL::integer AS f2, NULL::integer AS f3
        FROM
        (-- dist1
         SELECT * FROM t27 AS _ WHERE "f0" IS NOT NULL)
       AS _) 
       AS _
       UNION
       SELECT * 
       FROM
       (SELECT
        NULL::integer AS f0, NULL::integer AS f1, f1 AS f2, f2 AS f3
        FROM
        (-- dist2
         SELECT * FROM t27 AS _ WHERE "f1" IS NOT NULL)
       AS _) 
       AS _) 
      AS _) 
     AS _) 
    AS _) 
   AS _
   WHERE 
   ("f0" IS NOT NULL) AND ("f1" IS NOT NULL)) 
  AS _) 
AS _;

Postgres 17で上記のクエリを実行すると、値が100の単一の列を持つ単一行が返されます。そして、実際に100までループしてそれを達成していると信じてください。信じられないなら、以下の変更を加えてください。

-     SELECT * FROM recursion ORDER BY step DESC LIMIT 1)
+     SELECT * FROM recursion ORDER BY step DESC)

これにより、代わりに各イテレーションのステップごとに1行が返されます。

生成されたSQLには明らかな最適化の余地がありますが、プロジェクトの面白い部分ではないので、私の時間を使う価値はないと思いました。

一体何が起こっているのか?

ここで、この背後にあるカテゴリ理論について時間をかけて議論しましょう。私は決して専門家ではありませんが、この分野で10年間頭をぶつけてきて学んだことは、少しでも進めば大きな違いを生むということです。

私たちの目的のために、型と、型間の矢印(関数)があります。常に恒等的な「何もしない矢印」id があります。

id  :: a ~> a

そして、一方の端をもう一方に繋げることで矢印を合成できます。

(⨟) :: (a ~> b) -> (b ~> c) -> (a ~> c)

Haskell(あるいは実際にはどんなプログラミング言語でも)とは異なり、関数適用という概念はありません。つまり、矢印はありません。

-- 存在しない!
($) :: (a ~> b) -> a -> b

矢印を合成することしかできず、適用することはできません。だから、これらを「関数」ではなく「矢印」と呼ぶのです。

積型(product types)を扱うための矢印がいくつかあります。2つの射影関数は fst と snd に対応し、ペアから個々の要素を取り出します。

prj₁ :: (a, b) ~> a
prj₂ :: (a, b) ~> b

そもそもペアをどのように作るのでしょうか?2つの矢印を実行して b と c を計算し、新しい矢印を生成して (b, c) のペアを生成する「フォーク」操作を使用できます。

(△)  :: (a ~> b) -> (a ~> c) -> (a ~> (b, c))

Haskellの経験がある場合、この操作を単に (,) ペアコンストラクタと考える誘惑に駆られるかもしれません。しかし、計算の型から、b と c の間にデータ依存関係がないことがわかります。そのため、ペアの各側を並列化することができます。

カテゴリ理論では、矢印の左右の区別はかなり任意です。これは双対性という概念につながり、矢印を反転させることでクールな新しい動作を得ることができます。積の仕組みをすべて双対化すると、余積(coproduct)の仕組みが得られます。a と b の余積は「a または b のどちらかだが、両方でもどちらでもない」ものです。

prj₁ と prj₂ の矢印の方向を入れ替え、(,) を Either に置き換えると、以下の注入が得られます。

inl :: a ~> Either a b
inr :: b ~> Either a b

そして、余積を削除するための「結合」操作が以下になります。

(▽) :: (a ~> c) -> (b ~> c) -> (Either a b ~> c)

ここでも、Haskellから来ると、これは単なる標準の either 関数です。これは2つのケースのいずれかへの分岐に対応します。

これら8つの操作だけで、すでに計り知れない表現力があることがわかります。⨟ を使ってデータ依存関係を、▽ を使って分岐を表現できます。△ を使うと並列化の機会が自動的にエンコードされ、複雑なデータ構造を構築する能力が得られ、prj₁ と prj₂ を使うとデータ構造から情報を取り出すことができます。

ILには変数名が一切見当たらないことに気づくでしょう。ソース言語のデシュガーはスタック(something to allocate △ id パターン経由)を構築し、後続の変数ルックアップをスタック上の射影のシーケンスに置き換えて値を再度見つけます。一方では、これはカテゴリ理論的なILを読みにくくしますが、リターゲットを非常に容易にします。多くのドメインにはグループ化の概念がありますが、ネイティブな命名の概念はありません。

例えば、電子回路では、Int32 を表す32本のワイヤのリボンを持つことができます。別の32本のワイヤのリボンがある場合、両方のワイヤを (Int32, Int32) のペアに対応する64本のワイヤのリボンに簡単にルーティングできます。

名前をILに到達する前に削除することで、コンパイラのバックエンドが名前を扱う必要がなくなります。スタック表現で作業するだけで、可能であれば射影のシーケンスを特別に最適化することができます。

この議論で特に興味深いのは、ループをどのようにデシュガーするかです。基本的なプリミティブは cochoice です。

cochoice :: (Either a c ~> Either b c) -> (a ~> b)

これは、Either 上の矢印を、Either のない矢印に魔法のように変換します。もちろん、その矢印を Either で実行する必要があります。その関数が inl を返した場合、私たちは満足し、それをそのまま出力できます。しかし、関数が inr を返した場合、それを Either 化された矢印に再度渡すしかありません。Haskellでは、cochoice は以下のように実装されます。

cochoiceHask :: (Either a c -> Either b c) -> a -> c
cochoiceHask f = go . Left
  where
    go :: Either a c -> b
    go eac =
      case f eac of
        Left b -> b
        Right c -> go (Right c)

ご覧のとおり、これは f が Left を返すまでループします。このループの定式化の素晴らしい点は、ループ本体を最初に通過したときとそれ以降の通過を静的に区別できることです。最初に eac は Left ですが、それ以外のすべての回では Right です。元の count プログラムではこれを利用していませんが、ループコードで最初の通過時に何かを初期化する必要があったことは何度ありますか?

SQLへのコンパイル

これがILの背後にある理論です。これをSQLにコンパイルするにはどうすればよいでしょうか?

前述したように、中間言語の各操作にSQL実装を与えるだけで済みます。簡単な例として、id は {} を SELECT * FROM {} にコンパイルします。ここで {} は矢印の入力です。

ここでの最も難しい部分は、データ表現を考案することでした。積の各要素を新しい列としてエンコードするのは明白に思えますが、余積についてはどうすればよいでしょうか?多くの試行錯誤の結果、余積をフラット化することにしました。例えば、型:

(Int, Either Int Int)

は3つの列として表現されます。

( f1 INT NOT NULL
, f2 INT
, f3 INT
)

ただし、f2 または f3 のどちらか一方のみが常に IS NOT NULL であるという制約があります。

これを確立すると、ほとんどすべてが些細なことになります。合成はネストされたクエリに対応します。フォークは CROSS JOIN で、各サブクエリの列を連結します。結合は UNION で、正しい余積コンストラクタを見ていることを保証するために WHERE field IS NOT NULL 句を追加します。

cochoice は唯一本当にトリッキーな部分ですが、これは再帰CTEに対応します。計算のために再帰CTEテーブルを生成するのは難しくありませんが、そこから最終値を取得するのは驚くほど困難でした。SQLテーブルのセマンティクスは、それらがマルチセットであり、任意の最大要素を持つということです。つまり、最終結果をクエリするには、関連する方法で構造化された列が必要です。Postgresが受け入れるものと、クエリの構造化方法のいくつかの癖のために、「ループした回数」列を挿入してそれに並べ替えることは、法外に困難でした。そのため、clock_timestamp() as step 列を追加してプロセッサクロックを参照し、それで並べ替えるというハックを使いました。

これは明らかにハックであり、もし私が1行以上の行を生成するプリミティブを追加した場合に問題を引き起こす可能性が高いですが、これもただの楽しみであり、誰が気にするでしょうか。私の詐欺行為に気分を害された場合は、プルリクエストを送ってください!

将来の愚かな方向性

このプロジェクトに取り組むための休暇時間がなくなってしまったので、計画していたメタサーキュラーな愚かさにはおそらく手が届かないでしょう。

コンパイラにはまだいくつかの文字列処理プリミティブ(追加は簡単)が必要ですが、その後、catlangで小さなBrainfuckインタプリタを書くのは簡単でしょう。それをSQLにコンパイルできます。これで、PostgresでBrainfuckインタプリタが実行されることになります。もちろん、これは以前にも手作業で行われてきましたが、私の知る限り、コンパイル経由では初めてです。

CからBrainfuckへのコンパイラが存在します。そしてPostgresはCで書かれています。Xzibitを喜ばせるような動きで、PostgresをPostgresで実行できます。そしてもちろん、BrainfuckをBrainfuckで実行するのは楽しいでしょう。誰かがそのようなものに貢献したい場合、それはクールなcatlangバックエンドになるでしょう。

ノートとデューデリジェンスなど

私がこのようなことをしたのは初めてではありません。catlangのソース言語は、Haskellのarrow syntaxに強く触発されており、それは結局Arrowsのデシュガーアルゴリズムに他なりません。Arrowsは、arr :: (a -> b) -> (a ~> b) という操作を必要とするため、少し間違った抽象化です。これは、Haskell関数をカテゴリに埋め込むことができる必要があることを意味しますが、これはほとんど不可能です。

残念ながら、Haskellのarrow構文はほとんどすべての操作を arr にデシュガーするため、arrow表記は事実上無用になります。理想的な世界では、このブログ記事で説明したすべてが小さなHaskellライブラリになり、arrow表記が重い作業を行うでしょう。しかし、私たちが生きている世界はそうではありません。

また、プログラミング言語の背後にあるカテゴリ理論的な意味論に気づいたのも初めてではありません。これについては誰を引用すべきか実際にはわかりませんが、ラムダ計算がデカルト閉カテゴリに対応することはよく知られた伝承です。「閉」という言葉は、eval :: (a ~> b, a) ~> b という操作があることを意味しますが、誰もがラムダ計算を実装しているので、それなしでどこまで進めるかを見るのは楽しいだろうと思いました。これはcatlangのチューリング完全性に対する制限ではありません(cochoice がすべてを提供してくれるため)。

私は、Compiling to Categoriesという論文を読んで以来、10年近く「カテゴリ優先」のプログラミング言語を書くことを考えてきました。その論文はHaskellを取り上げ、それをカテゴリにデシュガーし直します。私はここで多くのトリックをその論文から盗みました。

とにかく。コードはすべてgithubで利用可能です。リポジトリは私の通常のコーディング基準には達していませんが、お詫び申し上げます。注目すべきは、Haskellコードを出力できるテンプレートハスケルバックエンドです。つまり、catlangをHaskellのarrowデシュガーがあるべき姿にコンパイルするクォーター(quasiquoter)を作成するのはそれほど難しくないでしょう。もしそのようなものへの需要が十分にあれば、この部分をライブラリにすることを検討します。


  1. このエッセイで矢印の型を見るとき、~> はcatlangで書ける矢印であり、-> はメタ理論に存在する矢印であるという区別をします。↩︎

← →