SCALA TUTORIAL (一 ... 四)

Posted 一起学Scala

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SCALA TUTORIAL (一 ... 四)相关的知识,希望对你有一定的参考价值。

参考自 https://www.scala-exercises.org/scala_tutorial/terms_and_types

下面的章节提供一个scala的快速教程

原文声明:主体内容基于 http://moocs.com 中的 Functional Programming Principles in Scala 以及 Functional Program Design in Scala


学习者需要有一些编程基础,并且熟悉 JVM



一、类型和语言基础

语言元素

    编程语言提供给程序员来进行计算的表达

    大多数编程语言都提供的要素

  • 最简单的基础表达式

  • 表达式合并的方式

  • 表达式的抽象方式,给表达式引入一个名称,让它可以被引用到


基础表达式

    基础表达式例子

  •     数字 “1”

    1

  • boolean 值

    true

  • 文本 "Hello,Scala!"

    "Hello,Scala!"

  • 字符

    'A'



    

    复合表达式

    使用 操作符 (operators) 来进行表达式的连接进行表达式的计算

  • 1 + 2

    1 + 2

  • 字符串连接

    "Hello," ++ "Scala!"

    

    求值

    非基础表达式

        1.运算括号中的的表达式

        2.上一个表达式的值继续运算

        3.直到没有操作符

    例子

        算术表达式

        (1 + 2) * 3

        3 * 3

        9


    方法调用

    通过调用方法来让表达式更简单

    "Hello,Scala!"的字符数量

    "Hello,Scala!".size

    使用 ( . ) dot 标记调用方法

    方法被应用的对象称之为目标对象

    

    返回 1 到 10 间的所有整数

    1.to(10)

    含参数的方法,参数放入 () 之中

    abs 进行绝对值计算

    toUpperCase方法返回目标字符串的大写形式

    -42.abs

    "Hello,Scala!".toUpperCase


    操作符也是方法

    操作符只是以符号作为方法名称

    3 + 2 == 3.+(2)

    通过中置语法(infix syntax)允许你可以省略 . 和 ()

    中置语法也可以改为常规调用

    1.to(10) == 1 to 10

    任何只有一个参数的方法都可以使用中置语法

    

    值和类型

    任何表达式都产生一个值和对应的类型,计算模型定义了如何从表达式中获取一个值,还有表达式的类型的值。

    0 和 1 都是数字,都是 Int 类型

    "foo" 和 "bar" 都是文本,都是 String 类型

   

    静态类型

    scala 编译器静态的检查类型,防止类型表达式类型不兼容

    

   

    通用类型

  • Int : 32-bit 整型 1,23,456

  • Double : 63-bit 浮点数 1.0,2.3,4.56

  • Boolean : 布尔值 true 和 false

  • String : 文本类型 "foo","bar"

注意:类型的名称都以大写字母开头


    

二、定义和求值

命名

思考一下计算求半径为10的圆面积的程序

3.14159 * 10 * 10

通过给中间表达式一个名称让复杂表达式更可读

val radius = 10

val pi = 3.14159


pi * radius * radius

除了使最后一个表达式更容易读懂之外,它还允许我们不重复半径的实际值。

求值

表达式求值的步骤

pi * radius * radius

3.14159 * radius * radius

3.14159 * 10 * radius

3.14159 * 10 * 10

方法

定义一个计算特定半径的圆的面积

def square(x:Double) = x * x

def area(radius:Double):Double = 3.14159 * square(radius)

area(10) == 314.159


多个参数的方法

使用逗号 ","来分隔多个参数

def sumOfSquares(x:Double, y:Double) = square(x) + square(y)


参数和返回类型

函数的参数类型指定在分号 ":" 之后

def power(x:Double, y:Int):Double = ...

如果指定返回类型,在参数列表后面


val 和 def

def:在每次求值过程都会使用def右边进行计算

val:定义了一个引用,指向值本身


val x = 2

val y = square(x)

y指向4,而不是 square(2)


函数的求值过程

函数参数的计算过程同操作数计算过程相似

    1.计算所有的参数,从左到右

    2.同时通过函数右边的表达式替换函数

    3.使用实际的参数来替换函数正式的参数

    sumOfSquare(3,2 + 2)

    sumOfSquare(3,4)

    square(3) + square(3)

    3 * 3 + square(3)

    3 * 3 + 4 * 4

    9 + 4 * 4

    9 + 16

    25


替换模型

表达式求值也称之为替换模型

这种模型就是将所有求值表达式化简计算为一个值

可以应用到所有的没有副作用(side effects)的表达式上

替换模型正式的使用在lambda推算中,这是函数式编程的基础


终止

在有限的步骤中,每个表达式都可以化简为一个值?

其实不然,下面一个计数列子

    def loop:Int = loop

    loop

这种定义在scala2.12.5版本已经无法执行了


值的定义和终止

val 和 def 在右侧不终止时候就变得很明显

def loop:Int = loop

def x = loop


val x = loop

会导致无限循环


修改求值策略

解释器在解释过程中会将参数中的表达式转化为值。

也可以将函数应用到无简化的参数

sumOfSquares(3, 2 + 2)

square(3) + square(2 + 2)

3 * 3 + square(2 + 2)

9 + square(2 + 2)

9 + (2 + 2) * (2 + 2)

9 + 4 * (2 + 2)

9 + 4 * 4

25


CALL-BY-NAME 和 CALL-BY-VALUE

两种方式都可以得到最终的结果,需要两个条件

  • 所有进行化简的表达式都由纯函数构成(pure functions)

  • 表达式求值可以结束


CALL-BY-VALUE 优点:函数每个参数都进行一次求值

CALL-BY-NAME 优点:函数体内没有对参数进行求值操作,则参数不会被进行求值

Scala常用的是 CALL-BY-VALUE




三、函数式循环

条件表达式

两个选项中选择,Scala提供条件表达式 if - else

看起来和java的if-else类似,但是他不是语句,而是表达式

举个栗子:

    def abs(x: Double) = if ( x >= 0 ) x else -x

    

x >= 0 是一个Boolean类型的



BOOLEAN表达式

Boolean 表达式简写为 b

true fals // 常量

!b            //取反

b && b    //与

b || b      //或

常用的比较操作

e <= e, e >= e, e < e , e > e, e == e , e != e


Boolean 的重写规则

下面是Boolean表达式的缩写规则

!true --> false

!false --> true

true && e --> e

false && e --> false

true || e --> true

false || e --> e


&& 和 || 可以进行短路计算


计算值的平方根

定义一个求平方根的方法


    

/** Calculates the square root of parameter x */

def sqrt(x: Double): Double = ...

通常通过牛顿迭代法

假设a。欲求a的平方根,首先估计一个值X1=a/2,然后根据迭代公式X(n+1)=(Xn+a/Xn)/2,算出X2,再将X2代公式的右边算出X3等等,直到计算出的(Xn * Xn - a)的绝对值小于某个值,即认为找到了精确的平方根。例算步骤如下。


方法

计算平方根 sqrt(7)

  • 估值从 x1 = 7 / 2开始

  • x(n + 1) = (xn + a/xn)/2

  • 判断精度绝对值是否达到范围(小于 0.000001)


x1 = 3.5

x2 = (x1 + a / x1) / 2

     = (3.5 + 7/3.5) / 2

     = (3.5 +  2) / 2

     = 5.5 / 2

     = 2.75

                                             x1 - x2 = 0.75

x3  = (x2 + a / x2) /2

      = (2.75 + 7/2.75) / 2

      = (2.75 + 2.54545455) / 2

      = 5.29545454 / 2

      = 2.6477272728

                                            x2 - x3 = 2.75 - 0.102272727

.

.

.

                                          当  x(n-1) - xn < 0.000000001

xn就是平方根


scala实现

首先定义一个迭代计算的方法


    def sqrtIter(guess:Double, x:Double) : Double = 

        if ( isGoodEnough( guess, x ) ) guess

        else sqrtIter( improve( guess, x) , x)


注意! sqrtIter 是一个递归调用,右侧调用他自己,递归方法必须明确指定返回类型。

对于没有递归调用的方法,返回类型是可选的。


接下来定义一个 improve 来进行计算下一个估值


    def improve(guess:Double,x:Double) = 

        (guess + x / guess) / 2

    def isGoodEnough(guess:Double, x:Double) = 

        abs(guess * guess - x) < 0.00001


最后,定义 sqrt 函数

    def sqrt(x :Double) = sqrtIter(x/2,x)


总结

上面已经展示了scala函数编程的最简单的元素

  • 算数,boolean 表达式

  • 条件表达式 if - else

  • 递归函数

Call - By - Name,Call - By - Value的求值策略

以及表达式替换模型的化简方式。

    

四、语法作用域

嵌套函数

    一个好的函数式编程的风格就是通过将一个任务切分到很多小的函数里面进行实现

    但是像第三章 sqrt 函数中的 sqrtIter,improve,isGoodEnough 在 sqrt函数外部都没有使用的意义,通常不希望用户直接访问这些函数。

    可以通过将这些方法放入到 sqrt函数里面来实现同样功能,并且避免名称空间污染

SQRT 第二版


def sqrt(x:Double):Double = {

  def sqrtIter(guess: Double, x: Double): Double = {

    if (isOk(guess, x)) guess

    else sqrtIter(improve(guess,x), x)

  }


  def improve(guess: Double, x: Double): Double = {

    (guess + x / guess) / 2

  }


  def isOk(guess: Double, x: Double) = {

    math.abs(guess * guess - x) < 0.001

  }


  sqrtIter(x/2,x)

}

所需的函数都定义在 sqrt 内部


Scala中的代码块

  • 代码块通过大括号来分割 { ... }

    {

        val x = f(3)

        x * x

    } 

  • 它包含一个定于或者表达式的序列

  • 代码块的最后一个表达式定义了代码块的值

  • return 表达式是一个可选的,可以指定,也可以省略,省略的情况返回最后代码块中一个表达式的值

  • 代码块  block 本身也是表达式,代码块的表达式可以出现在任何表达式可以出现的地方


代码块中的访问范围

  • 在代码块中的定义只能在代码块内部访问

  • 代码块内部变量与代码块外部定义的变量名称相同时会覆盖代码块外部定义的变量


val x = 0
def f(y: Int) = y + 1
val result = {
 val x = f(3)
 x * x
}

词法域

代码块内部可以访问外部变量,除非外部变量被代码块内部变量覆盖时代码块内部不能访问到外部变量

所以,我们可以通过消除多余的x参数来简化sqrt,因为x在sqrt中都是相同的

SQRT 第三版

def sqrt(x: Double):Double = {
  def sqrtIter(guess: Double): Double = {
    if(isOk(guess)) guess
    else sqrtIter(improve(guess))
  }
  def improve(guess:Double):Double = {
    (guess + x / guess) / 2
  }
  def isOk(guess:Double):Boolean = {
    math.abs(guess * guess - x) < 0.001
  }
  sqrtIter(x)
}


分号

scala中,行尾的分号通常时可选的

你可以这样写

    val x = 1;

但是大多数时候都是省略

另一方面,多个语句在同一行,必须通过分号进行分割

    val y = x + 1; y * y


分号和中置操作符

将一个长表达式分割到多行的例子

someLongExpression
+someOtherExpression

使用分号则可以理解成两个表达式

someLongExpression;
+someOtherExpression

两种方法解决这个问题

你可以将多行表达式写在一对括号中,它会通知scala编译器这个表达式没有结束

(someLongExpression
+someOtherExpression)

也可以将操作符写到第一行后面,这也会scala编译器该表达式没有结束

someLongExpression+
someOtherExpression

顶级定义

Scala 程序中,def , val 必须在一个顶级的object文件中定义

object MyExecutableProgram {
  val myVal = …
  def myMethod = …
}

上面的代码定义的对象名为MyExecutableProgram,可以通过 . 来引用它的成员

MyExecutableProgram.myMethod

MyExecutableProgram没有嵌套在另一个定义里面,所以是一个顶级定义


包和导入

可以通过包组织顶级定义,将一个class或者object放入包中,使用 package 来声明包

// file foo/Bar.scala

package foo

object Bar{ ... }


// file foo/Baz.scala

package foo

object Baz { ... }


同一个包中的定义可以被访问到

// file foo/Baz.scala

package foo

object Baz {

    // Bar 可以被访问因为都在 foo 包中

    Bar.someMethod

}


如果在别的包中的定义是不可以直接被访问的,必须使用全限定名来引用

// file quux/Quux.scala

package quux

object Quux {

  foo.Bar.someMethod

}


最后,你可以声明导入来避免重复的全限定名

//file quux/Quux.scala

package quux

import foo.Bar

object Quux {

  //通过导入可以直接引用 Bar

  Bar.someMethod

}


自动导入

在Scala程序中一成员是自动导入的:

  • 所有 scala 包中的成员

  • 所有 java.lang 包中的成员

  • 单例对象 scala.Predef 的所有成员

下面是部分类型的全限定名

Int                        scala.Int

Boolean               scala.Boolean

Object                  java.lang.Object

String                   java.lang.String


编写一个可执行程序

接下来可以在Scala中创建可执行程序

HelloWorld例子


object HelloWorld{
  def main(args: Array[String]):Unit = {
    println("HelloWorld")
  }
}


进行编译

$scalac HelloWorld.scala


会生成

HelloWorld.class

HelloWorld$.class


运行

scala HelloWorld


四、尾递归

递归函数的应用

思考比较两个递归函数求值的步骤

首先看 gcd 求两个数最大公约数的函数

下面是一个欧几里得算法实现的 gcd

def gcd(a:Int,b:Int):Int =
  if (b == 0) a else gcd(b, a%b)


gcd(14,21)的求值步骤

gcd(14,21)
if(21==0) 14 else gcd(21,14%21)
if(false) 14 else gcd(21,14%21)
gcd(21,14%21)
gcd(21,14)
if(14==0) 21 else gcd(14,21%14)
if(false) 21 else gcd(14,21%14)
gcd(14,7)
gcd(7,14%7)
gcd(7,0)
if(0==0) 7 else gcd(0,7%0)
if(true) 7 else gcd(0,7%0)
7


接下来看一下阶乘 factorial

def factorial(n: Int): Int = 

  if (n == 0) 1 else n * factorial(n-1)


factorial(4)的计算过程如下:

factorial(4)

if(4 == 0) 1 else 4 * factorial(4 - 1)

4 * factorial(3)

4 * (3 * factorial(2))

4 * (3 * (2 * factorial(1)))

4 * (3 * (2 * (1 * 1)))

24


两个过程中有什么不同的?

gcd 整个还原求值过程中是震荡的,从一个gcd调用之后到另一个gcd调用,最后调用到最终结构

factorial在整个过程中一直在形成一个更大的表达式,最后化简为值


尾递归

重写规则的差异实际上直接转化为计算机实际执行的差异。事实上,如果您有一个递归函数,它调用自己作为最后一个动作,那么您可以重用该函数的堆栈框架。这叫做尾部递归。

通过这个技巧,尾递归函数可以在一个固定空间大小的栈中运行,这是迭代的另一种形式,可以说尾递归是循环的另一种形式,并且执行效率和循环一样。

再看 gcd ,在 else 的部分,gcd 最后一个操作是调用自身,这就转换成一个重写序列它的大小基本上是恒定的,在计算机上的实际执行中,它会转换成一个尾部递归调用,可以在恒定的空间中执行。


factorial,可以看到后面调用 factorial(n - 1),也就是仍然有工作要做,n 仍然被使用,所以递归调用不是尾递归,过程中虽然序列在减少,但是其实一直在积累中间结果,所以阶乘不是尾递归函数。


SCALA 中的尾递归

可以使用 @tailrec 注解来要求函数是尾递归函数。


阶乘的尾递归版本

def factorial(n: Int): Int = {
  @tailrec
  def iter(x: Int, result: Int): Int =
    if (x ==1 ) result
    else iter(x -1 , result * x)

  iter(n, 1)
}






以上是关于SCALA TUTORIAL (一 ... 四)的主要内容,如果未能解决你的问题,请参考以下文章

Chisel3 - Tutorial - Adder4

scala言语基础学习四

STATES TUTORIAL(第四部分)

Tutorial 01_JAVA基本语法[实验任务四]

Redis Tutorial

寒假四:scala的安装以及使用