scala 的偏函数、柯理化、隐式参数和隐式对象

458 阅读3分钟

偏函数

偏函数最基本的定义是,我们想给函数固定定义域。在 scala 中,我们使用PartialFunction 来实现,定义如下:

object TestFuture {
  def main(args: Array[String]): Unit = {
    val divide = new PartialFunction[Int, Int] {
      def isDefinedAt(x: Int): Boolean = x != 0
      def apply(x: Int): Int = 100/x
    }
    println(divide(10))
    if (divide.isDefinedAt(0)) {
      println(divide(0))
    } else {
      println("can not divided by zero")
    }
  }
}

输出结果:

10
can not divided by zero

但是这类方式过于繁琐了,我们可以结合 case的方式来简化定义,比如:

object TestPartial {
  def main(args: Array[String]): Unit = {
    val divide: PartialFunction[Int, Int] = {
      case x  if x != 0 => 100/x
    }
    divide(10)
  }
}

上面的定义在除数为0的时候,仍然会抛出异常,我们可以定义下异常情况,然后组合到一起,借助orElse 来组合。给出代码示例:

object TestPartial {
  def main(args: Array[String]): Unit = {
    // 偏函数必须声明类型
    val divideNonZero: PartialFunction[Int, Int] = {
      case x  if x != 0 => 100/x
    }
        
    val divideZero: PartialFunction[Int, Int] = {
      case 0 => -1
    }
    
    // 组合后仍是偏函数
    val divide = divideNonZero orElse divideZero

    println(divide(10))  // 10
    println(divide(0))   // -1
  }
}

上面的定义方式,其实就是组合了一个match 块,等加成下面的定义方式:

def div(x: Int): Int = {
  x match {
    case 0 => -1
    case _ => 100/x
  }
}

只不过偏函数的方式更加函数化了一些。

除了orElse之外,我们还有andThen 方式,顾名思义,衔接前后的结果。举个例子:

object TestPartial {
  def main(args: Array[String]): Unit = {
    val add1: PartialFunction[Int, Int] = {
      case x => x + 1
    }

    val add2: PartialFunction[Int, Int] = {
      case x => x + 2
    }

    val add = add1 andThen add2

    println(add(3))  // 6
  }
}

可以看出,andThen 按照链式的方式衔接前后的结果。

柯理化

我们先看一下函数柯理化的定义:zh.wikipedia.org/wiki/柯里化

scala 的柯理化是指,把接受两个参数函数,固定一个参数后,返回一个新函数的行为。给出代码示例:

object TestCurry {
  def main(args: Array[String]): Unit = {
    def multiply(a: Int)(b: Int) = a * b
    def mul3 = multiply(3)_  // 注意这里的下划线
    println(mul3(10))
  }
}

柯理化之后,我们可以省去每次传入的固定参数了,抽象层级也更高了。

隐式参数

先给个例子:

object TestImplicit {
  def main(args: Array[String]): Unit = {
    val value = 10
    implicit val multiplier: Int = 3
    def multiply(implicit by: Int) = value * by
    println(multiply)      // 30
    println(multiply(10))  // 100
  }
}

函数在声明隐式参数implicit 之后,可以不去显示的指定参数。如果不指定参数,则会先找当前代码块中声明为implicit 同类型的数据进行填充;如果当前代码块不存在,则继续向上找;知道最后找不到的话,报错。

隐式参数结合柯理化用更加广泛的用途,尤其是在各类基础代码库中。我们给一个简单的例子,说明如何结合的:

object TestImplicitCurry {
  def main(args: Array[String]): Unit = {
    implicit val op = "add"
    def numOp(a: Int, b: Int)(implicit op: String): Int = {
      op match {
        case "add" => a + b
        case _ => 0
      }
    }
    val result = numOp(1, 2)
    
    println(result)  // 3
  }

numOp 会自动使用op 参数。

隐式对象

scala 的class 也可以声明为implicit 的。implicit 可以对传入的class进行包装。被包转的类可以直接调用implict包转类的方法。举个例子说明:

object TestFuture {
  def main(args: Array[String]): Unit = {
    val t = 5
    t times println("HI") 
  }

  implicit class IntWithTimes(x: Int) {
    def times[A](f: => A): Unit = {
      var cur = x;
      while (cur > 0) {
        f
        Thread.sleep(1000)
        cur -= 1
      }
    }
  }
}

main 函数的Int类型的t没有times方法,但是scala直接把t隐式转换成了IntWithTimes,此时就有了对应的方法了。

implicit class 必须定义在object class 或者trait 内部,而且必须要有方法,同时不能是case class。可以使用一个隐式参数