Profunctor の dimap に着目してみた

以前の記事で Cats の`Arrow`を[試してみた](https://qiita.com/yasuabe2613/items/d2ffd775804be52a20ad#1-the-arrow)。`cats.arrow` パッケージの中で、`Arrow` は `Strong`を継承し、その `Strong` は 階層最上位の `Profunctor` を継承していた。

この`Profunctor`の`dimap`がちょっと面白いので、Cats の`Kleisli`、Monocle の Lens 、マクロなどを併用してコードを書いてみた。

動くコード: [note.worksheet.sc](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/profunctor_sample/note.worksheet.sc), [Name.scala](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/profunctor_sample/Name.scala)

## 依存ライブラリ等
- Scala 3.1.3
- cats 2.8.0
- monocle 3.1.0

## Profunctor の dimap
`Profunctor`には`dimap`、`lmap`、`rmap`があるが、`lmap`と`rmap`は`dimap`を使って実装されているので、`Profunctor`のコアな部分は`dimap`になる。この記事では`dimap`に着目する。

`dimap`の型は以下のようになる。

```scala
def dimap(fab: F[A, B])(f: C => A)(g: B => D): F[C, D]
```
絵にすると下図のようになる。

![profunctor_dimap.png](https://qiita-image-store.s3.amazonaws.com/0/9633/9cf8460f-73c2-0731-cfd5-7ca7f4e92db0.png)

抽象的にいえば`Profunctor`は $C^{op}\times C\rightarrow C$と書け([Milewski7.2](https://www.youtube.com/watch?v=wtIKd8AhJOc&t=2444))、結果側の対象$C$から入力側の$A$に向かう、逆向きの矢印$f$が、双対圏 $C^{op}$に対応している。ちなみに $f$ も $g$ 同様に下向きにしたものが`Bifunctor`の`bimap`で $C\times C\rightarrow C$ と表現できる([Milewski7.1](https://www.youtube.com/watch?v=pUQ0mmbIdxs&t=2109))。(`bimap`の文字 *b* を左右逆にすると`dimap`になって意味も逆になるのが面白い。)

## シンプルな例
[dimap の apiドキュメント](https://typelevel.org/cats/api/cats/arrow/Profunctor.html#dimap[A,B,C,D](fab:F[A,B])(f:C=%3EA)(g:B=%3ED):F[C,D])に、以下のようなシンプルなコード例が載っている。

```scala
val fab: Double => Double = x => x + 0.3

val f: Int    => Double = x => x.toDouble / 2
val g: Double => Double = x => x * 3
val h = Profunctor[Function1].dimap(fab)(f)(g)
h(3) //res0: Double = 5.4
```
`Profunctor[Function1].dimap(fab)(f)(g)`の部分は、`cats.syntax.profunctor._` の構文をつかうと`fab.dimap(f)(g)`のように書ける。以降、この profunctor 構文をつかう。

これも上のように絵にしてみると以下のようになる。

![profunctor_dimap_apidoc.png](https://qiita-image-store.s3.amazonaws.com/0/9633/7aeb7bbc-ba54-9b41-0f7b-7a5036cde4fb.png)

## アクセッサへの応用
上の数値型の計算より少しだけリアルな例題に応用してみる。

- `Person`型と`Name`型がある
- `Person`型は`Name`型の`name`フィールドを持つ。
- `Name`型には「小文字アルファベット1文字以上」という制約がある[^refined_to_macro]。

```scala
opaque type Name <: String = String // 別ファイルにてマクロと一緒に定義
case class Person(name: Name)

val p1 = Person(Name.from("test"))
val p2 = Person(Name.from("testfoo"))
```
<details><summary>※ Name の実装</summary><div>

```scala
object Name:
  opaque type Name <: String = String

  def apply(s: String): Either[String, Name] =
    if s.matches("[a-z]+") then s.asRight else "error".asLeft

  inline def from(inline s: String): Name = ${ fromImpl('s) }

  private def fromImpl(s: Expr[String])(using Quotes): Expr[Name] =
    import quotes.reflect.*
    s.asTerm match
      case Inlined(_, _, Literal(StringConstant(str))) =>
        Name(str).fold(report.errorAndAbort, _ => s)
      case _ => report.errorAndAbort("not constant")
```
</div></details>

この`Person#name`フィールドがプレフィックス 'test' で始まっていたら、それを削除するという関数を書きたい。

ただし、もし値が 'test' なら、これを取り除いた結果は空文字列になるので、`Name`型の制約が満たされず関数の結果は失敗となる。これを以下のように`ErrorOr[Name]`と表すことにする。

```scala
type ErrorOr[A] = Either[String, A]
```
普通の関数として書けば `Name => ErrorOr[Name]` といった型になるが、ここでは `ErrorOr` まで含めて `Profunctor[F[_,_]]` の `F` に組み込んで、クライスリ射`Kleisli[ErrorOr, Name, Name]`で表すことにする。

`dimap`の結果となる最終的な射は、`Kleisli[ErrorOr, Person, Person]`といった型となる。

とりあえず、ここまで図式化すると下図のようになる。

![profunctor_dimap_person.png](https://qiita-image-store.s3.amazonaws.com/0/9633/ccbfad76-5b59-c11f-ff8a-dae7907f9a4c.png)

これを Scala で書くと以下のようになる。

```scala
val stripTest: Kleisli[ErrorOr, Name, Name] =
  Kleisli(name => Name(name.stripPrefix("test")))

val nameLens = GenLens[Person](_.name)

def dimapped(p: Person) =
  stripTest.dimap(nameLens.get)(nameLens.replace(_)(p))
```
- **図のfab**: `dimap`対象のクライスリ射は`stripTest`で表した
- **図のf**: `Person#name`のアクセッサは、Monocle を使って`nameLens`とした。Person→Nameの場合、`nameLens.get`となる。
- **図のg**: Name→Personは、Personインスタンスを指定して`nameLens.replace(_)(p)`となる。
- **図のresult**: `fab`を`f`と`g`で`dimap`した結果を`dimapped`で表した。

以下のように実行できる。

```scala
def stripTestFromName(p: Person) = dimapped(p).run(p)

stripTestFromName(p1)                       //Left(error)
stripTestFromName(p2)                       //Right(Person(foo))
stripTestFromName(Person(Name.from("abc"))) //Right(Person(abc))
```
Kleisli、lens、マクロ を併用することで、図のシンメトリックなところがコードにも反映できた。

## 雑感等
- `cats.arrow`パッケージの階層最上位にあるのでもわかるように`Profunctor`は抽象度が高く、`Functor`の`map`や、`Contravariant`の`contramap`も、`Profunctor`インスタンスがあれば `rmap` や `lmap` で表現できる。

- `lmap`、`rmap`そのものは、`dimap`の片側の $f$または $g$を`identity`にすることで得られる。[Cats のソース](https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/arrow/Profunctor.scala)にもこの辺りが反映されていて面白い。

- 逆に`Functor`と`Contravariant`があれば`Profunctor#dimap`を実装することもできるらしい([Milewski7.2: 00:46ごろ](https://www.youtube.com/watch?v=wtIKd8AhJOc&index=14&list=PLbgaMIhjbmEnaH_LTkxLI7FMa2HsnawM_))。

- あと圏論関連の講義で、双対で説明できるものは解説が省略されることが多いけど、独習する場合、やっぱり「余〜」、「Co〜」、「Contra〜」も図やコードを書いてみると、もとの概念の理解の助けにもなる気がする。`Cokleisli`とか、`Comonad`、`Coflatmap`あたりも後で見ておきたい。

## 参考
- [Youtube動画: Category Theory 7.2: Monoidal Categories, Functoriality of ADTs, Profunctors](https://www.youtube.com/watch?v=wtIKd8AhJOc)
- [apidoc: cats/arrow/Profunctor.html#dimap](https://static.javadoc.io/org.typelevel/cats-core_2.12/1.0.1/cats/arrow/Profunctor.html#dimap[A,B,C,D](fab:F[A,B])(f:C=%3EA)(g:B=%3ED):F[C,D])

[^refined_to_macro]: もともと refined を使っていたが、Scala 3 が未対応なので macro でベタ書きした(2022-08-09)