最近、圏論とプログラミングという素晴らしい資料を拝読しました。圏論とプログラミング愛に溢れる資料で読んでいて目頭が熱くなりました。そうだよな・・・プログラマにも圏論いるよな・・・
ただ、自分にとって残念だったのは、資料で説明用に選択されたプログラミング言語が**「Haskell」だったことです。もちろんHaskellは素晴らしい言語です。ただ、自分にとってHaskellは外国語なのでちょっと理解が難しいのです。なのでこの資料が「Scala」**で書かれていたらと夢想せずにはいられなかったのです。
Scalaと言えば昨年末にScala3のリサーチコンパイラのDottyがFeature Completeを宣言しました1。この宣言で新機能の追加は終了して、あとは2020年末のリリースに向けてひたすら品質を上げていく段階に突入しました。つまり、ようやく次世代のScalaが全貌を現したということです。
ここである考えが脳裏を過りました。もしかしたら「圏論とプログラミング」に出てくるHaskellの記述を**「素のScala3」**で置き換えられるのではないかと。
本記事ではその疑問を検証してみたいと思います。
(本記事は自分のブログからの転載記事です。)
TL;DR
-
圏論とプログラミングに記載されているHaskellの記述をScala3に書き換えてみた
- Scala3(Dotty)は0.22.0-RC1を利用
- 外部ライブラリは使わない
- 標準言語機能と標準ライブラリとコンパイラオプションは利用してもよい
- メタプログラミングは使わない
- 書き換えたのは余米田の補題まで
- それ以降はだれか挑戦してください・・・
- 結論
- かなり自然に圏論の概念を記述できるようになった
- 記述量的にはHaskellに大分近づいたがまだ若干差がある
- 行数的にはかなり肉薄したが、Scalaは型記述が多いため文字数ではまだ差がある
はじめに
本記事では「圏論とプログラミング」(以降、「元記事」と表記)に記述されているHaskellの記述をScala3(Dotty 0.22.0-RC1)で書き換える検証を行った結果を解説します。
本記事のHaskellのソースコードは基本的に元記事からの引用になります2。Scala3のコードはなるべくHaskellのソースコードを忠実に再現するようにしていますが、言語の機能や慣習の違いにより様々な差異があることをご了承ください。
基本的には以下のルールで書き換えています。
- なるべくScala3の新機能3を用いてスマートに書き換える
- 外部ライブラリは使わない4
- 標準言語機能と標準ライブラリのみ使って良い
- コンパイラオプションは使って良い5
- メタプログラミングは使わない
- 本記事では「素のScala3」でどこまでできるかがテーマなので今回は用いないこととする
- Haskellで用いられている記号の演算子はScalaでは文字のメソッド名に変換する
- Haskellの型引数は小文字が用いられるが、Scalaでは大文字にする
Scala3で圏論の概念を実装してみる
それではいよいよScala3で実装してみます。以降のタイトルは元記事と対応しています。元記事の発表スライドメモを並べながら見るとわかりやすいと思います。
型クラスを使った圏の定義
まずは基本的な圏の定義です。行数はHaskellもScala3も3行に収まっていますが、文字数的にはHaskellのほうが少ないようです。
class Category cat where
id :: cat a a
(.) :: cat b c -> cat a b -> cat a c
trait Category[F[_, _]]
def id[A]: F[A, A]
def [A, B, C](x: F[B, C]).compose(y: F[A, B]): F[A, C]
Scala 3の解説をすると、まずこのインデント記法を初めて見る方は驚くかもしれません。Scala 3ではHaskellやPythonのようにインデントベースで書けるようになりました。もちろん括弧を用いた記法も可能です。詳細は以下を御覧ください。
また以下の拡張メソッド記法も初見はGo言語っぽいなと思いました。以下のメソッドはF[B,C]
型を拡張してcompose
メソッドを追加する構文です。
def [A, B, C](x: F[B, C]).compose(y: F[A, B]): F[A, C]
拡張メソッドの詳細は以下を確認してください。
圏の例:クライスリ圏
次にクライスリ圏の定義ですが、その前にモナドを定義する必要があります。Haskellのモナド関連の定義はここから引用させて頂きました。
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
return = pure
trait Functor[F[_]]
def [A, B](x: F[A]).map(y: A => B): F[B]
trait Applicative[F[_]] extends Functor[F]
def [A, B](x: F[A => B]).ap(y: F[A]): F[B]
def pure[A](f: A): F[A]
def [A, B](x: F[A]).map(y: A => B): F[B] = ap(pure(y))(x)
trait Monad[F[_]] extends Applicative[F]
def [A, B](x: F[A]).flatMap(y: A => F[B]): F[B]
Scalaの定義も大分スッキリしていると思います。特徴としてはScala3では継承を用いて実装を行っています。継承を用いずに型制約を実現することもできますが、型が素直なis-a
関係にあるときは継承を用いたほうがシンプルになります。また、慣習の違いによりHaskellとScala3では関数名を変えています。Haskellの関数とScalaのメソッドの対応は以下のとおりです。
Haskell | Scala3 |
---|---|
fmap | map |
<*> | ap |
pure | pure |
>>= | flatMap |
return | pure |
準備が整ったのでクライスリ圏の定義を比較してみます。
newtype Kleisli m a b = Kleisli { runKleisli :: a -> m b}
instance Monad m => Category (Kleisli m) where
id :: Kleisli m a a
id = Kleisli pure
(.) :: Kleisli m b c -> Kleisli m a b -> Kleisli m a c
f . g = Kleisli (\x -> runKleisli g x >>= runKleisli f)
opaque type Kleisli[F[_], A, B] = A => F[B]
object Kleisli
def apply[F[_], A, B](x: A => F[B]) = x
given kleisliCategory[F[_]](using m: Monad[F]) as Category[Kleisli[F, *, *]]
def id[A] = Kleisli(m.pure(_))
def [A, B, C](x: Kleisli[F, B, C]).compose(y: Kleisli[F, A, B]) =
Kleisli[F, A, C](k => y(k).flatMap(x))
Scala3では新しく「不透明型エイリアス(Opaque Type Alias)」を用いています。これはHaskellのnewtypeに対応するもので、単一の型のゼロコスト抽象化を提供します。詳細は以下を確認してください。
またScala3では型クラスのインスタンスを定義するためにgiven
、using
、as
という構文が追加されています。これらは従来のimplicit
構文をわかりやすくしたものです。詳細は以下を確認してください。
あと、型の部分適用を簡単にするためにKind projector syntax supportを有効にしています。利用した箇所は「Kleisli[F, *, *]
」の部分です。詳細は以下を確認してください。
圏の例:レンズ圏
次はレンズ圏の定義です。定義だけ見ると相当ややこしい感じがします・・・
data Lens a b = Lens (a -> b) (a -> b -> a)
instance Category Lens where
id :: Lens a a
id = Lens Prelude.id (const Prelude.id)
(.) :: Lens b c -> Lens a b -> Lens a c
Lens g1 s1 . Lens g2 s2 = Lens (g1 . g2) s3
where
s3 a c = s2 a (s1 (g2 a) c)
trait Lens[A, B](val g: A => B, val s: A => B => A)
object Lens
def apply[A, B](g: A => B, s: A => B => A) = new Lens(g, s){}
given Category[Lens]
def id[A] = Lens(identity, Function.const(identity))
def [A, B, C](x: Lens[B, C]).compose(y: Lens[A, B]) =
val s3: A => C => A = a => c => y.s(a)(x.s(y.g(a))(c))
Lens(x.g compose y.g, s3)
Scala3では新しくトレイトにパラメータを持てるようになったのでこれを利用しています。またLensを生成しやすくするためにコンパニオンオブジェクトにapplyメソッドを定義しています。Scala3のCreator Applicationsが利用できるかと思ったのですがトレイトには利用できないようです。詳細は以下を確認してください。
関手
ようやく関手まで来ました。ここの関手は圏論の関手です。HaskellのコードはMaybe
がHask圏(型と関数の圏)からHask圏への関手になっていることを示していますが、ScalaのコードはMaybe
と同等の意味論を持つOption
がScala圏(この名称でいいかは謎)からScala圏への関手になっていることを示しています。
class (Category c, Category d) => Functor' c d f where
fmap' :: c a b -> d (f a) (f b)
instance Functor' (->) (->) Maybe where
fmap' _ Nothing = Nothing
fmap' f (Just a) = Just (f a)
trait Functor_[C[_, _], D[_, _], F[_]](using Category[C], Category[D])
def fmap[A, B](c: C[A, B]): D[F[A], F[B]]
given Functor_[Function1, Function1, Option]
def fmap[A, B](f: A => B) = x => x match
case None => None
case Some(a) => Some(f(a))
そういえばusing
ってトレイトに直接書けることを今回気づきました。正直Haskellの定義を見たときにScala3に翻訳するのが難しそうかなって思いましたが、案外簡単にいけました。
また、Functorのgivenインスタンスの実装はHaskellに倣いましたが、以下のようにOption
のmap
に委譲もできます。
given Functor_[Function1, Function1, Option]
def fmap[A, B](c: A => B) = _.map(c)
自己関手(Endofunctor)
こちらがプログラミングでよく使われる「関手」です。ここでもHaskellをScala3の記述量は大差ないものになっています。
class Functor f where
fmap :: (a -> b) -> f a -> f b
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
trait Functor[F[_]]
def [A, B](x: F[A]).map(y: A => B): F[B]
given Functor[Option]
def [A, B](x: Option[A]).map(y: A => B) = x.map(y)
関手圏と自然変換
ついに関手圏と自然変換までやってきました。そしてここでHaskellとScalaに決定的な差がついてしまいました。
newtype f :~> g = NT { unNT :: forall x. f x -> g x }
instance Category (:~>) where
id :: a :~> a
id = NT Prelude.id
(.) :: (b :~> c) -> (a:~> b) -> (a :~> c)
NT f . NT g = NT (f . g)
trait NT[F[_], G[_]]
def apply[A](fa: F[A]): G[A]
trait CategoryK[K[F[_], G[_]]]
def id[A[_]]: K[A, A]
def [A[_], B[_], C[_]](x: K[B, C]).compose(y: K[A, B]): K[A, C]
given CategoryK[NT]
def id[A[_]] = new NT { def apply[E](fa: A[E]) = fa }
def [A[_], B[_], C[_]](x: NT[B, C]).compose(y: NT[A, B]) =
new NT { def apply[X](fa: A[X]) = x(y(fa)) }
まず、Scala3にはHaskellのようなforall
がありません。つまり単純にunNTのような関数を定義することはできませんでした。また自然変換(NT)の型引数は2つなので、内部のメソッド(apply
)に型引数を追加してunNTっぽい関数を実装しました。こうすることでapply
の型引数A
を外部に晒さずにすみます。
次の問題は自然変換を圏のインスタンスにしようとしたとき、圏(Category)と自然変換(NT)のカインドが異なることです。Categoryのインスタンスになれるのは2つの型引数を持つ1階の型コンストラクタですが、NTは2つの1階の型コンストラクタを引数に持つのでカインドが異なると怒られるのです。こういう場合のためにScala3で追加されたカインドポリモルフィズムが使えるのではと思ったのですが、残念ながら今回のケースでは利用できませんでした。
仕方がないので2つの型コンストラクタを引数にとるCategoryK
を作成してNTをインスタンスにしてみました・・・
自然変換の例:多相関数
今まで自然変換が多相関数だと言われてもピンと来なかったのですがNTを使ってheadやlengthを実装してみることでようやく腹落ちしました。
まずはheadからです。元記事にはListの定義がなかったので追加しています。
data List a = Nil' | Cons' a (List a) -- 元記事になかったので追加
head' :: List :~> Maybe
head' = NT $ \case
Nil' -> Nothing
Cons' a _ -> Just a
def head: NT[List, Option] = new NT {
def apply[X](fa: List[X]) = fa match
case Nil => None
case a :: _ => Some(a)
}
ScalaではListのメソッドのheadOption
を利用すれば以下のように簡潔に定義できます。
def head2: NT[List, Option] =
new NT { def apply[X](fa: List[X]) = fa.headOption }
次にLength
メソッドです。注目すべきはConst
です。片側を捨てる関手ですが、こういうときに役に立ちます。
newtype Const a b = Const { getConst :: a } -- 元記事になかったので追加
length' :: List :~> Const Int
length' = NT $ \case
Nil' -> Const 0
Cons' _ as -> Const $ 1 + getConst (unNT length' as)
opaque type Const[A, B] = A
object Const
def apply[A](x: A) = x
def length: NT[List, Const[Int, *]] = new NT {
def apply[X](fa: List[X]) = fa match
case Nil => Const(0)
case _ :: as => Const(1 + length(as))
}
Scala3のコードにも大分なれて来たと思いますが、ここでもKind Projectorを利用してConstにIntを部分適用しています。
米田の補題
ようやく圏論の華もしくは到達点とも言える米田の補題までやってきました。この抽象度の高い概念の記述の際にもScala3はHaskellと同等の記述力を発揮します。
newtype Yoneda f a =
Yoneda { runYoneda :: forall b. (a -> b) -> f b}
instance Functor (Yoneda f) where
fmap f m = Yoneda (\k -> runYoneda m (k . f))
trait Yoneda[F[_], A]
def apply[B](f: A => B): F[B]
given yonedaFunctor[F[_]] as Functor[Yoneda[F, *]]
def [A, B](x: Yoneda[F, A]).map(f: A => B) = new Yoneda {
def apply[C](k: B => C) = x(k compose f)
}
次にliftYoneda
とlowerYoneda
です。これもほとんど遜色なく記述できています。
liftYoneda :: Functor f => f a -> Yoneda f a
liftYoneda a = Yoneda (\f -> fmap f a) -- 元記事になかったので追加
lowerYoneda :: Yoneda f a -> f a
lowerYoneda (Yoneda f) = f id -- 元記事になかったので追加
def liftYoneda[F[_], A](fa: F[A])(using Functor[F]): Yoneda[F, A] = new Yoneda {
def apply[B](f: A => B): F[B] = fa.map(f)
}
def lowerYoneda[F[_], A](y: Yoneda[F, A]): F[A] = y(identity)
余米田の補題(Coyoneda)
最後に余米田の補題です。ここまで来るとFreerモナドの夢を見る一歩手前です。
data Coyoneda f a where
Coyoneda :: (z -> a) -> f z -> Coyoneda f a
instance Functor (Coyoneda f) where
fmap f (Coyoneda g v) = Coyoneda (f Prelude.. g) v
enum Coyoneda[F[_], A]
case New[F[_], A, Z](za: Z => A, fz: F[Z]) extends Coyoneda[F, A]
given coyonedaFunctor[F[_]] as Functor[Coyoneda[F, *]]
def [A, B](x: Coyoneda[F, A]).map(f: A => B) = x match
case Coyoneda.New(g, v) => Coyoneda.New(f compose g, v)
ここに来てScala3の新機能を利用しました。enum
です。Haskellの方でGADTが登場したので満を持してここにenum
を持ってきました。enum
は代数的データ型を簡単に書ける機能です。詳細は以下を確認してください。
最後にliftCoyoneda
とlowerCoyoneda
を比較して終わりです。最後は文字数的にも肉薄しています。
liftCoyoneda :: f a -> Coyoneda f a
liftCoyoneda = Coyoneda Prelude.id -- 元記事になかったので追加
lowerCoyoneda :: Functor f =>Coyoneda f a -> f a
lowerCoyoneda (Coyoneda f m) = fmap f m -- 元記事になかったので追加
def liftCoyoneda[F[_], A](fa: F[A]) =
Coyoneda.New(identity[A], fa)
def lowerCoyoneda[F[_]: Functor, A](x: Coyoneda[F, A]) = x match
case Coyoneda.New(f, m) => m.map(f)
Scala3とHaskellの比較
実際にどれだけのScalaとHaskellの記述量を比較してみました。Haskell側は記述量を揃えるために標準ライブラリで提供されているものも定義しました。
また、Hakellは8個ものGHC言語拡張を利用していました。これもそのままソースコードに記述して行数としてカウントすることにしました。一種のペナルティみたいなものです。
比較結果は以下のとおりです。行数ベースではかなり肉薄していると思います。ただScalaの方が型の記述が多くなるため文字数としてはHaskellが簡潔になります。
言語 | 文字数(スペース込み) | 文字数(スペース無視) | 行数 |
---|---|---|---|
Haskell | 2786 | 2076 | 113 |
Scala | 3334 | 2630 | 112 |
以下に比較に用いたコンパイル可能なHaskellとScalaのコードを記載します。興味がある方は中を覗いてみてください。
コンパイル可能なHaskellコード
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE GADTs #-}
import Prelude hiding (Functor, Applicative, Monad, pure, (>>=))
class Category cat where
id :: cat a a
(.) :: cat b c -> cat a b -> cat a c
instance Category (->) where
id :: (->) a a
id a = a
(.) :: (->) b c -> (->) a b -> (->) a c
f . g = \x -> f (g x)
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
return = pure
newtype Kleisli m a b = Kleisli { runKleisli :: a -> m b}
instance Monad m => Category (Kleisli m) where
id :: Kleisli m a a
id = Kleisli pure
(.) :: Kleisli m b c -> Kleisli m a b -> Kleisli m a c
f . g = Kleisli (\x -> runKleisli g x >>= runKleisli f)
data Lens a b = Lens (a -> b) (a -> b -> a)
instance Category Lens where
id :: Lens a a
id = Lens Prelude.id (const Prelude.id)
(.) :: Lens b c -> Lens a b -> Lens a c
Lens g1 s1 . Lens g2 s2 = Lens (g1 Prelude.. g2) s3
where
s3 a c = s2 a (s1 (g2 a) c)
class (Category c, Category d) => Functor' c d f where
fmap' :: c a b -> d (f a) (f b)
instance Functor' (->) (->) Maybe where
fmap' _ Nothing = Nothing
fmap' f (Just a) = Just (f a)
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
newtype f :~> g = NT { unNT :: forall x. f x -> g x }
instance Category (:~>) where
id :: a :~> a
id = NT Prelude.id
(.) :: (b :~> c) -> (a:~> b) -> (a :~> c)
NT f . NT g = NT (f Prelude.. g)
data List a = Nil' | Cons' a (List a)
head' :: List :~> Maybe
head' = NT $ \case
Nil' -> Nothing
Cons' a _ -> Just a
newtype Const a b = Const { getConst :: a }
length' :: List :~> Const Int
length' = NT $ \case
Nil' -> Const 0
Cons' _ as -> Const $ 1 + getConst (unNT length' as)
newtype Yoneda f a =
Yoneda { runYoneda :: forall b. (a -> b) -> f b}
instance Functor (Yoneda f) where
fmap f m = Yoneda (\k -> (runYoneda m) (k Prelude.. f))
liftYoneda :: Functor f => f a -> Yoneda f a
liftYoneda a = Yoneda (\f -> Main.fmap f a)
lowerYoneda :: Yoneda f a -> f a
lowerYoneda (Yoneda f) = f Prelude.id
data Coyoneda f a where
Coyoneda :: (z -> a) -> f z -> Coyoneda f a
instance Functor (Coyoneda f) where
fmap f (Coyoneda g v) = Coyoneda (f Prelude.. g) v
liftCoyoneda :: f a -> Coyoneda f a
liftCoyoneda = Coyoneda Prelude.id
lowerCoyoneda :: Functor f => Coyoneda f a -> f a
lowerCoyoneda (Coyoneda f m) = Main.fmap f m
main = do
putStrLn "Hello Category Theory!"
コンパイル可能なScala3コード
object CategoryTheoryExampleDefs
trait Category[F[_, _]]
def id[A]: F[A, A]
def [A, B, C](x: F[B, C]).compose(y: F[A, B]): F[A, C]
given Category[Function1]
def id[A] = x => x
def [A, B, C](x: Function1[B, C]).compose(y: Function1[A, B]) = a =>x(y(a))
trait Functor[F[_]]
def [A, B](x: F[A]).map(y: A => B): F[B]
trait Applicative[F[_]] extends Functor[F]
def [A, B](x: F[A => B]).ap(y: F[A]): F[B]
def pure[A](f: A): F[A]
def [A, B](x: F[A]).map(y: A => B): F[B] = ap(pure(y))(x)
trait Monad[F[_]] extends Applicative[F]
def [A, B](x: F[A]).flatMap(y: A => F[B]): F[B]
opaque type Kleisli[F[_], A, B] = A => F[B]
object Kleisli
def apply[F[_], A, B](x: A => F[B]) = x
given kleisliCategory[F[_]](using m: Monad[F]) as Category[Kleisli[F, *, *]]
def id[A] = Kleisli(m.pure(_))
def [A, B, C](x: Kleisli[F, B, C]).compose(y: Kleisli[F, A, B]) =
Kleisli[F, A, C](k => y(k).flatMap(x))
trait Lens[A, B](val g: A => B, val s: A => B => A)
object Lens
def apply[A, B](g: A => B, s: A => B => A) = new Lens(g, s){}
given Category[Lens]
def id[A] = Lens(identity, Function.const(identity))
def [A, B, C](x: Lens[B, C]).compose(y: Lens[A, B]) =
val s3: A => C => A = a => c => y.s(a)(x.s(y.g(a))(c))
Lens(x.g compose y.g, s3)
trait Functor_[C[_, _], D[_, _], F[_]](using Category[C], Category[D])
def fmap[A, B](c: C[A, B]): D[F[A], F[B]]
given Functor_[Function1, Function1, Option]
def fmap[A, B](f: A => B) = x => x match
case None => None
case Some(a) => Some(f(a))
given Functor[Option]
def [A, B](x: Option[A]).map(y: A => B) = x.map(y)
trait NT[F[_], G[_]]
def apply[A](fa: F[A]): G[A]
trait CategoryK[K[F[_], G[_]]]
def id[A[_]]: K[A, A]
def [A[_], B[_], C[_]](x: K[B, C]).compose(y: K[A, B]): K[A, C]
given CategoryK[NT]
def id[A[_]] = new NT { def apply[E](fa: A[E]) = fa }
def [A[_], B[_], C[_]](x: NT[B, C]).compose(y: NT[A, B]) =
new NT { def apply[X](fa: A[X]) = x(y(fa)) }
def head: NT[List, Option] = new NT {
def apply[X](fa: List[X]) = fa match
case Nil => None
case a :: _ => Some(a)
}
opaque type Const[A, B] = A
object Const
def apply[A](x: A) = x
def length: NT[List, Const[Int, *]] = new NT {
def apply[X](fa: List[X]) = fa match
case Nil => Const(0)
case _ :: as => Const(1 + length(as))
}
trait Yoneda[F[_], A]
def apply[B](f: A => B): F[B]
given yonedaFunctor[F[_]] as Functor[Yoneda[F, *]]
def [A, B](x: Yoneda[F, A]).map(f: A => B): Yoneda[F, B] = new Yoneda {
def apply[C](k: B => C) = x(k compose f)
}
def liftYoneda[F[_], A](fa: F[A])(using Functor[F]): Yoneda[F, A] = new Yoneda {
def apply[B](f: A => B): F[B] = fa.map(f)
}
def lowerYoneda[F[_], A](y: Yoneda[F, A]): F[A] = y(identity)
enum Coyoneda[F[_], A]
case New[F[_], A, Z](za: Z => A, fz: F[Z]) extends Coyoneda[F, A]
given coyonedaFunctor[F[_]] as Functor[Coyoneda[F, *]]
def [A, B](x: Coyoneda[F, A]).map(f: A => B) = x match
case Coyoneda.New(g, v) => Coyoneda.New(f compose g, v)
def liftCoyoneda[F[_], A](fa: F[A]) =
Coyoneda.New(identity[A], fa)
def lowerCoyoneda[F[_]: Functor, A](x: Coyoneda[F, A]) = x match
case Coyoneda.New(f, m) => m.map(f)
@main def example: Unit =
import CategoryTheoryExampleDefs.{_, given}
println("Hello Category Theory")
HaskellとScala3の開発環境
HaskellとScala 3の開発環境にはVisual Studio Codeを使いました。どちらも文法エラーですぐに赤くなるし、マウスオーバーで型がすぐに分かるので非常に助かりました。Language Server Protocolは偉大です。環境構築は以下を参照しました。
まとめ
圏論とプログラミングに記載されているHaskellコードを以下の条件でScala3に書き換えてみました。
- Scala3(Dotty)は0.22.0-RC1を利用
- 外部ライブラリは使わない
- 標準言語機能と標準ライブラリとコンパイラオプションは利用してもよい
- メタプログラミングは使わない
- 外部ライブラリは使わない
- 余米田の補題まで書き換えてみた
- それ以降はだれか挑戦してください・・・
結論としては、Scala3でかなり自然に圏論の概念を記述できるようになったように感じました。記述量的にも大分Haskellに近づいたように思います。この一年近くはScala3のimplicit
周りの文法がめちゃくちゃ変わってどうなるか心配だったのですが、なんとなくいい感じの文法に仕上がってきたのではないかと思います(個人の感想です)。あとはラムダ関数がジェネリクスに対応してくれるとかなり嬉しいですね6。
本記事がScala3と圏論に興味がある方の一助になれば幸いです。
おまけ
いつの間にか圏論と学びをめぐる往復書簡というものが始まっていました。数学ガールの圏論版が出て欲しいなぁ・・・
参考文献
- 圏論とプログラミング
- プログラマのためのモナド(圏論)
- Dotty Blog
- Dotty Documentation
- Cats: Home
- Notes on Category Theory in Scala 3 (Dotty) | Typista.org
- Scala3に入るかもしれないContextual Abstractionsを味見してみた(更新・追記あり) - Qiita
- Scala 3、Pythonのようにインデントベースの構文で書けるようになるってよ! - Qiita
- Scalaプログラマが圏論を学ぶためのオススメ文献 - 3選 - Qiita
-
コンパイルを通すため、必要に応じて若干修正してある箇所もあります。 ↩
-
ここで言う新機能とはScala2にはなく、Dotty 0.22.0-RC1までにリリースされている機能を指しています。まだScala3はリリースされていないので、ここで紹介した機能も変更されたり削除されたりする可能性があります。 ↩
-
元記事のHaskellのコードにもGHCの言語拡張が用いられているので、Scala3でもOKとしました。Scalaでは
-Ykind-projector
フラグを利用しています。 ↩ -
実装が一部マージされているようです。👉[Proof of concept] Polymorphic function types by smarter · Pull Request #4672 · lampepfl/dotty ↩