使用Option []参数保护功能操作的正确方法

时间:2016-09-27 18:24:25

标签: scala optional-parameters

我有一个代码,其中一个类可以提供自身的修改副本,如下所示:

case class A(i: Int, s: String) {
  def foo(ii: Int): A = copy(i = ii)
  def bar(ss: String): A = copy(s = ss)
}

我想创建一个带有一些可选参数的函数,如果定义了这些参数,则使用这些参数创建这些修改后的副本:

def subA(a: A, oi: Option[Int] = None, os: Option[String] = None): A = {
  if (oi.isDefined && os.isDefined)
    a.foo(oi.get).bar(os.get)
  else if (oi.isDefined && !os.isDefined)
    a.foo(oi.get)
  else if (!oi.isDefined && os.isDefined)
    a.bar(os.get)
  else
    a
}

这显然是不可持续的,因为我添加了新的可选参数,我必须为每个参数组合创建案例......

我也做不到:

a.foo(oi.getOrElse(a.i)).bar(os.getOrElse(a.s))

因为在我的实际代码中,如果未提供oios,我不应该运行相关的foobar函数。换句话说,我没有oios的默认参数,而是它们的存在定义了我是否应该运行某些函数。

当前解决方案,扩展课程:

implicit class A_extended(a: A) {
  def fooOption(oi: Option[Int]): A = if (oi.isDefined) a.foo(oi.get) else a
  def barOption(os: Option[String]): A = if (os.isDefined) a.bar(os.get) else a
}

def subA(a: A, oi: Option[Int] = None, os: Option[String] = None): A = {
  a.fooOption(oi).barOption(os)
}

但是这个问题经常出现,并且经常这样做有点乏味,有类似的东西:

// oi: Option[Int], foo: Int => A
oi.ifDefinedThen(a.foo(_), a) // returns a.foo(oi.get) if oi is not None, else just a

或者我应该只扩展Option来提供此功能吗?

3 个答案:

答案 0 :(得分:2)

在选项fold

上使用final def fold[B](ifEmpty: => B)(f: A => B): B
def subA(a: A, oi: Option[Int] = None, os: Option[String] = None): A = {
       val oia = oi.fold(a)(a.foo)
       os.fold(oia)(oia.bar)
}

Scala REPL

scala> def subA(a: A, oi: Option[Int] = None, os: Option[String] = None): A = {
   val oia = oi.fold(a)(a.foo)
   os.fold(oia)(oia.bar)
  }
defined function subA

scala> subA(A(1, "bow"), Some(2), Some("cow"))
res10: A = A(2, "cow")

使用模式匹配优雅地处理选项。创建一个选项元组,然后使用模式匹配来提取内部值

val a = Some(1)

val b = Some("some string")

(a, b) match {

 case (Some(x), Some(y)) =>

 case (Some(x), _) =>

 case (_, Some(y)) =>

 case (_, _) =>

}

答案 1 :(得分:0)

嗯......您可以使用反射为您的案例类创建任意copiers甚至updaters

区别在于updater更新了case class instance,而copier创建了包含更新字段的新副本。

updater的实现可以如下完成,

import scala.language.existentials
import scala.reflect.runtime.{universe => ru}

def copyInstance[C: scala.reflect.ClassTag](instance: C, mapOfUpdates: Map[String, T forSome {type T}]): C = {
  val runtimeMirror = ru.runtimeMirror(instance.getClass.getClassLoader)
  val instanceMirror = runtimeMirror.reflect(instance)
  val tpe = instanceMirror.symbol.toType

  val copyMethod = tpe.decl(ru.TermName("copy")).asMethod
  val copyMethodInstance = instanceMirror.reflectMethod(copyMethod)

  val updates = tpe.members
    .filter(member => member.asTerm.isCaseAccessor && member.asTerm.isMethod)
    .map(member => {
      val term = member.asTerm
      //check if we need to update it or use the instance value
      val updatedValue = mapOfUpdates.getOrElse(
        key = term.name.toString,
        default = instanceMirror.reflectField(term).get
      )
      updatedValue
    }).toSeq.reverse

  val copyOfInstance = copyMethodInstance(updates: _*).asInstanceOf[C]
  copyOfInstance
}

def updateInstance[C: scala.reflect.ClassTag](instance: C, mapOfUpdates: Map[String, T forSome {type T}]): C = {
  val runtimeMirror = ru.runtimeMirror(instance.getClass.getClassLoader)
  val instanceMirror = runtimeMirror.reflect(instance)
  val tpe = instanceMirror.symbol.toType

  tpe.members.foreach(member => {
    val term = member.asTerm
    term.isCaseAccessor && term.isMethod match {
      case true =>
        // it is a case class accessor, check if we need to update it
        mapOfUpdates.get(term.name.toString).foreach(updatedValue => {
          val fieldMirror = instanceMirror.reflectField(term.accessed.asTerm)
          // filed mirrors can even update immutable fields !!
          fieldMirror.set(updatedValue)
        })
      case false => // Not a case class accessor, do nothing
    }
  })

  instance
}

由于你想使用Option来复制,这里是你的定义一次并用于所有案例类copyUsingOptions

def copyUsingOptions[C: scala.reflect.ClassTag](instance: C, listOfUpdateOptions: List[Option[T forSome {type T}]]): C = {
  val runtimeMirror = ru.runtimeMirror(instance.getClass.getClassLoader)
  val instanceMirror = runtimeMirror.reflect(instance)
  val tpe = instanceMirror.symbol.toType

  val copyMethod = tpe.decl(ru.TermName("copy")).asMethod
  val copyMethodInstance = instanceMirror.reflectMethod(copyMethod)

  val updates = tpe.members.toSeq
    .filter(member => member.asTerm.isCaseAccessor && member.asTerm.isMethod)
    .reverse
    .zipWithIndex
    .map({ case (member, index) =>
      listOfUpdateOptions(index).getOrElse(instanceMirror.reflectField(member.asTerm).get)
    })

  val copyOfInstance = copyMethodInstance(updates: _*).asInstanceOf[C]
  copyOfInstance
}

现在您可以使用这些updateInstance或copyInstance来更新或复制任何案例类的实例,

case class Demo(id: Int, name: String, alliance: Option[String], power: Double, lat: Double, long: Double)
// defined class Demo

val d1 = Demo(1, "player_1", None, 15.5, 78.404, 71.404)
// d1: Demo = Demo(1,player_1,None,15.5,78.404,71.404)

val d1WithAlliance = copyInstance(d1, Map("alliance" -> Some("Empires")))
// d1WithAlliance: Demo = Demo(1,player_1,Some(Empires),15.5,78.404,71.404)

val d2 = copyInstance(d1, Map("id" -> 2, "name" -> "player_2"))
d2: Demo = Demo(2,player_2,None,15.5,78.404,71.404)

val d3 = copyWithOptions(
  d1, List(Some(3),
  Some("player_3"), Some(Some("Vikings")), None, None, None)
)
// d3: Demo = Demo(3,player_3,Some(Vikings),15.5,78.404,71.404)


// Or you can update instance using updateInstance

val d4 = updateInstance(d1, Map("id" -> 4, "name" -> "player_4"))
// d4: Demo = Demo(4,player_4,None,15.5,78.404,71.404)

d1
// d1: Demo = Demo(4,player_4,None,15.5,78.404,71.404)

答案 2 :(得分:0)

另一个选择(没有双关语,嘿)是让foobar自己采取并折叠Option s:

case class A(i: Int, s: String) {
  def foo(optI: Option[Int]): A =
    optI.fold(this)(ii => copy(i = ii))

  def bar(optS: Option[String]): A =
    optS.fold(this)(ss => copy(s = ss))
}

然后,subA可以是最小的:

object A {
  def subA(
    a: A,
    optI: Option[Int] = None,
    optS: Option[String] = None): A =
    a foo optI bar optS
}

如果你必须维护API,你也可以重载foobar以取明IntString。在这种情况下,让Option - 采取方法调用相应的非Option - 采取方法。