tl;dr
(f, g) mapN (_ compose _)
しましょうf -> g mapN (_ <<< _)
って書くとちょっとかっこいい
つづき
最近ずっとCatsの記事を書いているな……
先日、(->) r
がApplicativeになるという話をした。
んで、この(->) r
が2つあるときに、これらを合成したい。
import cats._ import cats.implicits._ object Foo val f = (r: Foo.type) => (n: Int) => 2 * n // f: Foo.type => Int => Int = <function1> val g = (r: Foo.type) => (n: Int) => 1 + n // g: Foo.type => Int => Int = <function1> val fg = f.product(g) map { case ff -> gg => ff compose gg } // fg: Foo.type => Int => Int = scala.Function1$$Lambda$9550/0x000000084258e840@2cae8b5d fg(Foo)(100) // res0: Int = 202
直接compose
を呼ぶことができないので、いったんproductにしてからcomposeしている。
ちなみに、|+|
や|-|
は動作するみたいだ:
val fg2 = f |+| g // fg2: Foo.type => Int => Int = cats.kernel.instances.Function1Semigroup$$Lambda$9573/0x00000008425ac840@cbd5065 fg2(Foo)(100) // res1: Int = 301 val fg3 = f |-| g // fg3: Foo.type => Int => Int = cats.kernel.instances.Function1Semigroup$$Lambda$9573/0x00000008425ac840@6c6b9f00 fg3(Foo)(100) // res2: Int = 99
せっかくなら|compose|
みたいなのも用意してほしい。
ところで、arrowの機能を使うとちょっとばかりかっこ良くなる:
val fg4 = (f &&& g) >>> { case ff -> gg => gg >>> ff } // fg4: Foo.type => Int => Int = scala.Function1$$Lambda$9550/0x000000084258e840@5ec28232 fg4(Foo)(100) // res3: Int = 202
>>>
はandThen
になるので、compose
を置換するにはgg >>> ff
と書かなければならない。
なんとなくApplicativeのなんかが使えそうな気がするのだけれど、<*>
とかはちょっと様子が変わってしまうし、map
も違う。
追記
関数をReaderに押し込んで、Reader同士のapにするとうまく動いてそうだけれどすごく手間がかかっている。
import cats.data.Reader import cats._ import cats.implicits._ object Foo val f = (r: Foo.type) => (n: Int) => 2 * n // f: Foo.type => Int => Int = <function1> val g = (r: Foo.type) => (n: Int) => 1 + n // g: Foo.type => Int => Int = <function1> val c = (f: Int => Int) => (g: Int => Int) => f compose g // c: (Int => Int) => (Int => Int) => Int => Int = <function1> val ff = Reader(f) // ff: Reader[Foo.type, Int => Int] = Kleisli(run = <function1>) val gg = Reader(g) // gg: Reader[Foo.type, Int => Int] = Kleisli(run = <function1>) val cc = c.pure[Reader[Foo.type, *]] // cc: data.Kleisli[[A]A, Foo.type, (Int => Int) => (Int => Int) => Int => Int] = Kleisli(run = cats.data.KleisliFunctions$$Lambda$9921/0x000000084262c040@398e2b5f) val composed = cc <*> ff <*> gg // composed: data.Kleisli[[A]A, Foo.type, Int => Int] = Kleisli(run = cats.data.Kleisli$$Lambda$10027/0x000000084261c840@6822855f) composed(Foo)(100) // res4: Int = 202
追記2
うんうんうなっていたら、複数名に助けてもらった。
haskellだとliftM2(もしくはliftA2)っての使ってliftM2 (.) g fって書けるんですけど、ScalaだとliftM2ないっぽい?(探してみてください) pic.twitter.com/d4tnptxC4U
— ひさケット (@hisaketM) 2022年6月13日
Scala/cats の場合、推論の関係とかもあって lift よりも map2 とかの方が使いやすくなってる感じですねーhttps://t.co/i16tfJIGMX pic.twitter.com/jl95xrjHqp
— がくぞ (@gakuzzzz) 2022年6月14日
map2
かmapN
を使うと良さそうだった。
import cats._ import cats.implicits._ object Foo val f = (r: Foo.type) => (n: Int) => n * 2 // f: Foo.type => Int => Int = <function1> val g = (r: Foo.type) => (n: Int) => n + 1 // g: Foo.type => Int => Int = <function1> val fg = f.map2(g)(_ compose _) // fg: Foo.type => Int => Int = cats.instances.Function1Instances$$anon$7$$Lambda$10702/1505168021@255f487b val fg2 = (f, g) mapN (_ compose _) // fg2: Foo.type => Int => Int = scala.Function1$$Lambda$11679/1409117269@7168031a fg(Foo)(100) // res0: Int = 202 fg2(Foo)(100) // res1: Int = 202
非常に綺麗!!
Scalaはその仕組み上liftするのが面倒なのでmapN
でどうliftするのかを暗に伝えるやりかたのほうがやりやすいみたいだ。
liftした関数compose2
があったとして、これは compose2 <*> f <*> g
と等価だ。