在Scala中实现ifTrue、ifFalse、ifSome、ifNone等功能,以避免使用if(...)和简单的模式匹配。

7
在Scala中,我逐渐放弃了以控制流为导向的Java/C编程思维方式,并习惯于先获取我感兴趣的对象,然后通常对集合应用像matchmap()foreach()这样的操作。我非常喜欢它,因为现在感觉代码结构更自然、更直接。
渐渐地,我希望能够以同样的方式编写条件语句;即先获取布尔值,然后再使用 match 执行各种操作。但是,完全使用match似乎有点过头了。
比较一下:
obj.isSomethingValid match {
  case true => doX
  case false => doY
}

与我使用更接近Java的风格所写的内容相比:

if (obj.isSomethingValid)
  doX
else
  doY

然后我想起了Smalltalk的ifTrue:ifFalse:消息(以及类似的变体)。在Scala中能否编写类似的内容?

obj.isSomethingValid ifTrue doX else doY

带有变体:

val v = obj.isSomethingValid ifTrue someVal else someOtherVal

// with side effects
obj.isSomethingValid ifFalse {
  numInvalid += 1
  println("not valid")
}

此外,这种风格是否可用于像Option这样的简单二状态类型?我知道使用Option的更惯用方法是将其视为集合并在其上调用filter()map()exists(),但通常,在最后,我发现如果它被定义了,我想执行一些doX,如果没有,则执行一些doY。类似于:
val ok = resultOpt ifSome { result =>
  println("Obtained: " + result)
  updateUIWith(result) // returns Boolean
} else {
  numInvalid += 1
  println("missing end result")
  false
}

在我看来,这个(依然?)比一个完整的match看起来更好。

我提供了一个我想到的基础实现;欢迎对这种风格/技术进行一般性评论和/或更好的实现!


7
“if”语句有什么问题?你可以使用这个简单、常见的控制结构,它在所有的编程语言中都被广泛使用,或者你可以创建一个花哨的抽象,对于新的开发人员来说是一道思维障碍,并且与“if”相比并没有提供任何优势。 - ryeguy
布尔值可以使用 if,但是针对 Option 的简单 match 呢? - Jean-Philippe Pellet
对于Option,有getOrElse - Madoc
因此,以不同的方式处理每种情况听起来很好。对于ifTrue/ifFalse和ifSome/ifNone的更一致的处理方式也很适合我... - Jean-Philippe Pellet
请参阅邮件列表上的讨论:http://www.scala-lang.org/node/3496 - Jean-Philippe Pellet
3个回答

14

首先,我们可能不能重用关键字else,因为它是关键字,使用反引号将其强制视为标识符相当丑陋,因此我将改用otherwise

这里是一个实现尝试。首先,使用pimp-my-library模式向Boolean添加ifTrueifFalse。它们对返回类型R进行参数化,并接受单个按名称指定的参数,如果实现了指定条件,则应该对其进行评估。但在这样做时,我们必须允许进行otherwise调用。因此,我们返回一个名为Otherwise0的新对象(为什么是0后面会解释),它作为Option[R]存储可能的中间结果。如果当前条件(ifTrueifFalse)被实现,则它被定义,否则为空。

class BooleanWrapper(b: Boolean) {
  def ifTrue[R](f: => R) = new Otherwise0[R](if (b) Some(f) else None)
  def ifFalse[R](f: => R) = new Otherwise0[R](if (b) None else Some(f))
}
implicit def extendBoolean(b: Boolean): BooleanWrapper = new BooleanWrapper(b)

目前,这个方法有效,让我可以写作。

someTest ifTrue {
  println("OK")
}

但是如果没有以下的otherwise子句,它无法返回类型为R的值。因此,这是Otherwise0的定义:

class Otherwise0[R](intermediateResult: Option[R]) {
  def otherwise[S >: R](f: => S) = intermediateResult.getOrElse(f)
  def apply[S >: R](f: => S) = otherwise(f)
}

如果从之前的ifTrueifFalse得到的中间结果未定义,那么它会仅在这种情况下评估其传递的命名参数,这正是所期望的。类型参数化[S >: R]的影响是S被推断为命名参数实际类型的最具体公共超类型,因此例如,此片段中的r具有推断类型Fruit

class Fruit
class Apple extends Fruit
class Orange extends Fruit

val r = someTest ifTrue {
  new Apple
} otherwise {
  new Orange
}
< p > apply() 别名甚至允许您跳过otherwise方法名称,以便对短代码块进行处理:

someTest.ifTrue(10).otherwise(3)
// equivalently:
someTest.ifTrue(10)(3)

最后,这是相应的 pimp for Option:

class OptionExt[A](option: Option[A]) {
  def ifNone[R](f: => R) = new Otherwise1(option match {
    case None => Some(f)
    case Some(_) => None
  }, option.get)
  def ifSome[R](f: A => R) = new Otherwise0(option match {
    case Some(value) => Some(f(value))
    case None => None
  })
}

implicit def extendOption[A](opt: Option[A]): OptionExt[A] = new OptionExt[A](opt)

class Otherwise1[R, A1](intermediateResult: Option[R], arg1: => A1) {
  def otherwise[S >: R](f: A1 => S) = intermediateResult.getOrElse(f(arg1))
  def apply[S >: R](f: A1 => S) = otherwise(f)
}

请注意,我们现在还需要Otherwise1,这样我们不仅可以将未包裹的值方便地传递给ifSome函数参数,还可以传递给ifNone后面的otherwise函数参数。


1
我的内心被震撼了。这是我一生中见过的最美丽的代码之一 :'( - javatarz

6

你可能过于具体地看待问题了。使用管道操作符可能更好:

class Piping[A](a: A) { def |>[B](f: A => B) = f(a) }
implicit def pipe_everything[A](a: A) = new Piping(a)

现在您可以:
("fish".length > 5) |> (if (_) println("Hi") else println("Ho"))

诚然,这种方式可能没有你想要的那么优雅,但它有一个巨大的优点,就是非常灵活——任何时候你想要把一个参数放在第一位(不仅限于布尔值),你都可以使用它。

此外,你已经可以按照自己的意愿使用选项:

Option("fish").filter(_.length > 5).
  map (_ => println("Hi")).
  getOrElse(println("Ho"))

尽管这些东西可以返回一个值,但并不意味着你必须避免使用它们。需要一些时间来适应其语法;这可能是创建自己的隐式的一个有效理由。但核心功能已经存在。(如果你创建自己的隐式,请考虑使用fold[B](f: A => B)(g: => B),一旦你习惯了它,缺少中间关键字实际上非常好。)
编辑:虽然使用|>符号作为管道的标准表示法,但我更喜欢将其作为方法名使用use,因为这样def reuse[B,C](f: A => B)(g: (A,B) => C) = g(a,f(a))看起来更自然。

感谢您的解释!实际上,这个管道操作符比我的提议要通用得多。我发现缺少中间关键字会导致可读性降低,特别是当两个块紧随其后时...我可以轻松地为我的OtherwiseX对象添加一个apply()方法并将其转发给otherwise,或者反过来。正如您所写的:习惯Option的本地语法需要时间...有时候我仍然觉得它相当神秘,如果嵌套多次(此外,我每次都必须检查它实际上是集合还是Option),而ifX和otherwise对我来说似乎更清晰。 - Jean-Philippe Pellet

2

为什么不直接这样使用:

val idiomaticVariable = if (condition) { 
    firstExpression
  } else { 
    secondExpression 
  } 

我认为这非常符合惯用语! :)


就像ryeguy的评论一样,这适用于if(boolean),但是对于Option呢? - Jean-Philippe Pellet
个人而言,我认为value match { case A => doA; case B => doB() }和只有两种变量的if (cond) doA else doB;语句没有区别。此外,对于许多程序员来说,if更加熟悉,并且它表达了意图:一种二进制决策。因此,如果您有一些与opt无关的操作并且无法将其映射到这些操作,则可以编写if (opt.isDefined) doA else doB。总之,在这里看不出任何问题... - tuxSlayer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接