Simplification or alternative for this Scala pattern match - scala

I have implemented a Play! 2 QueryStringBindable in Scala for a Range type. A Range consists of either a min or max value or both (of type Float). In my QueryBindable implementation I use the internalBinder to convert the two possible parameters min and max to Option[Either[String, Float]], combine them in a tuple, do a pattern match over this and finally return an Option[Either[String, Range]]. This works but as you can see in the code below the pattern match is very verbose. Is there a more concise way of doing this in Scala?
Maybe leverage higher order functions somehow to get the same result structure back?
import play.api.mvc.QueryStringBindable
case class Range(min: Option[Float], max: Option[Float])
object Range {
implicit def rangeQueryStringBindable(implicit intBinder: QueryStringBindable[Float]) = new QueryStringBindable[Range] {
override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, Range]] = {
val minOpt = intBinder.bind("min", params)
val maxOpt = intBinder.bind("max", params)
(minOpt, maxOpt) match {
case (None, None) => None
case (Some(Right(min)), Some(Right(max))) => Some(Right(Range(Some(min), Some(max))))
case (None, Some(Right(max))) => Some(Right(Range(None, Some(max))))
case (Some(Right(min)), None) => Some(Right(Range(Some(min), None)))
case (Some(Left(minError)), Some(Left(maxError))) => Some(Left(minError))
case (Some(Left(minError)), None) => Some(Left(minError))
case (None, Some(Left(maxError))) => Some(Left(maxError))
case (Some(Right(_)), Some(Left(maxError))) => Some(Left(maxError))
case (Some(Left(minError)), Some(Right(_))) => Some(Left(minError))
}
}
override def unbind(key: String, range: Range): String = {
(range.min, range.max) match {
case (Some(min), Some(max)) => intBinder.unbind("min", min) + "&" + intBinder.unbind("max", max)
case (Some(min), None) => intBinder.unbind("min", min)
case (None, Some(max)) => intBinder.unbind("max", max)
case (None, None) => throw new IllegalArgumentException("Range without values makes no sense")
}
}
}
}

(minOpt,maxOpt) match {
case (None,None) => None
case (Some(Left(m)),_) => Some(Left(m))
case (_,Some(Left(m))) => Some(Left(m))
case (_,_) => Some(Right(Range(minOpt.map(_.right.get),maxOpt.map(_.right.get))))
}

With a couple of functions to convert an Option[Either[Error, A]] to Either[Error, Option[A]] you can end up with something a bit cleaner in my view. I also recommend renaming Range since it conflicts with a class with the same name in scala.collections.immutable.
import play.api.mvc.QueryStringBindable
case class RealRange(min: Option[Float], max: Option[Float])
object BindingEitherUtils {
implicit class OptionWithEitherFlatten[A, B](value: Option[Either[A, B]]) {
def flattenRight: Either[A, Option[B]] = {
value.map { either =>
either.right.map{ right => Some(right) }
}.getOrElse{ Right(None) }
}
}
implicit class EitherWithUnflatten[A, B](value: Either[A, Option[B]]) {
def unflattenRight: Option[Either[A, B]] = {
value.fold(left => Some(Left(left)), _.map{ right => Right(right) })
}
}
}
object RealRange {
import BindingEitherUtils._
val minError = "Invalid minimum value for RealRange"
val maxError = "Invalid maximum value for RealRange"
implicit def rangeQueryStringBindable(implicit floatBinder: QueryStringBindable[Float]) = new QueryStringBindable[RealRange] {
override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, RealRange]] = {
val minOpt = floatBinder.bind("min", params).flattenRight
val maxOpt = floatBinder.bind("max", params).flattenRight
minOpt.left.map{ _ => minError }.right.flatMap { min =>
maxOpt.left.map{ _ => maxError }.right.flatMap { max =>
(min, max) match {
case (None, None ) =>
Right(None)
case (Some(minVal), Some(maxVal)) if minVal > maxVal =>
Left("Minimum value is larger than maximum value")
case _ =>
Right(Some(RealRange(min, max)))
}
}
}.unflattenRight
}
override def unbind(key: String, range: RealRange): String = {
(range.min, range.max) match {
case (Some(min), Some(max)) => floatBinder.unbind("min", min) + "&" + floatBinder.unbind("max", max)
case (Some(min), None) => floatBinder.unbind("min", min)
case (None, Some(max)) => floatBinder.unbind("max", max)
case (None, None) => throw new IllegalArgumentException("RealRange without values makes no sense")
}
}
}
def test(): Unit = {
val binder = rangeQueryStringBindable
Seq[(String, String)](
("10", "20"),
("10", null),
(null, "10"),
(null, null),
("asd", "asd"),
("10", "asd"),
("asd", "10"),
("asd", null),
(null, "asd"),
("20", "10")
).foreach{ case (min, max) =>
val params = Seq(
Option(min).map{ m => "min" -> Seq(m) },
Option(max).map{ m => "max" -> Seq(m) }
).flatten.toMap
val result = binder.bind("", params)
println(s"$params => $result" )
}
}
}
Which results in:
Map(min -> List(10), max -> List(20)) =>
Some(Right(RealRange(Some(10.0),Some(20.0))))
Map(min -> List(10)) =>
Some(Right(RealRange(Some(10.0),None)))
Map(max -> List(10)) =>
Some(Right(RealRange(None,Some(10.0))))
Map() =>
None
Map(min -> List(asd), max -> List(asd)) =>
Some(Left(Invalid minimum value for RealRange))
Map(min -> List(10), max -> List(asd)) =>
Some(Left(Invalid maximum value for RealRange))
Map(min -> List(asd), max -> List(10)) =>
Some(Left(Invalid minimum value for RealRange))
Map(min -> List(asd)) =>
Some(Left(Invalid minimum value for RealRange))
Map(max -> List(asd)) =>
Some(Left(Invalid maximum value for RealRange))
Map(min -> List(20), max -> List(10)) =>
Some(Left(Minimum value is larger than maximum value))

Yes, it can be simplified.
For the bind method you can place a few wildcards, when you have errors to simplify it. That way you only have 4 permutations for the Range assembly logic. I wouldn't do too much magic here as it would complicate understanding your code.
override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, Range]] = {
val minOpt = intBinder.bind("min", params)
val maxOpt = intBinder.bind("max", params)
(minOpt, maxOpt) match {
case (None, None) => None
case (Some(Right(min)), Some(Right(max))) => Some(Right(Range(Some(min), Some(max))))
case (None, Some(Right(max))) => Some(Right(Range(None, Some(max))))
case (Some(Right(min)), None) => Some(Right(Range(Some(min), None)))
// Error handling
case (Some(Left(minError)), _) => Some(Left(minError))
case (_, Some(Left(maxError))) => Some(Left(maxError))
}
}
For the unbind I would use a different approach, by utilizing Option's map function and then combining them into a Iterable you can call mkString and it will do nothing for 1 string and append a & if there are two strings. The code example has types, so you can understand easier.
def unbind(key: String, range: Range): String = {
val minString: Option[String] = range.min.map(min => intBinder.unbind("min", min))
val maxString: Option[String] = range.max.map(max => intBinder.unbind("max", max))
val strings: Iterable[String] = minString ++ maxString
strings match {
case Nil => throw new IllegalArgumentException("Range without values makes no sense")
case _ => strings.mkString("&")
}
}
And if you're into short code:
def unbind(key: String, range: Range): String = {
val minString = range.min.map(min => intBinder.unbind("min", min))
val maxString = range.max.map(max => intBinder.unbind("max", max))
minString ++ maxString match {
case Nil => throw new IllegalArgumentException("Range without values makes no sense")
case strings => strings.mkString("&")
}
}

Related

I don't know how to sort by condition in scala

sort in descending order of date, if same, sort by name, if same, sort by highest score
in English, math, and science.
object OptionAnswer {
case class Score(
name: String,
english: Int,
math: Int,
science: Int,
date: LocalDate
)
def main(args: Array[String]): Unit = {
val scoreSeq: Seq[Score] = List(scoreOfAlice,scoreOfBob,scoreOfCharlie,scoreOfDave)
val keys = Seq(-5, 1, 2, 3, 4)
println(sortScore(scoreSeq, keys))
}
def sortScore(scoreSeq: Seq[Score], keys: Seq[Int]): Seq[Score] = {
val keys_set = keys.toSet
keys_set match{
case 1 => scoreSeq.sortBy(score => score.name)
case 2 => scores.sortBy(score => -score.english)
case 3 => scores.sortBy(score => -score.math)
case 4 => scores.sortBy(score => -score.science)
case -1 => scoreSeq.sortBy(score => -score.name)
case -2 => scores.sortBy(score => score.english)
case -3 => scores.sortBy(score => score.math)
case -4 => scores.sortBy(score => score.science)
case _ => score
}
You need to start by writing the function to sort by a single key:
def sortByKey(scores: Seq[Score], key: Int): Seq[Score] = {
val res = math.abs(key) match {
case 1 => scores.sortBy(_.name)
case 2 => scores.sortBy(-_.english)
case 3 => scores.sortBy(-_.math)
case 4 => scores.sortBy(-_.science)
case _ => scores
}
if (key < 0) {
res.reverse
} else {
res
}
}
Then you can just use foldLeft to apply the sort to a list of keys:
def sortScores(scores: Seq[Score], keys: Seq[Int]): Seq[Score] =
keys.distinct.reverse.foldLeft(scores)(sortByKey)
The keys are distinct to remove duplicates and in reverse because the most important key is the one applied last.
The alternative is to use the list of keys to create a single complex ordering function but I'll leave someone else to write that solution.

Pattern-match against Any => List[Any] => List[Long]

Using Scala I am trying to pattern match against a class that returns a value type of Any to pull out any List[Any] and pattern match against List[Long] and List[Double].
Is there a more elegant way to do this?
Running scala 2.11
case class Accumulator (
name: Option[String],
value: Option[Any]
)
def bar[T <: Any](value: T): Unit = {
val listOfAny = value.asInstanceOf[List[Any]]
val listOfTypes = listOfAny.map(x => x.getClass.getSimpleName).toSet
listOfTypes.size match {
case 1 => listOfTypes.head match {
case "Long" => println("Long list")
case "Double" => println("Double list")
case _ => Unit
}
case _ => Unit //Probably throw an error log
}
}
def foo(accumulator: Accumulator): Unit = {
accumulator match {
case Accumulator(_, Some(value)) => value match {
case v if v.isInstanceOf[List[_]] => bar(v)
case _ => Unit
}
case _ => Unit
}
}
//Should print out "Long List"
foo(Accumulator(None, Some(List(1L, 2L, 3L))))
//Should print out "Double List"
foo(Accumulator(None, Some(List(1.0, 2.0, 3.0))))
Edit:
Was able to clean up the string matching with stable identifiers
case class Accumulator (
name: Option[String],
value: Option[Any]
)
def bar[T <: Any](value: T): Unit = {
val listOfAny = value.asInstanceOf[List[Any]]
val listOfTypes = listOfAny.map(x => x.getClass).toSet
listOfTypes.size match {
case 1 =>
val headType: Class[_] = listOfTypes.head
// Stable identifiers
val ClassOfLong: Class[java.lang.Long] = classOf[java.lang.Long]
val ClassOfDouble: Class[java.lang.Double] = classOf[java.lang.Double]
headType match {
case ClassOfLong =>
val result: Long = listOfAny.asInstanceOf[List[Long]].sum
println(s"Long List sum: $result")
case ClassOfDouble =>
val result: Double = listOfAny.asInstanceOf[List[Double]].sum
println(s"Double List sum: $result")
case _ => Unit
}
case _ => Unit //Probably throw an error log
}
}
def foo(accumulator: Accumulator): Unit = {
accumulator match {
case Accumulator(_, Some(value)) => value match {
case v if v.isInstanceOf[List[_]] => bar(v)
case _ => Unit
}
case _ => Unit
}
}
//Should print out "Long List sum: 6"
foo(Accumulator(None, Some(List(1L, 2L, 3L))))
//Should print out "Double List sum: 6.0"
foo(Accumulator(None, Some(List(1.0, 2.0, 3.0))))
If the lists are not empty and all the elements are of the same type, you can match both the returned object and the first element in the list. Something like this
def surprise(): Any = Random.nextInt(3) match {
case 0 => List(1L, 2L, 3L)
case 1 => List(0.5, 1.5, 2.5)
case _ => "foo"
}
0 to 10 foreach { _ =>
surprise() match {
case l # List(_: Long, _*) =>
println(s"Longs: $l")
case l # List(_: Double, _*) =>
println(s"Doubles: $l")
case x =>
println(s"Something else: $x")
}
}
Output:
"""
Something else: foo
Something else: foo
Longs: List(1, 2, 3)
Doubles: List(0.5, 1.5, 2.5)
Doubles: List(0.5, 1.5, 2.5)
Doubles: List(0.5, 1.5, 2.5)
Doubles: List(0.5, 1.5, 2.5)
Doubles: List(0.5, 1.5, 2.5)
Something else: foo
Longs: List(1, 2, 3)
Something else: foo
"""
case class Accumulator (
name: Option[String],
value: Option[Any]
)
def bar[T <: Any](value: T): Unit = {
val listOfAny = value.asInstanceOf[List[Any]]
val listOfTypes = listOfAny.map(x => x.getClass).toSet
listOfTypes.size match {
case 1 =>
val headType: Class[_] = listOfTypes.head
// Stable identifiers
val ClassOfLong: Class[java.lang.Long] = classOf[java.lang.Long]
val ClassOfDouble: Class[java.lang.Double] = classOf[java.lang.Double]
headType match {
case ClassOfLong =>
val result: Long = listOfAny.asInstanceOf[List[Long]].sum
println(s"Long List sum: $result")
case ClassOfDouble =>
val result: Double = listOfAny.asInstanceOf[List[Double]].sum
println(s"Double List sum: $result")
case _ => Unit
}
case _ => Unit //Probably throw an error log
}
}
def foo(accumulator: Accumulator): Unit = {
accumulator match {
case Accumulator(_, Some(value)) => value match {
case v if v.isInstanceOf[List[_]] => bar(v)
case _ => Unit
}
case _ => Unit
}
}
//Should print out "Long List sum: 6"
foo(Accumulator(None, Some(List(1L, 2L, 3L))))
//Should print out "Double List sum: 6.0"
foo(Accumulator(None, Some(List(1.0, 2.0, 3.0))))

Case Classes w/ Option Parameters & Case Matching

I have a case class that has multiple parameters of which some are Options. Here is a simplified example:
case class Foobar(a: String, b: Option[String], c: Option[CustomClass])
I want to be able to match cases of Foobar where b and/or c is not None. For example, one case could be:
testResult match {
case Foobar("str1", Some(_), None) => "good"
case Foobar("str2", None, Some(_)) => "ok"
case _ => "bad"
}
Furthermore, I want to reference the case patterns via variables and this is where I'm stuck. I want to do something like the following:
val goodPat = Foobar("str1", Some(_), None) // compile fail
val okPat = Foobar("str2", None, Some(_)) // compile fail
testResult match {
case `goodPat` => "good"
case `okPat` => "ok"
case _ => "bad"
}
Is something like this possible? Is there another way to specify "not None"? Is there another way to approach this problem?
EDIT: I'm adding more details and context to the question. I have a large List of 2-tuples representing unit tests for a particular function. The 2-tuples represent the input and expected output. Eg.
// imagine this list is much bigger and Foobar contains more Option parameters
val tests = List(
("test1", Foobar("idkfa", None, None)),
// I know these fail to compile but I need to do something like this
("test2", Foobar("idclip", Some("baz"), Some(_)),
("test3", Foobar("iddqd", Some(_), None)
)
tests.foreach(test => {
val (input, expected) = test
myFunction(input) match {
case `expected` => println("ok")
case _ => println("bad")
}
})
I think you seeking for something like this:
case class DoomOpt(s: String)
case class Foobar(a: String, b: Option[String], c: Option[DoomOpt])
def myFunction(s: String): Foobar = { // your code here }
val tests = Map[String, PartialFunction[Foobar, Unit]](
"idkfa" → { case Foobar("idkfa", None, None) ⇒ },
"test2" → { case Foobar("idclip", Some("baz"), Some(_)) ⇒ },
"test3" → { case Foobar("iddqd", Some(_), None) ⇒ },
"idspispopd" → { case Foobar("idspispopd", Some(_), None) ⇒ }
)
tests.foreach { case (input, checker) =>
if (checker.isDefinedAt(myFunction(input)))
println("ok")
else
println("bad")
}
Pattern matching uses extractors which provide the unapply function to deconstruct the object. So... you can just supply your custom extractor in this case. Create a list of these extractor test cases and apply them one by one.
case class Foobar(s: String, o: Option[Int])
trait TestExtractor {
def unapply(fbar: Foobar): Boolean
}
object somePatExtractor extends TestExtractor {
def unapply(fbar: Foobar): Boolean = fbar match {
case Foobar("yes", Some(_)) => true
case _ => false
}
}
object nonePatExtractor extends TestExtractor {
def unapply(fbar: Foobar): Boolean = fbar match {
case Foobar("yes", None) => true
case _ => false
}
}
object bazPatExtractor extends TestExtractor {
def unapply(fbar: Foobar): Boolean = fbar match {
case Foobar("yes", Some("baz")) => true
case _ => false
}
}
val testList: List[(String, TestExtractor)] = List(("test1", nonePatExtractor), ("test2", bazPatExtractor), ("test3", somePatExtractor))
val fooToTest = Foobar("yes", Some(5))
testList.foreach({
case (testName, extractor) => {
fooToTest match {
case pat # extractor() => println("testName :: " + testName + ", Result :: ok")
case _ => println("testName :: " + testName + ", Result :: bad")
}
}
})
And if you are looking for a more extendible approach then you can consider something like following,
case class Foobar(s: String, o1: Option[Int], o2: Option[String])
case class TestCondition(df: Foobar => Boolean) {
def test(foobar: Foobar): Boolean = df(foobar)
}
val o1IsNone = TestCondition(f => f.o1.isEmpty)
val o1IsSome = TestCondition(f => f.o1.isDefined)
val o2IsNone = TestCondition(f => f.o2.isEmpty)
val o2IsSome = TestCondition(f => f.o2.isDefined)
case class TestCase(tcs: List[TestCondition]) {
def test(foobar: Foobar) = tcs.foldLeft(true)({ case (acc, tc) => acc && tc.test(foobar) })
}
val testList = List[(String, TestCase)](
("test1", TestCase(List(o1IsSome, o2IsSome))),
("test2", TestCase(List(o1IsSome, o2IsNone))),
("test3", TestCase(List(o1IsNone, o2IsSome))),
("test4", TestCase(List(o1IsNone, o2IsNone)))
)
val foobarToTest = Foobar("yes", Some(5), None)
testList.foreach({
case (testName, testCase) => {
foobarToTest match {
case foobar: Foobar if testCase.test(foobar) => println("testName :: " + testName + ", Result :: ok")
case _ => println("testName :: " + testName + ", Result :: bad")
}
}
})

Can this be made prettier? Matching string to case class field

Sorry about the title, please edit it to be more descriptive if you can!
Is there a way to generalize this with scala? I have quite a few fields that can be filtered against, and this is just plain ugly! The problem I ran against was matching parameter name against the case class field, can it be done in a more general way, without this much code duplication?
get("/MostClicked") { request =>
val res = MongoDbOps.findMostClicked()
val res1 = request.params.get("source") match {
case None => res
case Some(f) => res.filter(_.source == f)
}
val res2 = request.params.get("category") match {
case None => res1
case Some(f) => res1.filter(_.category == f)
}
// more of the same...
render.plain {
res2.toJson.prettyPrint
}.toFuture
}
You can try either of the two approaches below.
case class MostClicked(
source: String,
category: String,
rating: String)
object MongoDbOps {
def findMostClicked() = List[MostClicked]()
}
class Request {
val params = Map[String, String]()
}
def get(path: String)(f: Request => String) = {
f(new Request)
}
First is to use a List of matchers and then apply them sequentially using foldLeft:
get("/MostClicked") { request =>
val res = MongoDbOps.findMostClicked()
val kfun = List(
"source" -> ((x: MostClicked, y: String) => x.source == y),
"category" -> ((x: MostClicked, y: String) => x.category == y),
"rating" -> ((x: MostClicked, y: String) => x.rating == y))
val r = kfun.foldLeft(res) { (x, y) =>
request.params.get(y._1)
.map(f => res.filter(y._2(_, f)))
.getOrElse(x)
}
r.toString
// more of the same...
render.plain {
r.toJson.prettyPrint
}.toFuture
}
Or simply make it more readable:
get("/MostClicked") { request =>
val res = MongoDbOps.findMostClicked()
val res1 = request.params.get("source")
.map(f => res.filter(_.source == f))
.getOrElse(res)
val res2 = request.params.get("category")
.map(f => res.filter(_.category == f))
.getOrElse(res1)
val res3 = request.params.get("rating")
.map(f => res.filter(_.rating == f))
.getOrElse(res2)
// more of the same...
render.plain {
res3.toJson.prettyPrint
}.toFuture
}
I had to break it down to parts and work out the types, thus the smaller methods.
It works in my simple experiment and gets rid of the duplication where it can, but might not be as readable?
Note that unless you get into reflection, you still need to create the name of the parameter and how it should be filtered.
This looks to be the same as tuxdna's answer except with types to increase readability and maintainability
SETUP
case class Request(params: Map[String, String])
case class Result(category: String, source: String)
type Filterer = (Result, String) => Boolean
case class FilterInfo(paramName: String, filterer: Filterer)
type Analyzer = FilterInfo => List[Result]
val request = Request(Map("source"->"b"))
EXTRACTION METHODS
def reduce(filterInfos: List[FilterInfo], results: List[Result]) = {
filterInfos.foldLeft(results) { (currentResult, filterInfo) =>
request.params.get(filterInfo.paramName)
.map(filterVal => currentResult.filter(filterInfo.filterer(_, filterVal)))
.getOrElse(currentResult)
}
}
USAGE
val filterInfos = List(
FilterInfo("source", (result, filterVal) => result.source == filterVal),
FilterInfo("category", (result, filterVal) => result.category == filterVal))
val res = List(Result("a","a"), Result("b", "b"))
reduce(filterInfos, res)
Used in your example it would be more like this:
get("/MostClicked") { request =>
val res = MongoDbOps.findMostClicked()
val filterInfos = List(
FilterInfo("source", (result, filterVal) => result.source == filterVal),
FilterInfo("category", (result, filterVal) => result.category == filterVal))
val finalResult = reduce(filterInfos, res)
render.plain {
finalResult.toJson.prettyPrint
}.toFuture
}

How to check if there's None in List[Option[_]] and return the element's name?

I have multiple Option's. I want to check if they hold a value. If an Option is None, I want to reply to user about this. Else proceed.
This is what I have done:
val name:Option[String]
val email:Option[String]
val pass:Option[String]
val i = List(name,email,pass).find(x => x match{
case None => true
case _ => false
})
i match{
case Some(x) => Ok("Bad Request")
case None => {
//move forward
}
}
Above I can replace find with contains, but this is a very dirty way. How can I make it elegant and monadic?
Edit: I would also like to know what element was None.
Another way is as a for-comprehension:
val outcome = for {
nm <- name
em <- email
pwd <- pass
result = doSomething(nm, em, pwd) // where def doSomething(name: String, email: String, password: String): ResultType = ???
} yield (result)
This will generate outcome as a Some(result), which you can interrogate in various ways (all the methods available to the collections classes: map, filter, foreach, etc.). Eg:
outcome.map(Ok(result)).orElse(Ok("Bad Request"))
val ok = Seq(name, email, pass).forall(_.isDefined)
If you want to reuse the code, you can do
def allFieldValueProvided(fields: Option[_]*): Boolean = fields.forall(_.isDefined)
If you want to know all the missing values then you can find all missing values and if there is none, then you are good to go.
def findMissingValues(v: (String, Option[_])*) = v.collect {
case (name, None) => name
}
val missingValues = findMissingValues(("name1", option1), ("name2", option2), ...)
if(missingValues.isEmpty) {
Ok(...)
} else {
BadRequest("Missing values for " + missingValues.mkString(", ")))
}
val response = for {
n <- name
e <- email
p <- pass
} yield {
/* do something with n, e, p */
}
response getOrElse { /* bad request /* }
Or, with Scalaz:
val response = (name |#| email |#| pass) { (n, e, p) =>
/* do something with n, e, p */
}
response getOrElse { /* bad request /* }
if ((name :: email :: pass :: Nil) forall(!_.isEmpty)) {
} else {
// bad request
}
I think the most straightforward way would be this:
(name,email,pass) match {
case ((Some(name), Some(email), Some(pass)) => // proceed
case _ => // Bad request
}
A version with stone knives and bear skins:
import util._
object Test extends App {
val zero: Either[List[Int], Tuple3[String,String,String]] = Right((null,null,null))
def verify(fields: List[Option[String]]) = {
(zero /: fields.zipWithIndex) { (acc, v) => v match {
case (Some(s), i) => acc match {
case Left(_) => acc
case Right(t) =>
val u = i match {
case 0 => t copy (_1 = s)
case 1 => t copy (_2 = s)
case 2 => t copy (_3 = s)
}
Right(u)
}
case (None, i) =>
val fails = acc match {
case Left(f) => f
case Right(_) => Nil
}
Left(i :: fails)
}
}
}
def consume(name: String, email: String, pass: String) = Console println s"$name/$email/$pass"
def fail(is: List[Int]) = is map List("name","email","pass") foreach (Console println "Missing: " + _)
val name:Option[String] = Some("Bob")
val email:Option[String]= None
val pass:Option[String] = Some("boB")
val res = verify(List(name,email,pass))
res.fold(fail, (consume _).tupled)
val res2 = verify(List(name, Some("bob#bob.org"),pass))
res2.fold(fail, (consume _).tupled)
}
The same thing, using reflection to generalize the tuple copy.
The downside is that you must tell it what tuple to expect back. In this form, reflection is like one of those Stone Age advances that were so magical they trended on twitter for ten thousand years.
def verify[A <: Product](fields: List[Option[String]]) = {
import scala.reflect.runtime._
import universe._
val MaxTupleArity = 22
def tuple = {
require (fields.length <= MaxTupleArity)
val n = fields.length
val tupleN = typeOf[Tuple2[_,_]].typeSymbol.owner.typeSignature member TypeName(s"Tuple$n")
val init = tupleN.typeSignature member nme.CONSTRUCTOR
val ctor = currentMirror reflectClass tupleN.asClass reflectConstructor init.asMethod
val vs = Seq.fill(n)(null.asInstanceOf[String])
ctor(vs: _*).asInstanceOf[Product]
}
def zero: Either[List[Int], Product] = Right(tuple)
def nextProduct(p: Product, i: Int, s: String) = {
val im = currentMirror reflect p
val ts = im.symbol.typeSignature
val copy = (ts member TermName("copy")).asMethod
val args = copy.paramss.flatten map { x =>
val name = TermName(s"_$i")
if (x.name == name) s
else (im reflectMethod (ts member x.name).asMethod)()
}
(im reflectMethod copy)(args: _*).asInstanceOf[Product]
}
(zero /: fields.zipWithIndex) { (acc, v) => v match {
case (Some(s), i) => acc match {
case Left(_) => acc
case Right(t) => Right(nextProduct(t, i + 1, s))
}
case (None, i) =>
val fails = acc match {
case Left(f) => f
case Right(_) => Nil
}
Left(i :: fails)
}
}.asInstanceOf[Either[List[Int], A]]
}
def consume(name: String, email: String, pass: String) = Console println s"$name/$email/$pass"
def fail(is: List[Int]) = is map List("name","email","pass") foreach (Console println "Missing: " + _)
val name:Option[String] = Some("Bob")
val email:Option[String]= None
val pass:Option[String] = Some("boB")
type T3 = Tuple3[String,String,String]
val res = verify[T3](List(name,email,pass))
res.fold(fail, (consume _).tupled)
val res2 = verify[T3](List(name, Some("bob#bob.org"),pass))
res2.fold(fail, (consume _).tupled)
I know this doesn't scale well, but would this suffice?
(name, email, pass) match {
case (None, _, _) => "name"
case (_, None, _) => "email"
case (_, _, None) => "pass"
case _ => "Nothing to see here"
}