5

I was wondering if there is a way to turn List[Kleisli[Option, Int, Int]] to Kleisli[Option, Int, List[Int]].

In particular I have the list of kleisli formed like this:

def k(a: String) = Kleisli[Option, Int, Int](m => Some(a.length * m))
val kList = List("hi", "hello").map(k)

What I do is the following

Kleisli[Option, Int, List[Int]](m => kList.map(_.run(m)).sequence)

which is very messy, not expressive and requires a lot of manual work.

Is there a better way?

gurghet
  • 7,591
  • 4
  • 36
  • 63
  • 1
    apart from the great answers below, I'd like to stress the fact that `sequence o map` (i.e. doing `map` and then doing `sequence`) is equivalent to `traverse` in the general case. This may help you refactor several other parts of your codebase ;) – Gabriele Petronella Oct 14 '17 at 09:32

3 Answers3

7

Yes, you can use traverse which does just that. If you're using cats <= 0.9.0 you can use the following code:

import cats.data._
import cats.instances.list._
import cats.instances.option._
import cats.syntax.traverse._

// ...
def k(a: String) = Kleisli[Option, Int, Int](m => Some(a.length * m))
val result: Kleisli[Option, Int, List[Int] = List("hi", "hello").traverseU(k)

If you're using Scala 2.11.9+, by adding scalacOptions += "-Ypartial-unification" to your build.sbt file, you can just use traverse in place of traverseU. Also, starting from version 1.0.0, traverseU and sequenceU will no longer exist.

Note that, if you're using Scala < 2.11.9 but >= 2.10.6 you can still enable partial unification by adding this plugin to your build.

lambdista
  • 1,850
  • 9
  • 16
  • 2
    I would add (as Luka said in his answer) that in cats version 1.0.0 you will need the `-Y-partial-unification` flag to make `traverse` compile ;) – Gabriele Petronella Oct 14 '17 at 09:34
  • Yeah, I hadn't written it because it's clearly stated in cats's github page but you're right, adding it will do no harm. ;) – lambdista Oct 14 '17 at 10:47
  • Why do we need the flag tho? Why is this not default? – gurghet Oct 15 '17 at 13:06
  • I guess because, even if it fixed a long-lasted [SI-2712](https://issues.scala-lang.org/browse/SI-2712) problem, it's something that was recently fixed so, even if we all are confident it doesn't break anything else, for cautiousness it's enabled through an explicit flag. Here you can find the [PR](https://github.com/scala/scala/pull/5102) against the scalac. – lambdista Oct 16 '17 at 11:55
4

The simplest you can do is to have partial-unification enabled and using traverse:

import cats.implicits._

List("hi", "hello").traverse(k)

This is the same as running sequence on your kList, as traverse is equivalent to map and then sequence.

The easiest way to enable partial-unification, is to add the sbt-partial-unification plugin.

If you're on Scala 2.11.9 or newer, you can also simply add the compiler flag:

scalacOptions += "-Ypartial-unification"

We from the cats team strongly encourage you to have this flag on at all times when using cats, as it makes everything just a lot easier.

Gabriele Petronella
  • 106,943
  • 21
  • 217
  • 235
Luka Jacobowitz
  • 22,795
  • 5
  • 39
  • 57
  • Are you sure enabling partial-unification would work also with the 0.9.0 version of cats? I think they removed the Unapply enabled methods, e.g. sequenceU, traverseU in the 1.0.0-MF. In this case your code wouldn't work with version 0.9.0. – lambdista Oct 14 '17 at 01:33
  • 2
    I see, traverseU and sequenceU were removed in 1.0.0-MF but, on the other hand, partial-unification will make traverse work even with previous version of cats. – lambdista Oct 14 '17 at 10:03
2

Using TraverseOps.sequence we can transform List[A[B]] to A[List[B]], where

A = ({type λ[α] = Kleisli[Option, Int, α]})#λ
B = Int

So the answer is:

def transform(x: List[Kleisli[Option, Int, Int]]) =
  x.sequence[({type λ[α] = Kleisli[Option, Int, α]})#λ, Int]

Following code is complete solution:

import scalaz._
import Scalaz._
import scalaz.Kleisli._

def transform(x: List[Kleisli[Option, Int, Int]]) = x.sequence[({type λ[α] = Kleisli[Option, Int, α]})#λ, Int]

def k(a: String) = Kleisli[Option, Int, Int](m => Some(a.length * m))
val kList = List("hi", "hello").map(k)
val res = transform(kList)
res.run(10)

https://scastie.scala-lang.org/2uZvWWb1ScOHNA55QOcWQA

Zang MingJie
  • 5,164
  • 1
  • 14
  • 27