0

How to conveniently convert Seq[Try[Option[String, Any]]] into Try[Option[Map[String, Any]]].

If any Try before convert throws an exception, the converted Try should throw as well.

Jing Huang
  • 41
  • 5
  • 1
    I don't think `Option[String,Any]` makes sense. [Option](https://www.scala-lang.org/api/current/scala/Option.html) takes a single type parameter. Did you mean `Option[(String,Any)]` ? – jq170727 Jan 30 '20 at 23:47
  • 1
    I don't get the point of `Try[Option[SOMETHING]]`. Option is either `None` or `Some` meaning invalid values have been taken care of by the time they reach `Try`. It seems a bit redundant. – jrook Jan 30 '20 at 23:57
  • 1
    @jrook There is scenario you want to keep track if an operation is Success or Fail, and even this operation is successful, it can return a None or Some. – SwiftMango Jan 31 '20 at 01:35
  • @texasbruce If I am controlling the signature and get `None` from an operation, I already know something bad happened. If I wanted to know the reason why that happened, then I would directly `Try` the operation. The case in which an operation yields None without any error seems dubious to me. Why would I want a logic that can produce `None` as a normal value? I would appreciate if you could show me a real-world use case. – jrook Jan 31 '20 at 20:52
  • @jrook There are many cases, such as, when you query a record from db. You use Try to indicate whether there's something wrong with db or connection. Then use Some or None to indicate whether the record exists. – SwiftMango Jan 31 '20 at 21:34
  • @texasbruce I would still not wrap these inside each other and instead use [toOption](https://stackoverflow.com/questions/43027503/scala-proper-using-of-try-and-option-combined-together). But I get the idea. thanks. – jrook Jan 31 '20 at 22:27

5 Answers5

2

Assuming that the input type has a tuple inside the Option then this should give you the result you want:

val in: Seq[Try[Option[(String, Any)]]] = ???

val out: Try[Option[Map[String,Any]]] = Try(Some(in.flatMap(_.get).toMap))

If any of the Trys is Failure then the outer Try will catch the exception raised by the get and return Failure

The Some is there to give the correct return type

The get extracts the Option from the Try (or raises an exception)

Using flatMap rather than map removes the Option wrapper, keeping all Some values and discaring None values, giving Seq[(String, Any)]

The toMap call converts the Seq to a Map

Tim
  • 26,753
  • 2
  • 16
  • 29
0

Here is something that's not very clean but may help get you started. It assumes Option[(String,Any)], returns the first Failure if there are any in the input Seq and just drops None elements.

foo.scala

package foo
import scala.util.{Try,Success,Failure}

object foo {
  val x0 = Seq[Try[Option[(String, Any)]]]()
  val x1 = Seq[Try[Option[(String, Any)]]](Success(Some(("A",1))), Success(None))
  val x2 = Seq[Try[Option[(String, Any)]]](Success(Some(("A",1))), Success(Some(("B","two"))))
  val x3 = Seq[Try[Option[(String, Any)]]](Success(Some(("A",1))), Success(Some(("B","two"))), Failure(new Exception("bad")))
  def f(x: Seq[Try[Option[(String, Any)]]]) =
    x.find( _.isFailure ).getOrElse( Success(Some(x.map( _.get ).filterNot( _.isEmpty ).map( _.get ).toMap)) )
}

Example session

bash-3.2$ scalac foo.scala
bash-3.2$ scala -classpath .
Welcome to Scala 2.13.1 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_66).
Type in expressions for evaluation. Or try :help.

scala> import foo.foo._
import foo.foo._

scala> f(x0)
res0: scala.util.Try[Option[Equals]] = Success(Some(Map()))

scala> f(x1)
res1: scala.util.Try[Option[Equals]] = Success(Some(Map(A -> 1)))

scala> f(x2)
res2: scala.util.Try[Option[Equals]] = Success(Some(Map(A -> 1, B -> two)))

scala> f(x3)
res3: scala.util.Try[Option[Equals]] = Failure(java.lang.Exception: bad)

scala> :quit
jq170727
  • 13,159
  • 3
  • 46
  • 56
0

If you're willing to use a functional support library like Cats then there are two tricks that can help this along:

  1. Many things like List and Try are traversable, which means that (if Cats's implicits are in scope) they have a sequence method that can swap two types, for example converting List[Try[T]] to Try[List[T]] (failing if any of the items in the list are failure).

  2. Almost all of the container types support a map method that can operate on the contents of a container, so if you have a function from A to B then map can convert a Try[A] to a Try[B]. (In Cats language they are functors but the container-like types in the standard library generally have map already.)

Cats doesn't directly support Seq, so this answer is mostly in terms of List instead.

Given that type signature, you can iteratively sequence the item you have to in effect push the list type down one level in the type chain, then map over that container to work on its contents. That can look like:

import cats.implicits._
import scala.util._

def convert(listTryOptionPair: List[Try[Option[(String, Any)]]]): Try[
  Option[Map[String, Any]]
] = {
  val tryListOptionPair = listTryOptionPair.sequence
  tryListOptionPair.map { listOptionPair =>
    val optionListPair = listOptionPair.sequence
    optionListPair.map { listPair =>
      Map.from(listPair)
    }
  }
}

https://scastie.scala-lang.org/xbQ8ZbkoRSCXGDJX0PgJAQ has a slightly more complete example.

David Maze
  • 130,717
  • 29
  • 175
  • 215
0

One way to approach this is by using a foldLeft:

// Let's say this is the object you're trying to convert
val seq: Seq[Try[Option[(String, Any)]]] = ???

seq.foldLeft(Try(Option(Map.empty[String, Any]))) {
  case (acc, e) =>
    for {
      accOption  <- acc
      elemOption <- e
    } yield elemOption match {
      case Some(value) => accOption.map(_ + value)
      case None        => accOption
    }
}

You start off with en empty Map. You then use a for comprehension to go through the current map and element and finally you add a new tuple in the map if present.

Andrei T.
  • 2,455
  • 1
  • 13
  • 28
-1

The following solutions is based on this answer to the point that almost makes the question a duplicate.

Method 1: Using recursion

def trySeqToMap1[X,Y](trySeq : Seq[Try[Option[(X, Y)]]]) : Try[Option[Map[X,Y]]] = {

  def helper(it : Iterator[Try[Option[(X,Y)]]], m : Map[X,Y] = Map()) : Try[Option[Map[X,Y]]] = {
    if(it.hasNext) {
      val x = it.next()
      if(x.isFailure)
        Failure(x.failed.get)
      else if(x.get.isDefined)
        helper(it, m + (x.get.get._1-> x.get.get._2))
      else
        helper(it, m)
    } else Success(Some(m))
  }

  helper(trySeq.iterator)
}

Method 2: directly pattern matching in case you are able to get a stream or a List instead:

  def trySeqToMap2[X,Y](trySeq : LazyList[Try[Option[(X, Y)]]], m : Map[X,Y]= Map.empty[X,Y]) : Try[Option[Map[X,Y]]] =
    trySeq match {
      case Success(Some(h)) #:: tail => trySeqToMap2(tail, m + (h._1 -> h._2))
      case Success(None) #:: tail => tail => trySeqToMap2(tail, m)
      case Failure(f) #:: _ => Failure(f)
      case _ => Success(Some(m))
    }

note: this answer was previously using different method signatures. It has been updated to conform to the signature given in the question.

jrook
  • 3,459
  • 1
  • 16
  • 33