【问题标题】:Logic operators for non-Boolean types in ScalaScala 中非布尔类型的逻辑运算符
【发布时间】:2013-02-11 22:43:57
【问题描述】:

我喜欢可以使用布尔运算符编写的简洁代码,而不是像 Lisp、Python 或 JavaScript 这样的(通常是动态的)语言中的条件语句,就像典型的那样:

x = someString or "default string"

if someString:
    x = someString
else:
    x = "default string"

在 Scala 中,我曾想过这样的事情:

object Helpers {
  case class NonBooleanLogic[A](x: A) {
    // I could overload the default && and ||
    // but I think new operators are less 'surprise prone'
    def |||(y: => A)(implicit booleanConversion: A => Boolean) = if (x) x else y
    def &&&(y: => A)(implicit booleanConversion: A => Boolean) = if (!x) x else y
  }

  implicit def num2bool(n : Int) = n != 0

  implicit def seq2bool(s : Seq[Any]) = !s.isEmpty

  implicit def any2bool(x : Any) = x != null

  implicit def asNonBoolean[T](x: T) = NonBooleanLogic(x)
}

object SandBox {
  // some tests cases...

  1 ||| 2                                         //> res2: Int = 1

  val x : String = null                           //> x  : String = null
  x ||| "hello"                                   //> res3: String = hello

  //works promoting 2 to Float
  1.0 &&& 2                                       //> res4: Double = 2.0

  //this doesn't work :(
  1 &&& 2.0
}

但出现了两个问题:

  1. 如何使其适用于具有共同祖先的类型而不恢复为 Any 类型?
  2. 这太酷了,以前一定有人做过,可能是在一个文档更好、经过测试和全面的库中。在哪里可以找到它?

【问题讨论】:

  • 顺便说一句,这更惯用:val x = if (someString != null && someString.size() > 0) someString else "default string";
  • 它应该是 JavaScript,而不是 Scala
  • 你的第二个sn-p可以写成val x = Option(someString).filterNot(_.isEmpty).getOrElse("default string")val x = if(someString != null && someString.size() > 0) someString else "default string"
  • 我想我会将 sn-ps 更改为 Python,以免一遍又一遍地回答相同的问题 xD

标签: scala boolean type-safety


【解决方案1】:

我会坚持使用 Option[T]... 这对 Scala 来说更惯用。我也经常在验证中使用它,例如:在 Web 表单中,有时不应将空字符串视为有效的用户输入。

例如,如果你认为一个空字符串/长度为零的字符串("") 为假,任何空引用也是假,任何数字零为假,你可以编写以下隐式定义。

object MyOptionConverter
{
    implicit def toOption(any: AnyRef) = Option(any)
    implicit def toOption(str: String) = {
        Option(str).filter(_.length > 0)
    }

    implicit def toOption[T](value: T)(implicit num: Numeric[T]): Option[T] = {
        Option(value).filter(_ != 0)
    }
}

import MyOptionConverter._

println(1 getOrElse 10)   // 1
println(5.5 getOrElse 20) // 5.5
println(0 getOrElse 30)  // 30
println(0.0 getOrElse 40) // 40
println((null: String) getOrElse "Hello")  // Hello
println((null: AnyRef) getOrElse "No object") // No object
println("World" getOrElse "Hello")

如果你真的需要定义你自己的操作符,把它转换成一个持有 Option[T] 的类,然后给它添加操作符。

object MyOptionConverter
{
    class MyBooleanLogic[T](x: Option[T], origin: T)
    {
        def |||(defaultValue: T) = x.getOrElse(defaultValue)
        def &&&(defaultValue: T) = x.isDefined match {
            case true  => defaultValue
            case false => origin
        }
    }

    implicit def toOption(any: AnyRef) = {
        new MyBooleanLogic(Option(any), any)
    }
    implicit def toOption(str: String) = {
        new MyBooleanLogic(Option(str).filter(_.length > 0), str)
    }

    implicit def toOption[T](value: T)(implicit num: Numeric[T])= {
        new MyBooleanLogic(Option(value).filter(_ != 0), value)
    }
}

import MyOptionConverter._

println(1 ||| 10)   // 1
println(5.5 ||| 20) // 5.5
println(0 ||| 30)  // 30
println(0.0 ||| 40) // 40
println((null: String) ||| "Hello")  // Hello
println((null: AnyRef) ||| "No object") // No object
println("World" ||| "Hello")


println(1 &&& 10)   // 10
println(5.5 &&& 20) // 20
println(0 &&& 30)  // 0
println(0.0 &&& 40) // 0.0
println((null: String) &&& "Hello")  // null
println((null: AnyRef) &&& "No object") // null
println("World" &&& "Hello") // Hello

【讨论】:

  • 好吧,只是坚持Option 类型和getOrElse 使and 方法有点孤立......并且在创建或覆盖逻辑运算符时引入它似乎有点多余或甚至是不必要的:-/ 虽然我喜欢隐式的Numeric 转换! :D
【解决方案2】:

听起来你正在尝试想出类似 Monad 的东西。您想要做的已经内置在语言中,并且在惯用的 scala 中很常见。我不是 Monad 方面的专家,但他们说 Option 是 Monad 的一种。

你特别要求有写作能力:

val x = someString or "default string"

是什么让 someString 评估为假?在大多数语言中,您将测试 if( someString != null ),这就是您在示例中所做的。惯用的 scala 避免使用 null,而是使用 None。

所以,在 scala 语法中,你会有

val someString:Option[String] = getAString()

val someString:Option[String] = Some("whatever")

val someString:Option[String] = None

然后你会有:

val x = someString getOrElse "default string"

这几乎正是您所要求的。

如果你想自己实现这样的东西,请查看 Option 中 getOrElse 的接口(类似的版本存在于 Map 和标准库的其他地方):

final def getOrElse[B >: A](default: ⇒ B): B

在此示例中,Option someString 具有由 A 表示的类型(即 String)。B 必须是 A 或 A 的超类型。返回类型将为 B(可能是 A)。例如:

val x:Option[Int]=1
x getOrElse 1.0 // this will be an AnyVal, not Any.

AnyVal 是 Int 和 Double 最具体的共同祖先。请注意,这里是 AnyVal,而不是 Any。

如果你希望它是 Double 而不是 AnyVal,你需要 x 是一个 Option[Double] (或者你需要另一个隐式)。有一个从 Int 到 Double 的内置隐式转换,但不是从 Option[Int] 到 Option[Double]。隐式转换是您的 2 被提升为 Float 的原因,而不是因为您的布尔逻辑。

我不认为您的操作符和隐含方法是此类问题的最佳解决方案。有很多方法可以使用 Options、filter、exists、map、flatMap 等编写简洁优雅的 scala 代码,可以处理您想要执行的各种操作。

您可能会发现这很有帮助:

http://www.codecommit.com/blog/ruby/monads-are-not-metaphors

【讨论】:

  • 另请参阅:james-iry.blogspot.co.uk/2007/09/… - 您展示的所有代码示例实际上都没有依赖 Option 作为 monad。 monad 就像一个接口;它描述了一个类型可以做什么,但不一定是全部。
  • 是的......我正在回答他问题的特定部分。他更笼统的想法,将某些值转换为false 的奇怪概念,然后将值映射到值......似乎相关。不过,我可能错了有任何相关性。
【解决方案3】:

我只为||| 版本创建了一个版本。是为了展示概念。代码可能会改进,我有点匆忙。

// Create a type that does the conversion, C is the resulting type
trait Converter[A, B, C] {
  def convert(a: A, b: => B)(implicit bool: BooleanConverter[A]): C
}

trait LowerPriorityConverter {
  // We can convert any type as long as we know how to convert A to a Boolean
  // The resulting type should be a supertype of both A and B
  implicit def anyConverter[A <: C, B <: C, C] = new Converter[A, B, C] {
    def convert(a: A, b: => B)(implicit bool: BooleanConverter[A]) = if (a) a else b
  }

  // We can go more specific if we can find a view from B to A
  implicit def aViewConverter[B <% A, A] = anyConverter[A, A, A]
}

object Converter extends LowerPriorityConverter {

  // For Doubles, Floats and Ints we have a specialized conversion as long as the
  // second type is a Numeric

  implicit def doubleConverter[A <: Double: Numeric, B: Numeric] = 
    new Converter[A, B, Double] {
      def convert(a: A, b: => B)(implicit bool: BooleanConverter[A]) =
        if (a) a else implicitly[Numeric[B]].toDouble(b)
    }
  implicit def floatConverter[A <: Float: Numeric, B: Numeric] = 
    new Converter[A, B, Float] {
      def convert(a: A, b: => B)(implicit bool: BooleanConverter[A]) = 
        if (a) a else implicitly[Numeric[B]].toFloat(b)
    }
  implicit def intConverter[A <: Int: Numeric, B: Numeric] = 
    new Converter[A, B, Int] {
      def convert(a: A, b: => B)(implicit bool: BooleanConverter[A]) = 
        if (a) a else implicitly[Numeric[B]].toInt(b)
    }
}

// We have created a typeclass for the boolean converters as well, 
// this allows us to use more generic types for the converters
trait BooleanConverter[A] extends (A => Boolean)

trait LowerPriorityBooleanConverter {
  implicit def any2bool = new BooleanConverter[AnyRef] {
    def apply(s: AnyRef) = s != null
  }
}

object BooleanConverter extends LowerPriorityBooleanConverter {

  implicit def num2bool[T: Numeric] = new BooleanConverter[T] {
    def apply(n: T) = implicitly[Numeric[T]].zero != n
  }

  // Note that this could catch String as well
  implicit def seq2bool[T <% GenTraversableOnce[_]] = new BooleanConverter[T] {
    def apply(s: T) = s != null && !s.isEmpty
  }

}

// This is similar to the original post
implicit class NonBooleanLogic[A](x: A) {

  // Note that we let the implicit converter determine the return type 
  // of the method
  def |||[B, C](y: => B)(
    // make sure we have implicits for both a converter and a boolean converter
    implicit converter: Converter[A, B, C], bool: BooleanConverter[A]): C =
    // do the actual conversion
    converter.convert(x, y)
}

几个测试的结果:

1 ||| 2                                       //> res0: Int = 1
(null: String) ||| "test"                     //> res1: String = test
1.0 ||| 2                                     //> res2: Double = 1.0
1 ||| 2.0                                     //> res3: Int = 1
List() ||| Seq("test")                        //> res4: Seq[String] = List(test)
1f ||| 2.0                                    //> res5: Float = 1.0
1f ||| 2f                                     //> res6: Float = 1.0
0f ||| 2.0                                    //> res7: Float = 2.0
0 ||| 2f                                      //> res8: Int = 2
2.0 ||| 2f                                    //> res9: Double = 2.0
2.0 ||| 3.0                                   //> res10: Double = 2.0
Seq("test") ||| List()                        //> res11: Seq[String] = List(test)
"" ||| "test"                                 //> res12: String = test

如您所见,为了保留类型,我们需要使用特定的模式。我从这里对我自己的一个问题的回答中了解到这一点:How to define a method for which the returntype is based on types of argument and type parameter in Scala?

这种方法的好处是您可以为特定类型添加特定转换器,而无需更改原始代码。

【讨论】:

    猜你喜欢
    • 2014-03-27
    • 2014-05-10
    • 2015-02-06
    • 1970-01-01
    • 1970-01-01
    • 2012-03-21
    • 2018-11-30
    • 2014-02-20
    相关资源
    最近更新 更多