Scala模式匹配的简化或替代方案

时间:2013-11-30 23:43:04

标签: scala playframework-2.0 pattern-matching

我已经实现了Play! 2 Scala中的QueryStringBindable用于Range类型。范围由最小值或最大值或两者组成(Float类型)。在我的QueryBindable实现中,我使用internalBinder将两个可能的参数min和max转换为Option [Either [String,Float]],将它们组合在一个元组中,对此进行模式匹配,最后返回一个Option [要么[String,范围]]。这有效,但正如您在下面的代码中看到的那样,模式匹配非常详细。在Scala中有更简洁的方法吗? 也许以某种方式利用更高阶函数来获得相同的结果结构?

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")
      }
    }
  }
}

3 个答案:

答案 0 :(得分:0)

使用几个函数将Option[Either[Error, A]]转换为Either[Error, Option[A]],您可以在我看来更清洁一些。我还建议重命名Range,因为它与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" )
    }
  }
}

结果是:

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))

答案 1 :(得分:0)

是的,它可以简化。

对于bind方法,您可以放置​​一些通配符,当您有错误时可以简化它。这样,Range汇编逻辑只有4个排列。我不会在这里做太多的魔法因为它会使理解你的代码变得复杂。

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))
  }
}

对于unbind,我会使用不同的方法,利用Option的地图函数,然后将它们组合成Iterable,你可以调用mkString,它将不会为1做任何事情字符串,如果有两个字符串,则追加&。代码示例有类型,因此您可以更容易理解。

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("&")
  }
}

如果你是短代码:

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("&")
  }
}

答案 2 :(得分:0)

(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))))
}