I want to combine two endofunctors in the context of Option
. The combining I want is by composing two endofunctors into one via Category.compose
. I found that MonoidK[Endo].algebra[*]
instance for Semigroup[Endo[*]]
satisfies my requirements. I am using Semigroup[Endo[*]]
to make Option
be monoidal over Endo
.
Here is my attempt of implementing it, but it combines two endofunctions not with the Semigroup[Endo[*]]
I want to provide, but rather via some cats.kernel
semigroup instance for A => B
.
import cats.*
import cats.syntax.all.*
import cats.implicits.*
val f: Endo[Int] = _ + 1
val g: Endo[Int] = _ + 2
val of = f.pure[Option]
val og = g.pure[Option]
given Semigroup[Endo[Int]] = MonoidK[Endo].algebra[Int]
val oh = Monoid[Option[Endo[Int]]].combine(of, og)
val res1 = // Some(9)
oh.mapApply(3)
val res2 = // Some(6)
Semigroup[Endo[Int]].combine(f, g).apply(3).pure[Option]
res2
is my desired result here. How can I fix it?
Let's add some debugging utilities:
// subtype of Endo[Int] which would print debug info in evaluation
case class EndoImpl(name: String, added: Int) extends (Int => Int) {
def apply(a: Int): Int = {
println(s"$name($a)==$a+$added")
a + added
}
override def compose[A](g: A => Int): A => Int = g.andThen(this)
override def andThen[A](g: Int => A): Int => A = (this, g) match {
case (f1: EndoImpl, g1: EndoImpl) =>
EndoImpl(s"(${f1.name} andThen ${g1.name})", f1.added + g1.added)
.asInstanceOf[Int => A]
case _ => super.andThen(g)
}
}
Then let's modify the example a bit
val f: Endo[Int] = EndoImpl("f", 1)
val g: Endo[Int] = EndoImpl("g", 2)
val of = f.pure[Option]
val og = g.pure[Option]
given Semigroup[Endo[Int]] = MonoidK[Endo].algebra[Int]
val oh: Option[Endo[Int]] = Monoid[Option[Endo[Int]]].combine(of, og)
println("oh.mapApply(3)")
println(
oh.mapApply(3)
)
println()
println("Semigroup[Endo[Int]].combine(f, g).apply(3).pure[Option]")
println(
Semigroup[Endo[Int]].combine(f, g).apply(3).pure[Option]
)
It prints:
oh.mapApply(3)
f(3)==3+1
g(3)==3+2
Some(9)
Semigroup[Endo[Int]].combine(f, g).apply(3).pure[Option]
(g andThen f)(3)==3+3
Some(6)
Interesting:
andThen
nor combine
, so it is NOT a monoid combining Endo
values using any of themcombine
functions and then apply
the valueLet's try to dig a big more. Both IntelliJ and VC Code+metals have utilities for previewing implicits passed arguments or used for conversions.
They let us see that the second (expected) implementation is roughly equal to:
val om: Option[Endo[Int]] = cats.kernel.instances.option.catsKernelStdMonoidForOption[Endo[Int]](
summon[Semigroup[Endo[Int]]]
).combine(of, og)
which works as expected:
(_ + 1) combine (_ + 2)
None
meanwhile the unexpected implementation is:
catsKernelStdCommutativeMonoidForOption[Endo[Int]](
catsKernelCommutativeGroupForFunction1[Int, Int]
).combine(of, og).mapApply(3)
which is:
(_ + 1)(3)
, (_ + 2)(3)
Semigroup[Int]
: 4 + 5
It's a matter of implicit priorities and I would resolve it by manually constructing the expected instance and exposing it as given
without priorities doing the guess work.
For debugging such issues I recommend getting familiar with related features in IDEs. I'd start with opening action/command palette and typing show implicit
and looking what pops out there.