Scala: short form of pattern matching that returns Boolean

后端 未结 6 2085
灰色年华
灰色年华 2020-12-01 03:28

I found myself writing something like this quite often:

a match {     
  case `b` => // do stuff
  case _ => // do nothing
}

Is there

相关标签:
6条回答
  • 2020-12-01 03:57

    This is exactly why I wrote these functions, which are apparently impressively obscure since nobody has mentioned them.

    scala> import PartialFunction._
    import PartialFunction._
    
    scala> cond("abc") { case "def" => true }
    res0: Boolean = false
    
    scala> condOpt("abc") { case x if x.length == 3 => x + x }
    res1: Option[java.lang.String] = Some(abcabc)
    
    scala> condOpt("abc") { case x if x.length == 4 => x + x }
    res2: Option[java.lang.String] = None
    
    0 讨论(0)
  • 2020-12-01 03:57

    The best I can come up with is this:

    def matches[A](a:A)(f:PartialFunction[A, Unit]) = f.isDefinedAt(a)
    
    if (matches(a){case ... =>}) {
        //do stuff
    }
    

    This won't win you any style points though.

    0 讨论(0)
  • 2020-12-01 04:06

    This might help:

    class Matches(m: Any) {
        def matches[R](f: PartialFunction[Any, R]) { if (f.isDefinedAt(m)) f(m) }
    }
    implicit def any2matches(m: Any) = new Matches(m)
    
    scala> 'c' matches { case x: Int => println("Int") }                                
    
    scala> 2 matches { case x: Int => println("Int") }  
    Int
    

    Now, some explanation on the general nature of the problem.

    Where may a match happen?

    There are three places where pattern matching might happen: val, case and for. The rules for them are:

    // throws an exception if it fails
    val pattern = value 
    
    // filters for pattern, but pattern cannot be "identifier: Type",
    // though that can be replaced by "id1 @ (id2: Type)" for the same effect
    for (pattern <- object providing map/flatMap/filter/withFilter/foreach) ...
    
    // throws an exception if none of the cases match
    value match { case ... => ... }
    

    There is, however, another situation where case might appear, which is function and partial function literals. For example:

    val f: Any => Unit = { case i: Int => println(i) }
    val pf: PartialFunction[Any, Unit] = { case i: Int => println(i) }
    

    Both functions and partial functions will throw an exception if called with an argument that doesn't match any of the case statements. However, partial functions also provide a method called isDefinedAt which can test whether a match can be made or not, as well as a method called lift, which will turn a PartialFunction[T, R] into a Function[T, Option[R]], which means non-matching values will result in None instead of throwing an exception.

    What is a match?

    A match is a combination of many different tests:

    // assign anything to x
    case x
    
    // only accepts values of type X
    case x: X
    
    // only accepts values matches by pattern
    case x @ pattern
    
    // only accepts a value equal to the value X (upper case here makes a difference)
    case X
    
    // only accepts a value equal to the value of x
    case `x`
    
    // only accept a tuple of the same arity
    case (x, y, ..., z)
    
    // only accepts if extractor(value) returns true of Some(Seq()) (some empty sequence)
    case extractor()
    
    // only accepts if extractor(value) returns Some something
    case extractor(x)
    
    // only accepts if extractor(value) returns Some Seq or Tuple of the same arity
    case extractor(x, y, ...,  z)
    
    // only accepts if extractor(value) returns Some Tuple2 or Some Seq with arity 2
    case x extractor y
    
    // accepts if any of the patterns is accepted (patterns may not contain assignable identifiers)
    case x | y | ... | z
    

    Now, extractors are the methods unapply or unapplySeq, the first returning Boolean or Option[T], and the second returning Option[Seq[T]], where None means no match is made, and Some(result) will try to match result as described above.

    So there are all kinds of syntactic alternatives here, which just aren't possible without the use of one of the three constructions where pattern matches may happen. You may able to emulate some of the features, like value equality and extractors, but not all of them.

    0 讨论(0)
  • 2020-12-01 04:10

    The match operator in Scala is most powerful when used in functional style. This means, rather than "doing something" in the case statements, you would return a useful value. Here is an example for an imperative style:

    var value:Int = 23
    val command:String = ... // we get this from somewhere
    command match {
      case "duplicate" => value = value * 2
      case "negate" => value = -value
      case "increment" => value = value + 1
      // etc.
      case _ => // do nothing
    }
    println("Result: " + value)
    

    It is very understandable that the "do nothing" above hurts a little, because it seems superflous. However, this is due to the fact that the above is written in imperative style. While constructs like these may sometimes be necessary, in many cases you can refactor your code to functional style:

    val value:Int = 23
    val command:String = ... // we get this from somewhere
    val result:Int = command match {
       case "duplicate" => value * 2
       case "negate" => -value
       case "increment" => value + 1
       // etc.
       case _ => value
    }
    println("Result: " + result)
    

    In this case, you use the whole match statement as a value that you can, for example, assign to a variable. And it is also much more obvious that the match statement must return a value in any case; if the last case would be missing, the compiler could not just make something up.

    It is a question of taste, but some developers consider this style to be more transparent and easier to handle in more real-world examples. I would bet that the inventors of the Scala programming language had a more functional use in mind for match, and indeed the if statement makes more sense if you only need to decide whether or not a certain action needs to be taken. (On the other hand, you can also use if in the functional way, because it also has a return value...)

    0 讨论(0)
  • 2020-12-01 04:12

    Kim's answer can be “improved” to better match your requirement:

    class AnyWrapper[A](wrapped: A) {
      def matches(f: PartialFunction[A, Unit]) = f.isDefinedAt(wrapped)
    }
    implicit def any2wrapper[A](wrapped: A) = new AnyWrapper(wrapped)
    

    then:

    val a = "a" :: Nil
    if (a matches { case "a" :: Nil => }) {
      println("match")
    }
    

    I wouldn't do it, however. The => }) { sequence is really ugly here, and the whole code looks much less clear than a normal match. Plus, you get the compile-time overhead of looking up the implicit conversion, and the run-time overhead of wrapping the match in a PartialFunction (not counting the conflicts you could get with other, already defined matches methods, like the one in String).

    To look a little bit better (and be less verbose), you could add this def to AnyWrapper:

    def ifMatch(f: PartialFunction[A, Unit]): Unit = if (f.isDefinedAt(wrapped)) f(wrapped)
    

    and use it like this:

    a ifMatch { case "a" :: Nil => println("match") }
    

    which saves you your case _ => line, but requires double braces if you want a block instead of a single statement... Not so nice.

    Note that this construct is not really in the spirit of functional programming, as it can only be used to execute something that has side effects. We can't easily use it to return a value (therefore the Unit return value), as the function is partial — we'd need a default value, or we could return an Option instance. But here again, we would probably unwrap it with a match, so we'd gain nothing.

    Frankly, you're better off getting used to seeing and using those match frequently, and moving away from this kind of imperative-style constructs (following Madoc's nice explanation).

    0 讨论(0)
  • 2020-12-01 04:22

    Patterns can also be used in for expressions. Your code sample

    a match {     
      case b => // do stuff
      case _ => // do nothing
    }
    

    can then be expressed as

    for(b <- Some(a)) //do stuff
    

    The trick is to wrap a to make it a valid enumerator. E.g. List(a) would also work, but I think Some(a) is closest to your intended meaning.

    0 讨论(0)
提交回复
热议问题