Scala中的函数式编程总结

函数式编程:

函数式编程是种编程方式,它将电脑运算视为函数的计算。函数编程语言最重要的基础是λ演算(lambda calculus),而且λ演算的函数可以接受函数当作输入(参数)和输出(返回值)。
和指令式编程相比,函数式编程强调函数的计算比指令的执行重要。
和过程化编程相比,函数式编程里函数的计算可随时调用。
简单说,“函数式编程"是一种"编程范式”(programming paradigm),也就是如何编写程序的方法论。
它属于"结构化编程"的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。

函数式编程一般都包括:

1.高阶函数
2.支持 闭包
3.类型推断
4.无副作用 无IO操作,不改变状态
5.引用透明 同一函数,传递相同参数,必然返回同一个结果。

函数式编程优点:

  1. 代码简洁,开发快速
  2. 接近自然语言,易于理解
    函数式编程的自由度很高,可以写出很接近自然语言的代码。
    将表达式(1 + 2) * 3 - 4,写成函数式语言:
    subtract(multiply(add(1,2), 3), 4)
    对它进行变形,不难得到另一种写法:
    add(1,2).multiply(3).subtract(4)
  3. 更方便的代码管理
    函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。
  4. 易于"并发编程"
    函数式编程不需要考虑"死锁"(deadlock),因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"(concurrency)。
    多核CPU是将来的潮流,所以函数式编程的这个特性非常重要。
  5. 代码的热升级
    函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机。Erlang语言早就证明了这一点,它是瑞典爱立信公司为了管理电话系统而开发的,电话系统的升级当然是不能停机的。

1.函数(头等函数)

1.1在Scala中,函数能作为参数进行传递,函数能调用满足参数要求的不同的函数作为参数
1.2函数是一个值
1.3字面量函数(匿名函数)
匿名函数的作用域非常小,往往只在参数中使用,其作用范围即是调用该匿名函数参数的函数体

	(args1:T1,args2:T2) => {//....}

	(a:Int,b:Int)=>{a+b}

1.4示例:

	val sum=(a:Int,b:Int) => {a+b}

函数调用:

	sum(1,2)
	(a:Int,b:Int) => {a+b}

函数类型:

	(T1,T2,..) => ResultType 

1.5函数使用规则:
尽量val定义变量,使用纯函数,以及无副作用的函数。
1.纯函数 y=kx+b k=2 b=1
1.返回值只与参数值有关
2.参数相同时,返回值必须相同
2.副作用
IO print foreach map
修改状态 val

2.函数与方法的区别

1.方法不能作为单独的表达式而存在(参数为空的方法除外),而函数可以。
2.函数必须要有参数列表,而方法可以没有参数列表
3.方法名是方法调用,而函数名只是代表函数对象本身
4.在需要函数的地方,如果传递一个方法,会自动进行ETA展开(把方法转换为函数)
5.传名参数本质上是个方法

3.高阶函数

3.1参数列表中出现参数类型为函数,或返回值为函数时,或两个同时满足时,该函数就称为一个高阶函数(higther-order function).

例:

	def a(f:(Double) => Double)=f(0.25);

注意:该函数的参数类型为(Double)=>Double

	def b(x:Double):Double=x+1;

	a(b);
	a(ceil);
	a(sqrt _);

3.2函数的类型

	(参数类型) => 结果类型

eg:

	((Double)=>Double) => Double

3.3一些有用的高阶函数

高阶函数是将一个或多个函数作为参数,或者作为结果返回函数,或者同时返回两个函数
Scala提供了许多高阶函数,包括三类:map、filter和reduce

3.3.1 map(func)

将func函数应用到集合的所有元素,返回结果的集合;

			(1 to 9).map( (x:Int)=>{0.1*x} )
			(1 to 9).map(0.1*_)
			(1 to 9).map("*" * _).foreach(println_)

foreach(func):将fun函数应用到每个元素;

eg:map向集合的每个元素应用一个函数,返回结果的集合

	  		Range(1, 6) map ((x: Int) => x * x)
	  		(1 until 6) map ((x: Int) => x * x)
	  		"boogie" map ((ch: Char) => "aeiou" contains ch)
	  		def addS(str: String) = str + "s"
	  		List("dog", "cat", "horse") map addS

3.3.2 filter(predicate)

将一个谓词(布尔函数)应用于集合的每个元素,并返回满足该布尔函数的那些元素的集合(谓词返回true);
eg:

		(1 to 9).filter(_%2==0)
		"University" filter ((ch: Char) => ch > 'm')
"Scala is a good language".split(" ") filter ((w: String) => w.length >= 5)

3.3.3 reduce(func):对集合中的元素对进行重复的二进制操作,返回单个值

eg:

		(1 to 10) reduce ((x: Int, y: Int) => x + y)
		(1 to 10) reduce ((x: Int, y: Int) => x * y)
		List("one", "two", "three") reduce ((x: String, y: String) => x + y)

更精简的方法调用:

		(1 to 10).sum
		(1 to 10).product 
		res17: Int = 3628800
		List("one", "two", "three").mkString

3.3.4 其他高阶函数练习

分割列表:

		"one two three" takeWhile ((ch: Char) => ch != ' ')
		"one two three" takeWhile (_ != ' ')
		"one two three" dropWhile (_ != ' ')
		"one two three" span (_ != ' ')
		"one two three" partition (_ != ' ')
		List(3, 5, 6, 8, 9) partition (_ % 2 == 0)

测试所有元素

sequence.forall(predicate) 检查序列的每个元素是否满足谓词
eg:

		List(1, 2, 3) forall (_ > 0)

sequence.exists(predicate)检查序列中的任何元素是否满足谓词
eg:

		List(1, 2, 3) exists (_ < 0)

理解下划线
如果有多个参数,有时可以为每个参数使用下划线
第一个下划线表示第一个参数,第二个下划线表示第二个参数,等等。

		List(5, 3, 4, 2, 1) sortWith (_ < _)
		"This is a list of words".split(" ") sortWith (_.length < _.length)

list.find(predicate) 根据predicate查找list,如果查找到满足条件的值,返回Some(value),如果没有查找到返回None

		List(3, 1, 4, 1, 6) find (_ > 3)
		List(3, 1, 4, 1, 6) find (_ > 7)
		"Read the assignment carefully".split(" ") find (_.length > 6)
		val digits = Math.PI.toString
		List(3, 1, 4, 1, 6) find (_ > 3)
		digits find (_ > 3)
		digits find (_ > '3')
		3 == '3'
		'3'.toInt

foreach:与前面讨论的高阶函数不同,foreach的返回值是Unit,()
foreach处理集合的每个元素,并用于它的副作用

		(1 to 10) foreach (x => print(x * x + " "))
		var sum = 0; (1 to 10) foreach (x => sum += x * x)

注意:Scala是"多范式":它是面向对象和函数式语言,不允许或至少试图避免副作用,foreach的用途是产生副作用!如果想使用副作用的高阶函数,请优先使用foreach。

3.4为什么选择高阶函数

1.使用高阶函数使代码更短以及更容易阅读
2.高阶函数使某些任务更加容易
注意:就像其他任何事情一样,学习简单有效地使用高阶函数需要反复练习。

4.函数的类型推断

	def getSum(f:Int=>Int):Int=f(1)
	getSum( (a:Int)=> {a+1} )
	getSum( (a:Int)=> a+1 )
	getSum( (a) => a+1 )
	getSum( a => a+1 )
	getSum( _+1)

5.闭包

在scala中,你可以在任何作用域内定义函数:包,类甚至是另一个函数或方法。
在函数体内,你可以访问到相应作用域内的任何变量。这听上去没什么大不了,但请注意,你的函数可以在变量不再处于作用域内时被调用。
eg:

def mulBy(factor:Double)=(x:Double)=>factor*x

考虑如下调用:

	val	triple=mulBy(3);
	val	half=mulBy(0.5);
	println(triple(14)+"   "+half(14))  打印出 42 7
	val f1=(a:Int)=>a  
	val f2=(a:Int)=>a*2 
	val f3=(a:Int)=>a*3
	def mul(a:Int) = (b:Int) => b*a  
	val f1=mul(1) 
	val f2=mul(2)  
	val f3=mul(3)

定义:闭包由代码和代码用到的任何非局部变量定义构成。
对于mul这个函数来说,每一个返回的函数都有自己的a设置。这样一个函数被称为闭包(closure).

6.柯里化函数

6.1 定义:

柯里化是指将接受两个参数的函数变成新的数的函数的接受一个参过程,新的函数返回一个以原有第二个参数作为参数的函数。

	def mul(a:Int,b:Int)=a*b 
	-->mul : (Int,Int) => Int
		mul(2,3)
	def mul(a:Int) = (b:Int) => a*b 
		mul(2)(3)

支持如下简写:

		def mul(a:Int)(b:Int)=a*b  
		-->mul : (Int)(Int) => Int
			mul(2)(3)

6.2 示例:

corresponds方法可以比较两个序列是否在某个比对条件下相同。
例如:

		val a=Array("Hello","World");
		val b=Array("hello","world");
		a.corresponds(b)(_.equalsIgnoreCase(_))

注意函数_.equalsIgnoreCase(_)是以一个经过柯里化的参数的形式传递的,

		def corresponds[B](that:Seq[B])(p:(A,B)=>Boolean):Boolean

在这里,that序列和前提函数p是分开的两个柯里化的参数。类型推断器可以分析出B出自that的类型,因此就可以利用这个信息来分析作为参数p传入的函数。拿本例来说,that是一个String类型的序列。因此,前提函数应有的来行为(String,String)=>Boolean。有了这个信息,编译器就可以接受(.equalsIgnoreCase())作为(a:String,b:String)=>a.equalsIgnoreCase(b)的简写了。

	def A(a:T1,b:T2,c:T3,d:T4) = E  
	def A = (a:T1) => (b:T2) => (c:T3) => (d:T4) => E
	def A(a:T1)(b:T2)(c:T3)(d:T4) = E  

6.3意义:

使用scala柯里化风格可以简化主函数的复杂度,提高主函数的自闭性,提高功能上的可扩张性(事实证明:流水化生产是最高效和安全的,代码编写也一样,一个函数实现维护一个功能的完成处理(逻辑处理,相关异常处理等),也是极简设计,优秀代码体检的追求)。

6.4具体适用场景

4.1例如 设计一个获取本地文本文件的所有行数据的功能,主函数功能主要是创建文件流读取文件的所有行,在读取过程中,需要做很多的辅助操作如判断本地文件是否存在和可读和关闭文件流。使用scala的函数式柯里化代码看上去将变得非常优雅

		def getLinesMain(filename:String):List[String]={
    		getLines(filename)(isReadable)(closeStream)
		}

		def getLines(filename: String)(isFileReadable: (File) => Boolean)(closableStream: (Closeable) => Unit):List[String] = {
		    val file = new File(filename)
		    if (isFileReadable(file)) {
		      val readerStream = new FileReader(file)
		      val buffer = new BufferedReader(readerStream)
		      try {
				var list: List[String] = List()
				var str = ""
				var isReadOver = false
				while (!isReadOver) {
				  str = buffer.readLine()
				  if (str == null) isReadOver = true
				  else list = str :: list
				}
				list.reverse
		      } finally {
				closableStream(buffer)
				closableStream(readerStream)
		      }
		    } else {
		      List()
		    }
		  }

		  def isReadable(file: File) = {
		    if (null != file && file.exists() && file.canRead()) true
		    else false
		  }
		  def closeStream(stream: Closeable) {
		    if (null != stream) {
		      try {
			stream.close
		      } catch {
			case ex => Log.error(“[”+this.getClass.getName+”.closeStream]”,ex.getMessage)
		      }
		    }
		  }

使用柯里化特性可以将复杂逻辑简单化,并能将很多常漏掉的主函数业务逻辑之外的处理暴露在函数的定义阶段,提高代码的健壮性,使函数功能更加细腻化和流程化。
4.2例如 使用REST风格的HTTP资源请求的基于三层架构的MVC模式的WEB开发中前端的请求服务器段处理过程主要包含:第一步服务器端接受用户资源请求,第二步前端调度器代理接受用户资源请求,第三步检查当前请求的合法性,第四步创建相应的申请过滤处理链处理请求,第五步创建渲染视图,第六步响应用户请求。

如果使用scala编写流程控制函数将非常简单和易于理解

	/**
	   * 用户资源请求=>调度器代理用户资源请求=>检查请求的合法性=>创建相应的资源申请责任链返回Model数据和视图URI=>创建视图=>响应用户请求
	   */
	  def serviceUserRequest[IN,M,V,OUT](requstInputData: IN)(dipatcherDelegeteOp: IN => M)(checkRequestValid: M => Boolean)(filterChains: M => M)(createResponseRestURLView: M => V)(createResponseStream:V=>OUT): OUT = {
	    val request = dipatcherDelegeteOp(requstInputData)
	    if (checkRequestValid(request)) {
	      val model = filterChains(request)
	      val view=createResponseRestURLView(model)
		 createResponseStream(view)
	    } else {
	      //error business handler
	      “return error view URI Stream"
	    }
	  }

7.部分应用函数

7.1定义

一个函数有N个参数, 而我们为其提供少于N个参数, 那就得到了一个新的函数,这个新的函数就称为原始函数的部分应用函数.

7.2.省略的参数可以使用“”来代替整个参数列表或使用“:类型”来代替某一个参数。

比如说,可以使用 println 来代替 println ().
	someNumbers.foreach(println _)
Scala 编译器自动将上面代码解释成:
	someNumbers.foreach( x => println (x))
因此这里的“_” 代表了 println 的整个参数列表,而不仅仅替代单个参数。
比如:一个加法函数。
	scala> def sum = (_:Int) + (_ :Int) + (_ :Int)
	sum: (Int, Int, Int) => Int
	scala> sum (1,2,3)
	res0: Int = 6
	产生一个部分应用函数:
	scala> val b = sum ( 1 , _ :Int, 3)
	b: Int => Int = <function1>
	scala> b(2)
	res1: Int = 6

变量 b 的类型为一函数,具体类型为 Function1(带一个参数的函数),它是由 sum 应用了第一个和第三个参数,构成的。调用b(2),实际上调用 sum (1,2,3)。

8.偏函数 PartialFuncation[ArgsType,ReturnType]

8.1定义

	被包在花括号内的一组case语句是一个偏函数--一个并非对所有输入值都有定义的函数。它是PartialFuncation[A,B]类的一个实例。(A是参数类型,B是返回类型)
	Scala中的PartialFunction是一个Trait,其的类型为PartialFunction[A,B],其中接收一个类型为A的参数,返回一个类型为B的结果。
	偏函数和其它函数一样,也定义了apply方法,apply方法会从匹配到的模式计算函数值。该特质有1个方法抽象方法:def isDefinedAt(a: A):Boolean,isDefinedAt方法决定了该方法的参数是否在给定的偏函数的定义域内,如果返回结果为true,表示在,否则不在。
	例如:
		scala> val pf:PartialFunction[Int,String] = {
		     |   case 1=>"One"
		     |   case 2=>"Two"
		     |   case 3=>"Three"
		     |   case _=>"Other"
		     | }
		pf: PartialFunction[Int,String] = <function1>

		scala> pf(1)
		res0: String = One

		scala> pf(2)
		res1: String = Two

		scala> pf(3)
		res2: String = Three

		scala> pf(4)
		res3: String = Other

8.2偏函数内部有一些方法

比如isDefinedAt、OrElse、 andThen、applyOrElse等等。

1.isDefinedAt : 这个函数的作用是判断传入来的参数是否在这个偏函数所处理的范围内。
刚才定义的pf来尝试使用isDefinedAt(),只要是Int类型都是正确的,因为有case _=> "Other"这一句。如果换成其他类型就会报错。
如果将case _=> "Other"这一行去掉,执行pf(4)则会抛出MatchError异常
2.orElse : 将多个偏函数组合起来使用,效果类似case语句。

		scala> val onePF:PartialFunction[Int,String] = {
		     |   case 1=>"One"
		     | }
		onePF: PartialFunction[Int,String] = <function1>

		scala> val twoPF:PartialFunction[Int,String] = {
		     |   case 2=>"Two"
		     | }
		twoPF: PartialFunction[Int,String] = <function1>

		scala> val threePF:PartialFunction[Int,String] = {
		     |   case 3=>"Three"
		     | }
		threePF: PartialFunction[Int,String] = <function1>

		scala> val otherPF:PartialFunction[Int,String] = {
		     |   case _=>"Other"
		     | }
		otherPF: PartialFunction[Int,String] = <function1>

		scala> val newPF = onePF orElse twoPF orElse threePF orElse otherPF
		newPF: PartialFunction[Int,String] = <function1>

		scala> newPF(1)
		res0: String = One

		scala> newPF(2)
		res1: String = Two

		scala> newPF(3)
		res2: String = Three

		scala> newPF(4)
		res3: String = Other

这样,newPF跟原先的pf效果是一样的。

8.3 andThen

相当于方法的连续调用,比如g(f(x))。

		scala> val pf1:PartialFunction[Int,String] = {
		     |   case i if i == 1 => "One"
		     | }
		pf1: PartialFunction[Int,String] = <function1>

		scala> val pf2:PartialFunction[String,String] = {
		     |   case str if str eq "One" => "The num is 1"
		     | }
		pf2: PartialFunction[String,String] = <function1>

		scala> val num = pf1 andThen pf2
		num: PartialFunction[Int,String] = <function1>

		scala> num(1)
		res4: String = The num is 1

pf1的结果返回类型必须和pf2的参数传入类型必须一致,否则会报错。

8.4 applyOrElse

它接收2个参数,第一个是调用的参数,第二个是个回调函数。如果第一个调用的参数匹配,返回匹配的值,否则调用回调函数。
scala> onePF.applyOrElse(1,{num:Int=>“two”})
res5: String = One

		scala> onePF.applyOrElse(2,{num:Int=>"two"})
		res6: String = two

在这个例子中,第一次onePF匹配了1成功则返回的是"One"字符串。第二次onePF匹配2失败则触发回调函数,返回的是"Two"字符串。

9.控制抽象 自定义控制结构

9.1 Scala中,可以将一系列语句归组成不带参数也没有返回值的函数。 () => 
	def runInThread(block:()=>Unit){
		new Thread {
			override def run(){
				block()
			}
		}.start()
	}
	 //调用
	runInThread{()=>println("Hi");Thread.sleep(10000);println("Bye")}

	可以去掉调用中的()=>,在参数声明和调用该函数参数的地方略去(),保留=>。
	def runInThread(block: => Unit) {
		new Thread {
			override def run () { block }
		}.start()
	}

	// 调用
	runInThread { println("Hi"); Thread.sleep(10000); println("Bye") }

2.Scala程序员可以构建控制抽象:看上去像是编程语言关键字的函数。
eg:

		def until(condition: =>Boolean)(block: =>Unit){
			if(!condition){
				block
				until(condition)(block)
			}
		}

调用:

		var x=10
		until(x==0){
			x-=1
			println(x)
		}		

这样的函数参数专业术语叫做传名参数(常规的参数叫传值参数)。函数在调用时,传名参数的表达式不会被求值,表达式会被当做参数传递下去。
3.控制抽象总结:
函数 = 通用部分 + 非通用部分
通用部分:函数体
非通用部分:​参数提供
​ 在这种函数的每一次调用中,你都可以把不同的函数值作为参数传入,于是被调用函数将在每次选用参数的时候调用传入的函数值。这种高阶函数——带其他函数做参数的函数。
1、减少代码重复,使用高阶函数
2、使用闭包减少代码重复
3、特定用途循环架构:scala的集合类型的特定用途循环反复提供了一个很好的例子。这些特殊目的的循环方法定义在特质Iterable中,被List、Set、Array、Map扩展。​
4、柯里化
scala允许创建新的"感觉像是原生语言支持"的控制抽象。为了搞明白如何让控制抽象感觉更像语言的扩展,需要明白称为柯里化的函数式编程技巧​
4.传名参数和传值参数
1.定义:
Scala的解释器在解析函数参数(function arguments)时有两种方式:
先计算参数表达式的值(reduce the arguments),再应用到函数内部;或者是将未计算的参数表达式直接应用到函数内部。
前者叫做传值调用(call-by-value),后者叫做传名调用(call-by-name)。

eg:

		package com.doggie  
		object Add {  
			def addByName(a: Int, b: => Int) = a + b   
		 	def addByValue(a: Int, b: Int) = a + b   
		}

addByName是传名调用,addByValue是传值调用。语法上可以看出,使用传名调用时,在参数名称和参数类型中间有一个" =>"符号。

eg:以a为2,b为2+2为例,他们在Scala解释器进行参数规约(reduction)时的顺序分别是这样的:

		  addByName(2, 2 + 2)  
		->2 + (2 + 2)  
		->2 + 4  
		->6  
		  
		  addByValue(2, 2 + 2)  
		->addByValue(2, 4)  
		->2 + 4  
		->6

可以看出,在进入函数内部前,传值调用方式就已经将参数表达式的值计算完毕,而传名调用是在函数内部进行参数表达式的值计算的。
这就造成了一种现象,每次使用传名调用时,解释器都会计算一次表达式的值。对于有副作用(side-effect)的参数来说,这无疑造成了两种调用方式结果的不同。
2.两者的比较
2.1传值调用在进入函数体之前就对参数表达式进行了计算,这避免了函数内部多次使用参数时重复计算其值,在一定程度上提高了效率。
2.2传名调用的一个优势在于,如果参数在函数体内部没有被使用到,那么它就不用计算参数表达式的值了。在这种情况下,传名调用的效率会高一点。
2.3下面我们以一个具体的例子来说明传名参数的用法:

	var assertionsEnabled=true
	def myAssert(predicate: () => Boolean ) =
	 if(assertionsEnabled && !predicate())
	 throw new AssertionError

这个myAssert函数的参数为一个函数类型,如果标志assertionsEnabled为True时,mymyAssert 根据predicate 的真假决定是否抛出异常,如果assertionsEnabled 为false,则这个函数什么也不做。

这个定义没什么问题,但调用起来看起来却有些别扭,比如:

			myAssert(() => 5 >3 )

还需要 ()=> ,你可以希望直接使用 5>3,但此时会报错:

			scala> myAssert(5 >3 )
			<console>:10: error: type mismatch;
			 found   : Boolean(true)
			 required: () => Boolean
				      myAssert(5 >3 )

此时,我们可以把按值传递(上面使用的是按值传递,传递的是函数类型的值)参数修改为按名称传递的参数,修改方法,是使用 => 开始而不是 ()=>来定义函数类型,如下:

			def myNameAssert(predicate:  => Boolean ) =
			  if(assertionsEnabled && !predicate)
			    throw new AssertionError

此时你就可以直接使用下面的语法来调用myNameAssert:

				myNameAssert(5>3)

此时就和Scala内置控制结构一样了,看到这里,你可能会想我为什么不直接把参数类型定义为Boolean,比如:

				def boolAssert(predicate: Boolean ) =
				  if(assertionsEnabled && !predicate)
				    throw new AssertionError

调用也可以使用

				boolAssert(5>3)

和myNameAssert 调用看起来也没什么区别,其实两者有着本质的区别,一个是传值参数,一个是传名参数,在调用boolAssert(5>3)时,5>3是已经计算出为true,然后传递给boolAssert方法,而myNameAssert(5>3),表达式5>3没有事先计算好传递给myNameAssert,而是先创建一个函数类型的参数值,这个函数的apply方法将计算5>3,然后这个函数类型的值作为参数传给myNameAssert。

因此这两个函数一个明显的区别是,如果设置assertionsEnabled 为false, 然后试图计算 x/0 ==0,

			scala> assertionsEnabled=false
			assertionsEnabled: Boolean = false

			scala> val x = 5
			x: Int = 5

			scala> boolAssert ( x /0 ==0)
			java.lang.ArithmeticException: / by zero
			  ... 32 elided

			scala> myNameAssert ( x / 0 ==0)

可以看到boolAssert 抛出 java.lang.ArithmeticException: / by zero 异常,这是因为这是个传值参数,首先计算 x /0 ,而抛出异常,而 myNameAssert 没有任何显示,这是因为这是个传名参数,传入的是一个函数类型的值,不会先计算x /0 ==0,而在myNameAssert 函数体内,由于assertionsEnabled为false,传入的predicate没有必要计算(短路计算),因此什么也不会打印。如果我们把myNameAssert 修改下,把predicate放在前面:

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值