使用 Option[] 参数保护函数操作的正确方法
Proper way to guard function operations using Option[] arguments
我有代码,其中 class 可以提供自身的修改副本,如下所示:
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))
因为在我的实际代码中,如果没有提供 oi
或 os
,我不应该 运行 它们相关的 foo
和 bar
函数。换句话说,我没有 oi
和 os
的默认参数,而是它们的存在决定了我是否应该 运行 某些函数。
当前解决方案,扩展class:
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
以提供此功能吗?
在选项 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 (_, _) =>
}
好吧...您可以使用反射为您的情况创建任意 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
s 进行复制,这里是您定义一次并用于所有大小写 类 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)
另一种选择(没有双关语意,呵呵)是让 foo
和 bar
自己接管并折叠 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,您也可以重载 foo
和 bar
以采用普通的 Int
和 String
;在这种情况下,使 Option
-taking 方法调用其相应的非 Option
-taking 方法。
我有代码,其中 class 可以提供自身的修改副本,如下所示:
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))
因为在我的实际代码中,如果没有提供 oi
或 os
,我不应该 运行 它们相关的 foo
和 bar
函数。换句话说,我没有 oi
和 os
的默认参数,而是它们的存在决定了我是否应该 运行 某些函数。
当前解决方案,扩展class:
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
以提供此功能吗?
在选项 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 (_, _) =>
}
好吧...您可以使用反射为您的情况创建任意 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
s 进行复制,这里是您定义一次并用于所有大小写 类 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)
另一种选择(没有双关语意,呵呵)是让 foo
和 bar
自己接管并折叠 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,您也可以重载 foo
和 bar
以采用普通的 Int
和 String
;在这种情况下,使 Option
-taking 方法调用其相应的非 Option
-taking 方法。