Search code examples
jsonscalaeithercirce

using the right side of the disjoint union properly


what's the best way to turn a Right[List] into a List

I will parse a Json String like so

val parsed_states = io.circe.parser.decode[List[List[String]]](source)

And that will create an value equivalent to this

val example_data = Right(List(List("NAME", "state"), List("Alabama", "01"), List("Alaska", "02"), List("Arizona", "04")))

I'm trying to grok Right, Left, Either and implement the best way to get a list of StateName, StateValue pairs out of that list above.

I see that any of these ways will give me what I need (while dropping the header):

val parsed_states = example_data.toSeq(0).tail
val parsed_states = example_data.getOrElse(<ProbUseNoneHere>).iterator.to(Seq).tail
val parsed_states = example_data.getOrElse(<ProbUseNoneHere>).asInstanceOf[Seq[List[String]]].tail

I guess I'm wondering if I should do it one way or another based on the possible behavior upstream coming out of io.circe.parser.decode or am I overthinking this. I'm new to the Right, Left, Either paradigm and not finding terribly many helpful examples.

in reply to @slouc

trying to connect the dots from your answer as they apply to this use case. so something like this?

    def blackBox: String => Either[Exception, List[List[String]]] = (url:String) => {
      if (url == "passalong") {
        Right(List(List("NAME", "state"), List("Alabama", "01"), List("Alaska", "02"), List("Arizona", "04")))
      }
      else Left(new Exception(s"This didn't work bc blackbox didn't parse ${url}"))
    }
    //val seed = "passalong"
    val seed = "notgonnawork"
    val xx: Either[Exception, List[List[String]]] = blackBox(seed)
    def ff(i: List[List[String]]) = i.tail
    val yy = xx.map(ff)
    val zz = xx.fold(
      _  => throw new Exception("<need info here>"),
      i => i.tail)

Solution

  • The trick is in not getting state name / state value pairs out of the Either. They should be kept inside. If you want to, you can transform the Either type into something else (e.g. an Option by discarding whatever you possibly had on the left side), but don't destroy the effect. Something should be there to show that decoding could have failed; it can be an Either, Option, Try, etc. Eventually you will process left and right case accordingly, but this should happen as late as possible.

    Let's take the following trivial example:

    val x: Either[String, Int] = Right(42)
    def f(i: Int) = i + 1
    

    You might argue that you need to get the 42 out of the Right so that you can pass it to f. But that's not correct. Let's rewrite the example:

    val x: Either[String, Int] = someFunction()
    

    Now what? We have no idea whether we have a Left or a Right in value x, so we can't "get it out". Which integer would you obtain in case it's a Left? (if you really do have an integer value to use in that case, that's fair enough, and I will address that use case a bit later)

    What you need to do instead is keep the effect (in this case Either), and you need to continue working in the context of that effect. It's there to show that there was a point in your program (in this case someFunction(), or decoding in your original question) that might have gone wrong.

    So if you want to apply f to your potential integer, you need to map the effect with it (we can do that because Either is a functor, but that's a detail which probably exceeds the scope of this answer):

    val x: Either[String, Int] = Right(42)
    def f(i: Int) = i + 1
    
    val y = x.map(value => f(value)) // Right(43)
    val y = x.map(f) // shorter, point-free notation
    

    and

    val x: Either[String, Int] = someFunction()
    def f(i: Int) = i + 1
    
    // either a Left with some String, or a Right with some integer increased by 1
    val y = x.map(f) 
    

    Then, at the very end of the chain of computations, you can handle the Left and Right cases; for example, if you were processing an HTTP request, then in case of Left you might return a 500, and in case of Right return a 200.

    To address the use case with default value mentioned earlier - if you really want to do that, get rid of the Left and in that case resolve into some value (e.g. 0), then you can use fold:

    def f(i: Int) = i + 1
    
    // if x = Left, then z = 0
    // if x = Right, then z = x + 1
    val z = x.fold(_ => 0, i => i + 1)