最近跟随慕课网课程《Scala程序设计语言》学习scala基础,随之笔记,以备后查。
函数式编程:它是一种编程范式,它是一种构建计算机程序和结构的方法和风格,它把计算当做数学函数求值的过程,并且避免了改变状态和可变的数据。
纯函数(Pure Function):或称函数的纯粹性(Purity),没有副作用(Side Effect)。
副作用:指状态的变化(mutation)。
val x = 1
def XplusY_V1(y: Int) = x + y
def XplusY_V2(y: Int) = {x += y; x }
x
> res0: Int = 1
XplusY_V1(2)
> res1: Int = 3
x
> res2: Int = 1
XplusY_V1(2)
> res3: Int = 3
x
> res4: Int = 3
在上面的伪代码中,对于V1函数执行之后,x的值没有变化,即该函数对x不具备副作用,是个纯函数。V2反之。
引用透明(Referential Transparency):对于相同的输入,总是得到相同的输出;如果 f(x) 的参数 x 和函数体都是引用透明的,那么函数 f 是纯函数。
例子:违反引用透明,对于相同的输入,append方法返回不一样,即违反引用透明性。
val x = new StringBuilder("Hello")
> x : StringBuilder = Hello
var y = x.append("World!")
> y : StringBuilder = Hello World!
var z = x.append("World!")
> z : StringBuilder = Hello World! World!
不变性(Immutability):为了获得引用透明性,任何值都不能变化。
函数是一等公民(First-class Funtion):一切都是计算,函数式编程中只有表达式,变量、函数都是表达式
表达式求值策略:Call By Value(属于 严格求值) 和 Call By Name(属于非严格求值);惰性求值(Lazy Evaluation,当定义表达式的时候,不会立即求值,第一次用到该表达式的时候才会求值)
递归函数(Recursive Function):函数式编程中没有循环,所有的循环都是用递归实现的。
函数式编程的优点:
-Dfile.encoding=UTF8-Dsbt.boot.directory=d:/Code/SBT/boot/ -Dsbt.ivy.home=d:/Code/SBT/ -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -XX:ReservedCodeCacheSize=128m -Dsbt.log.format=true
其中,-Dfile.encoding=UTF8-Dsbt.boot.directory=d:/Code/SBT/boot/和-Dsbt.ivy.home=d:/Code/SBT/两个参数表明jar包的存放位置(默认是放在系统盘用户目录下的.sbt和.ivy目录)
三种变量修饰符
val——定义immutable variable,常量
var——定义mutable variable,变量
lazy val——定义惰性求值的常量,一般,如果我们定义的变量,在后续程序中可能不会被用到,则可定义lazy val变量,该类型变量会在第一次用到的时候被求值,而var和val类型的变量则会在定义时直接求值。
可以不显式指定变量的类型,因为Scala会自动进行类型推导
val x = 10 //> x : Int = 10
val y = 20 //> y : Int = 20
//res0是scala为没有变量名的表达式取的默认名,在命令行中res可直接引用
x + x //> res0: Int = 20
val z = x*y //> z : Int = 200
// z = x+y //会报错,val定义的值为常量,一旦确定,则不允许被改变
var a = 200 //> a : Int = 200
a = 300 //var定义的变量可以被修改
val d = 20 //> d : Int = 20
val e = 30 //> e : Int = 30
lazy val f = d*e //> f: => Int
f*10 //> res1: Int = 6000
f //> res2: Int = 600
val a:Byte = 10 //> a : Byte = 10
val b:Short = 20 //> b : Short = 20
val c:Int = 30 //> c : Int = 30
val d:Long = 40 //> d : Long = 40
val e:Float = 50 //> e : Float = 50.0
val f:Double = 60.98765 //> f : Double = 60.98765
val x:Long = b //> x : Long = 20
同Java一样,低精度向高精度赋值时,不需要类型转换,反之不可以。
val m = true //> m : Boolean = true
val n = false //> n : Boolean = false
val q = 'X' //> q : Char = X
boolean型有true和false两种,字符型同java一样,用单引号表示。
Unit类似Java中的void,往往作为函数的返回值类型出现,在函数式编程中,Unit往往表示函数是有副作用的,因为函数不返回任何的值
//()是Unit类型的文字量
val u:Unit=() //> u : Unit = ()
val p = () //> p : Unit = ()
Null与Nothing
null的语义和java中null的语义是类似的,表示一个引用类型,他的值是空,在scala中通常不会使用null。
nothing的语义是程序异常终止,对一个函数而言,如果返回值是nothing,表明该函数发生了异常。
def foo() = throw new Exception("error occurred") //> foo: ()Nothing
String
构建于Java 的String之上,新增了字符串插值(interpolation)的特性 。
val name="Sung_Lee" //> name : String = Sung_Lee
s"my name is ${name}" //> res0: String = my name is Sung_Lee
用s标记开头,双引号文字量内用${}来包含已有的变量(上述的基本数据类型会当做字符串处理)。
Block
在scala中,代码块用于组织多个表达式,而代码块也是一个表达式,它最终求得的值是最后一个表达式的值。
函数
def functionName(param:ParamType,...):ReturnType={
//function body:expressions
}
上述是函数的基本定义格式,参数列表中,每个参数是”参数名:参数类型”,参数列表中参数可以有多个,中间用”,”隔开。
Eg.1
def hello(name: String): String = {
s"Hello,${name}"
} //> hello: (name: String)String
hello("lisong") //> res1: String = Hello,lisong
Eg.2,scala可以自己推断出返回类型,可以将返回值类型省略
def hello(name: String)= {
s"Hello,${name}"
} //> hello: (name: String)String
hello("lisong") //> res1: String = Hello,lisong
Eg.3
def add(x:Int,y:Int) = {
x+y
} //> add: (x: Int, y: Int)Int
add(1,2) //> res2: Int = 3
if(logical_exp) valA else valB
Eg.1
val o = 1 //> o : Int = 1
if(o == 1) o //> res5: AnyVal = 1
if(a != 1) "not one" //> res6: Any = ()
if(a != 1) "not one" else o //> res7: Any = 1
在上述代码中,第三句,a!=1为false,但又没有else语句,故该表达式的返回结果为空,在Scala中,空是Unit,即返回Unit的文字量”()”。
用于实现循环的一种推导式,本质上是由scala的函数,像mapper、reduce组合而实现的。for是一种简写方式,也是scala语法糖的一种。
Eg.1
val l = List("alice","bob","cathy") //> l : List[String] = List(alice, bob, cathy)
for(
s <- l //generator,循环遍历l,每次迭代,把l中的一个元素给s
) println(s) //> alice
//| bob
//| cathy
for {
s <- l
if (s.length() > 3) //filter,如果当前迭代的s的长度大于3,则输出,否则直接进行下次循环
} println(s) //> alice
//| cathy
//yield,意为导出,循环中,若满足s1!="",则导出当前s1到一个新的collection中
val result_for = for{
s <- l
s1 = s.toUpperCase() //variable binding
if (s1 != "")
} yield (s1) //> result_for : List[String] = List(ALICE, BOB, CATHY)
try表达式
它的语义同java中try、catch、finally的语义相同,但它不是一个语句,而是一个表达式,会返回一个值。
try {
Integer.parseInt("dog");
} catch {
case _ => 0 //"_"是通配符,他可以通配所有的对象,无论抛出什么异常,总能catch到,并返回0
} finally { //同java,无论有没有异常抛出,finally总会被执行
println("aways be printed")
} //> aways be printed
//| res8: Int = 0
match表达式
类似于Java中的switch
exp match { //主要用在pattern match中
case p1 => val1
case p2 => val2
...
case _=> valn
}
val是表达式的返回值,当所有pattern都没有被匹配的时候,总会匹配”_“,返回valn,类似于Java中的default。
val code = 1 //> code : Int = 1
val result_match = code match { //主要用在pattern match中
case 1 => "one"
case 2 => "two"
case _ => "others"
} //> result_match : String = one
Scala中有两种求值策略(Evaluation Strategy):
1.Call By Value —— 对函数实参求值,且仅求值一次
2.Call By Name —— 函数实参每次在函数体内被用到时都会求值
Scala通常使用Caal By Value,如果过函数形参类型以 => 开头,那么会使用Call By Name
def foo(x: Int) = x //call by value
def foo(x: => Int) = x //call by name
Eg.1
def test1(x: Int,y: Int): Int = x * x //> test1: (x: Int, y: Int)Int
def test2(x: => Int, y: => Int):Int = x*x //> test2: (x: => Int, y: => Int)Int
test1(3+4,8) //> res9: test1(7,8)——>7*7——>49
test2(3+4,8) //> res10: test2(3+4,8)——>(3+4)*(3+4)——>7*7——>49
Eg.2
def bar(x: Int, y: => Int): Int = 1 //> bar: (x: Int, y: => Int)Int
def loop(): Int = loop //> loop: ()Int
bar(1, loop) //> res11: Int = 1
bar(loop, 1)
在第一次调用中,loop传给形参y,但bar函数体中一直没有用到y,且形参y是call by name的,故一直不会被求值。在第二次调用中,递归表达式loop传给形参x,但x是call by value的,会直接对实参loop求值,loop递归不停,故不能返回值。
在Scala中,函数是第一等公民
Scala语言支持:
1.把函数作为实参传递给另外一个函数
2.把函数作为返回值
3.把函数赋值给变量
4.把函数存储在数据结构里
在Scala中,函数就像普通变量一样,同样也具有函数的类型。
函数类型
在Scala语言中,函数类型的格式为A => B ,表示一个接受 类型A的参数,并返回类型B 的函数。例如,Int => String是把整型映射为字符串的函数类型
在Scala中,用函数作为形参或返回值的函数,称为高阶函数。
def operate(f: (Int, Int) => Int) = {
f(4,4)
} //> operate: (f: (Int, Int) => Int)Int
def greeting() = (name:String) => {"hello" + " " + name}
//> greeting: ()String => String
像第二个函数的定义方式称为匿名函数。
匿名函数(Anonymous Function),就是函数常量,也称为函数文字量(Function Literal)。在Scala中,匿名函数的定义格式为:
(形参列表) => { 函数体 }
Eg.1
def function2(m: (Int) => Int) = m //> function2: (m: Int => Int)Int => Int
def funint2(m: Int): Int = m //> funint2: (m: Int)Int
function2(funint2)(2) //> res5: Int = 2
def function3(f: (Int, Int) => Int) = f //> function3: (f: (Int, Int) => Int)(Int, Int) => Int
def funint3(m: Int,n: Int): Int= m*n //> funint3: (m: Int, n: Int)Int
function3(funint3)(2,3) //> res6: Int = 6
Eg.2
def operate(f: (Int, Int) => Int) = {
f(4,4)
} //> operate: (f: (Int, Int) => Int)Int
def add(x:Int,y:Int) = {
x+y
} //> add: (x: Int, y: Int)Int
def greeting() = (name:String) => {"hello" + " " + name}
//> greeting: ()String => String
(x: Int, y: Int) => {x+y} //> res11: (Int, Int) => Int = <function2>
add(1,2) //> res12: Int = 3
greeting()("LS") //> res13: String = hello LS
operate(add) //> res14: Int = 8
柯里化函数(Curried Function)把具有多个参数的函数转换为一条函数链,每个节点上是单一参数。
Eg.1 一下两个add函数定义是等价的
def add1(x:Int,y:Int) = x+y //> add1: (x: Int, y: Int)Int
//下面是Scala中柯里化的语法
def add2(x:Int)(y:Int) = x + y //> add2: (x: Int)(y: Int)Int
Eg.2
def curriedAdd(a:Int)(b:Int) = a + b //> curriedAdd: (a: Int)(b: Int)Int
curriedAdd(2)(2) //> res15: Int = 4
//"_"通配了第二个形参b,第一个形参给定为1(偏应用函数),估只有一个参数是灵活可变的
val addOne = curriedAdd(1) _ //> addOne : Int => Int = <function1>
addOne(2) //> res16: Int = 3
Eg.1
def factorial(n: Int): Int = {
if(n <= 0) 1
else n*factorial(n-1)
} //> factorial: (n: Int)Int
factorial(4) //> res17: Int = 24
为了防止堆栈溢出,采取了尾递归的优化方案
尾递归函数
尾递归函数(Tail Recursive Function)中所有递归形式的调用都出现在函数的末尾。当编译器检测到一个函数调用是尾递归的时候,他就覆盖当前的活动记录而不是在栈中去创建一个新的。
Eg.2
@scala.annotation.tailrec
def factorial(n: Int,m: Int): Int={
if (n<=0) m
else factorial(n-1, m*n)
} //> factorial: (n: Int, m: Int)Int
factorial(5, 1) //> res0: Int = 120
Scala对形式上严格的尾递归进行了优化,对于严格的尾递归,可以放心使用,不必担心性能问题。对于是否是严格尾递归,若不能自行判断, 可使用Scala提供的尾递归标注@scala.annotation.tailrec,这个符号除了可以标识尾递归外,更重要的是编译器会检查该函数是否真的尾递归,若不是,会导致编译错误。
由于JVM的限制,对尾递归深层次的优化比较困难,因此,Scala对尾递归的优化很有限,它只能优化形式上非常严格的尾递归。也就是说,下列情况不在优化之列。
//call function value will not be optimized
val func = factorialTailrec _
def factorialTailrec(n: BigInt, acc: BigInt): BigInt = {
if(n <= 1) acc
else func(n-1, acc*n)
}
//indirect recursion will not be optimized
def foo(n: Int) : Int = {
if(n == 0) 0;
bar(n)
}
def bar(n: Int) : Int = {
foo(n-1)
}
Eg.1 求解
def sum(f: Int => Int)(a: Int)(b: Int): Int = {
@annotation.tailrec
def loop(n: Int, acc: Int): Int = {
if(n>b) {
println(s"n=${n},acc=${acc}")
acc
}else{
println(s"n=${n},acc=${acc}")
loop(n + 1,acc+f(n))
}
}
loop(a, 0)
} //> sum: (f: Int => Int)(a: Int)(b: Int)Int
sum(x => x)(1)(5) //> n=1,acc=0
//| n=2,acc=1
//| n=3,acc=3
//| n=4,acc=6
//| n=5,acc=10
//| n=6,acc=15
//| res0: Int = 15
//下面函数变量等价于调用sum(x => x*x)(1)(5)
val sumsquar = sum(x => x * x) _ //> sumsquar : Int => (Int => Int) = <function1>
sumsquar(1)(5) //> n=1,acc=0
//| n=2,acc=1
//| n=3,acc=5
//| n=4,acc=14
//| n=5,acc=30
//| n=6,acc=55
//| res1: Int = 55
val a = List(1,2,3,4) //> a : List[Int] = List(1, 2, 3, 4)
val b = 0 :: a //> b : List[Int] = List(0, 1, 2, 3, 4)
val c = "x"::"y"::"z"::Nil //> c : List[String] = List(x, y, z)
//连接操作符:::,用于连接两个List,Any型是String(引用类型)和Int(值类型)的父类型
val d = a:::c //> d : List[Any] = List(1, 2, 3, 4, x, y, z)
a.head //> res0: Int = 1
d.head //> res1: Any = 1
c.head //> res2: String = x
a.tail //> res3: List[Int] = List(2, 3, 4)
c.tail //> res4: List[String] = List(y, z)
a.isEmpty //> res5: Boolean = false
Nil.isEmpty //> res6: Boolean = true
def walkthru(l:List[Int]): String = {
if(l.isEmpty) ""
else l.head.toString()+" "+walkthru(l.tail)
} //> walkthru: (l: List[Int])String
walkthru(a) //> res7: String = "1 2 3 4 "
//与2取余数,若为奇数,则true,保留元素,否则去除
a.filter(x => x%2 ==1) //> res8: List[Int] = List(1, 3)
//字符串转为List
"boom".toList //> res9: List[Char] = List(b, o, o, m)
//过滤只留数字
"76 And Lakers".toList.filter(x => Character.isDigit(x)) //> res10: List[Char] = List(7, 6)
//其参数为boolean型的函数表达式,true继续take,false则停止
"76 And Lakers".toList.takeWhile {x => x!='L' } //> res11: List[Char] = List(7, 6, , A, n, d, )
c //> res12: List[String] = List(x, y, z)
c.map(x => x.toUpperCase()) //> res13: List[String] = List(X, Y, Z)
//"_"通配也是一样的道理,是匿名函数的简写方法
c.map(_.toUpperCase()) //> res14: List[String] = List(X, Y, Z)
a.filter(_%2 == 1) //> res15: List[Int] = List(1, 3)
//每个元素加10
a.filter(_%2 == 1).map(_+10) //> res16: List[Int] = List(11, 13)
//q是一个List型的List
val q = List(a,List(4,5,6)) //> q : List[List[Int]] = List(List(1, 2, 3, 4), List(4, 5, 6))
//下面两个过滤是等价的,第二条语句对每一个List也用了通配
q.map(x => x.filter(_%2 == 0)) //> res17: List[List[Int]] = List(List(2, 4), List(4, 6))
q.map(_.filter(_%2 == 0)) //> res18: List[List[Int]] = List(List(2, 4), List(4, 6))
//将二维的List转成一维
q.flatMap (_.filter(_%2 == 0)) //> res19: List[Int] = List(2, 4, 4, 6)
reduceLeft
reduceLeft( op : (T , T) => T ) ,op是我们自定义的操作
Eg.1 以下实现了两两op(这里是+)得到新结果,再与后面元素op,直至结束
a //> res20: List[Int] = List(1, 2, 3, 4)
a.reduceLeft( (x,y) => x+y ) //> res21: Int = 10
a.reduce( _ + _ ) //> res22: Int = 10
foldLeft( z : U ) (op: ( U , T ) => U ) ,z是进行规约的初始值,是U类型的,op是操作,foldLeft比reduceLeft更加通用,原因是,reduceLeft最终归约出来的值的类型是T类型,即我们输入集合的元素的类型。但foldLeft规约得到的类型跟初始值z是相同类型,即U。
Eg.2
a.foldLeft(0)(_ + _) //> res23: Int = 10
a.foldLeft(1)(_ * _) //> res24: Int = 24
Eg.1 定义一个Range
//to是一个闭区间,默认步长为1
1 to 5 //> res25: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5)
//用by定义步长
1 to 10 by 2 //> res26: scala.collection.immutable.Range = Range(1, 3, 5, 7, 9)
//将序列转化为List
(1 to 5).toList //> res27: List[Int] = List(1, 2, 3, 4, 5)
//until也是定义序列的一种方式,需要注意的是until是一个前闭后开区间
1 until 5 //> res28: scala.collection.immutable.Range = Range(1, 2, 3, 4)
Eg.2 定义一个Stream
//惰性求值列表体现在,只有第一个元素是有值的,其余元素都是在用的时候才会求值
1 #:: 2 #:: 3 #:: Stream.empty //> res29: scala.collection.immutable.Stream[Int] = Stream(1, ?)
val stream=(1 to 1000).toStream //> stream : scala.collection.immutable.Stream[Int] = Stream(1, ?)
使用Stream惰性求值对程序的运行性能有帮助,即用到的时候求值,按需求值,可以避免内存溢出的异常。
Eg.3 访问Stream中的元素,与List访问元素方法类似
//访问Stream中的第一个元素
stream.head //> res30: Int = 1
//访问尾Stream(除去第一个元素外的剩余元素组成的stream),这里依然是惰性求值的特点
stream.tail //> res31: scala.collection.immutable.Stream[Int] = Stream(2, ?)
(1,2) //> res32: (Int, Int) = (1,2)
1 -> 2 //> res33: (Int, Int) = (1,2)
val t = (1,"Alice","Math",95.5) //> t : (Int, String, String, Double) = (1,Alice,Math,95.5)
//访问某个分量
t._1 //> res34: Int = 1
t._2 //> res35: String = Alice
t._3 //> res36: String = Math
t._4 //> res37: Double = 95.5
如果想让一个函数返回多个值,则可以把多个值封装在一个tuple中(有点类似于Java中的类)
Eg.2
def sumSq(in: List[Int]):(Int,Int,Int) = {
//第一个分量表示目标List长度,第二个分量是求和,第三个分量是求平方和
in.foldLeft((0,0,0)) ((t,v) => (t._1+1,t._2+v,t._3+v*v))
} //> sumSq: (in: List[Int])(Int, Int, Int)
sumSq(a) //> res38: (Int, Int, Int) = (4,10,30)
//定义一个Map
val p = Map(1 -> "David" , 9 -> "Alex")
//> p : scala.collection.immutable.Map[Int,String] = Map(1 -> David, 9 -> Alex)
//通过key取value
p(1) //> res39: String = David
p(9) //> res40: String = Alex
//是否包含(取key判定)
p.contains(1) //> res41: Boolean = true
p.contains(2) //> res42: Boolean = false
//列出map中的所有key
p.keys //> res43: Iterable[Int] = Set(1, 9)
//列出map中的所有value
p.values //> res44: Iterable[String] = MapLike(David, Alex)
//添加一个[ k , v ]
p + (8 -> "Ls")
//> res45: scala.collection.immutable.Map[Int,String] = Map(1 -> David, 9 -> Alex, 8 -> Ls)
//删除key为1的[ k , v ] pair
p - 1
//> res46: scala.collection.immutable.Map[Int,String] = Map(9 -> Alex)
//添加多个pair,用List存储
p ++ List(2 -> "Alice",5 -> "Bob")
//> res47: Map[Int,String] = Map(1 -> David, 9 -> Alex, 2 -> Alice, 5 -> Bob)
//删除多个pair,用List存储
p -- List(1,9,2)
//> res48: scala.collection.immutable.Map[Int,String] = Map()
p ++ List(2 -> "Alice",5 -> "Bob") -- List(1,9)
//> res50: scala.collection.immutable.Map[Int,String] = Map(2 -> Alice, 5 -> Bob)
Eg.1
def qSort(a : List[Int]) : List[Int] = {
if(a.length < 2 ) a
else{
//下面List拼接用 “++” ,同“:::”效果相同
qSort(a.filter( _ < a.head )) ++ a.filter( _ == a.head) ++ qSort( a.filter( _ > a.head ) )
}
} //> qSort: (a: List[Int])List[Int]
qSort(List(8,6,3,7,2,6,8,5,3,1,9)) //> res51: List[Int] = List(1, 2, 3, 3, 5, 6, 6, 7, 8, 8, 9)