【问题标题】:Proper way to guard function operations using Option[] arguments使用 Option[] 参数保护函数操作的正确方法
【发布时间】:2016-09-28 07:05:00
【问题描述】:

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

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 来提供这个功能?

【问题讨论】:

    标签: scala optional-parameters


    【解决方案1】:

    在选项final def fold[B](ifEmpty: => B)(f: A => B): B上使用fold

    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 (_, _) =>
    
    }
    

    【讨论】:

    • 这仍然需要枚举ab 的所有组合,当我有很多Option 参数时,这会变得非常难看。
    • 是的,我曾经想知道为什么 Option 是可迭代的,这是它的另一个重要原因。
    • @spiffman 使用简单的地图和平面地图组合。编辑了答案
    • @spiffman oi.map(a.foo).flatMap(a => os.map(a.bar)).getOrElse(a)
    • map / flatMap 解决方案不正确,请尝试subA(new A(5, "five"), None, Some("six"))。如果第一个选项是 None,则输出不会链接到第二个选项。
    【解决方案2】:

    嗯...您可以使用反射为您的案例类创建任意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
    }
    

    既然你想使用Options 来复制,这里是你的定义一次并与所有案例类一起使用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)
    

    【讨论】:

    • 这与我的实际问题无关,但是哇,这太酷了!我对updateInstance 之类的担心是它改变了不可变val 的值,使其变为可变。
    • 我认为您想要一个用于案例类的通用复印机,并试图使用Options 完成它。 Aso,如果您不需要更新,那么您可以使用copyInstance,它反映在实例上以捕获其复制方法,然后使用它来创建包含更新字段的副本。
    • 复制函数只是为了说明使用多个Option参数来保护某些函数是否被执行的问题。不管怎样,我很欣赏提交,很多反射 API 文档很密集,我通过阅读你的代码了解了更多关于反射的知识:)
    • 是的......你的问题看起来与我回答的完全不同。希望这个答案对寻找这种解决方案的人有所帮助。
    • 你应该将它提交给 StackOverflow 文档,Scala 反射页面中目前几乎没有:stackoverflow.com/documentation/scala/5824/reflection
    【解决方案3】:

    另一种选择(不是双关语,呵呵)是让foobar 自己接管并折叠Options:

    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-taking 方法调用它们对应的非Option-taking 方法。

    【讨论】:

      猜你喜欢
      • 2020-12-13
      • 2020-12-17
      • 2018-04-27
      • 1970-01-01
      • 1970-01-01
      • 2014-08-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多