Inferred type in for comprehension

Viewed 179

Setup:

import scalaz._; import Scalaz._

case class Foo(map: Map[Int, String] = Map.empty, set: Set[Int] = Set.empty)

val `foo.map`: Lens[Foo, Map[Int, String]] = Lens.lensu((f, m) => f.copy(map = m), _.map)
val `foo.set`: Lens[Foo, Set[Int]] = Lens.lensu((f, s) => f.copy(set = s), _.set)

case class Bar(cond: Boolean)

val listOfBars = List(Bar(true), Bar(false))

Now this doesn't compile

listOfBars.runTraverseS[Foo, Unit](Foo()) { bar =>
  if (bar.cond)
    `foo.map` += 1 -> "a"
  else
    `foo.set` += 2
}

This issue is that a Lens[Foo, Map[K, V]].+= returns a State[Foo, Map[K, V]] whereas Lens[Foo, Set[A]].+= returns a State[Foo, Set[A]]. Recall that State[S, A] is invariant in A

However, this does compile:

listOfBars.runTraverseS[Foo, Unit](Foo()) { bar =>
  for {
    _ <- State.init[Foo]
    x <- {

         if (bar.cond)
           `foo.map` += 1 -> "a"
         else
           `foo.set` += 2

    }
  } yield ()
}
  1. Why does this compile?
  2. What is the type of x? (IDEA says it's type $_1 or something like that - I assume that it is not a denotable type)

EDIT

If I change yield () to yield println(x) and println the output of the for comprehension, I get this:

Map(1 -> a)
Set(2)
(Foo(Map(1 -> a),Set(2)),List((), ()))
1 Answers

The problem is that the type of the if expression

if (bar.cond)
  `foo.map` += 1 -> "a"
else
  `foo.set` += 2 

is State[Foo, Serializable] or similar (the common base type that scala can unify the Map and the Set types); but runTraverseS[Foo, Unit] expects a State[Foo, Unit] (as Unit is declared), and those types cannot unify.

The solution is simple, just map over the result and "void" it like this:

val unit = ()

listOfBars.runTraverseS[Foo, Unit](Foo()) { bar =>
  (if (bar.cond)
    `foo.map` += 1 -> "a"
  else
    `foo.set` += 2
  ).map(Function.const(unit))
}

This is actually what the "for" version of the code does (it just drops the value x, whose type is something like State[Foo, Serializable].

Related