scala学习小记

以下内容均摘自:https://blog.csdn.net/qq_34291505

一、Scala基本数据类型

Scala是静态语言,在编译期间会检查每个对象的类型。对于类型不匹配的非法操作,在编译时就能被发现。对于动态语言而言,这种非法操作需要等到运行时才能被发现,此时可能造成严重错误。所以,静态语言相比诸如Python这样的动态语言在某些方面是有优势的。对于Chisel而言,我们就需要这种优势。因为Chisel需要编译成Verilog,我们不能产生非法的Verilog语句并且等到模块运行时才去发现它。

Scala标准库定义了一些基本类型,如下表所示。除了“String”类型是属于java.lang包之外,其余都在Scala的包里。

Byte 8-bit有符号整数,补码表示,范围是 -2^{7} 到 2^{7}-1
Short 16-bit有符号整数,补码表示,范围是 -2^{15} 到 2^{15}-1
Int 32-bit有符号整数,补码表示,范围是 -2^{31} 到 2^{31}-1
Long 64-bit有符号整数,补码表示,范围是 -2^{63} 到 2^{63}-1
Char 16-bit字符,Unicode编码,范围是 0 到 2^{16}-1
String 字符串
Float 32-bit单精度浮点数,符合IEEE 754标准
Double 64-bit双精度浮点数,符合IEEE 754标准
Boolean 布尔值,其值为true或者false

事实上,在定义变量时,应该指明变量的类型,只不过Scala的编译器具有自动推断类型的功能,可以根据赋给变量的对象的类型,来自动推断出变量的类型。如果要显式声明变量的类型,或者无法推断时,则只需在变量名后面加上一个冒号“ : ”,然后在等号与冒号之间写出类型名即可。

二、函数相关

1、函数字面量

函数式编程有两个主要思想,其中之一就是:函数是一等(first-class)的值。换句话说,一个函数的地位与一个Int值、一个String值等等,是一样的。既然一个Int值可以成为函数的参数、函数的返回值、定义在函数体里、存储在变量里,那么,作为地位相同的函数,也可以这样。你可以把一个函数当参数传递给另一个函数,也可以让一个函数返回一个函数,亦可以把函数赋给一个变量,又或者像定义一个值那样在函数里定义别的函数(即前述的嵌套函数)。就像写一个整数字面量“1”那样,Scala也可以定义函数的字面量。函数字面量是一种匿名函数的形式,它可以存储在变量里、成为函数参数或者当作函数返回值,其定义形式为:

(参数1: 参数1类型, 参数2: 参数2类型, …) => { 函数体 }

通常,函数字面量会赋给一个变量,这样就能通过“变量名(参数)”的形式来使用函数字面量。在参数类型可以被推断的情况下,可以省略类型,并且参数只有一个时,圆括号也可以省略。

函数字面量的形式可以更精简,即只保留函数体,并用下划线“_”作为占位符来代替参数。在参数类型不明确时,需要在下划线后面显式声明其类型。多个占位符代表多个参数,即第一个占位符是第一个参数,第二个占位符是第二个参数……因此不能重复使用某个参数。例如:

scala> val f = (_: Int) + (_: Int)
f: (Int, Int) => Int = $$Lambda$1072/1534177037@fb42c1c

scala> f(1, 2)
res0: Int = 3

无论是用“def”定义的函数,还是函数字面量,它们的函数体都可以把一个函数字面量作为一个返回结果,这样就成为了返回函数的函数;它们的参数变量的类型也可以是一个函数,这样调用时给的入参就可以是一个函数字面量。类型为函数的变量,其冒号后面的类型写法是“(参数1类型, 参数2类型,...) => 返回结果的类型”。例如:

scala> val add = (x: Int) => {
    
     (y: Int) => x + y }
add: Int => (Int => Int) = $$Lambda$1192/1767705308@55456711

scala> add(1)(10)
res0: Int = 11

scala> def aFunc(f: Int => Int) = f(1) + 1
aFunc: (f: Int => Int)Int

scala> aFunc(x => x + 1)
res1: Int = 3

在第一个例子中,变量add被赋予了一个返回函数的函数字面量。在调用时,第一个括号里的“1”是传递给参数x,第二个括号里的“10”是传递给参数y。如果没有第二个括号,得到的就不是11,而是“(y: Int) => 1 + y”这个函数字面量。

在第二个例子中,函数aFunc的参数f是一个函数,并且该函数要求是一个入参为Int类型、返回结果也是Int类型的函数。在调用时,给出了函数字面量“x => x + 1”。这里没有显式声明x的类型,因为可以通过f的类型来推断出x必须是一个Int类型。在执行时,首先求值f(1),结合参数“1”和函数字面量,可以算出结果是2。那么,“f(1) + 1”就等于3了。

2、柯里化

对大多数编程语言来说,函数只能有一个参数列表,但是列表里可以有若干个用逗号间隔的参数。Scala有一个独特的语法——柯里化,也就是一个函数可以有任意个参数列表。柯里化往往与另一个语法结合使用:当参数列表里只有一个参数时,在调用该函数时允许单个参数不用圆括号包起来,改用花括号也是可行的。这样,在自定义类库时,自定义方法就好像“if(...) {...}”、“while(...) {...}”、“for(...) {...}”等内建控制结构一样,让人看上去以为是内建控制,丝毫看不出是自定义语法。例如:

scala> def add(x: Int, y: Int, z: Int) = x + y + z
add: (x: Int, y: Int, z: Int)Int

scala> add(1, 2, 3)
res0: Int = 6

scala> def addCurry(x: Int)(y: Int)(z: Int) = x + y + z
addCurry: (x: Int)(y: Int)(z: Int)Int

scala> addCurry(1)(2) {
    
    3}
res1: Int = 6

3、传名参数

第四点介绍了函数字面量如何作为函数的参数进行传递,以及如何表示类型为函数时参数的类型。如果某个函数的入参类型是一个无参函数,那么通常的类型表示法是“() => 函数的返回类型”。在调用这个函数时,给出的参数就必须写成形如“() => 函数体”这样的函数字面量。

为了让代码看起来更舒服,也为了让自定义控制结构更像内建结构,Scala又提供了一个特殊语法——传名参数。也就是类型是一个无参函数的函数入参,传名参数的类型表示法是“=> 函数的返回类型”,即相对常规表示法去掉了前面的空括号。在调用该函数时,传递进去的函数字面量则可以只写“函数体”,去掉了“() =>”。例如:

var assertionEnabled = false
 
// predicate是类型为无参函数的函数入参
def myAssert(predicate: () => Boolean) =
  if(assertionEnabled && !predicate())
    throw new AssertionError
// 常规版本的调用
myAssert(() => 5 > 3)
 
// 传名参数的用法,注意因为去掉了空括号,所以调用predicate时不能有括号
def byNameAssert(predicate: => Boolean) =
  if(assertionEnabled && !predicate)
    throw new AssertionError
// 传名参数版本的调用,看上去更自然
byNameAssert(5 > 3)

可以看到,传名参数使得代码更加简洁、自然,而常规写法则很别扭。事实上,predicate的类型可以改成Boolean,而不必是一个返回布尔值的函数,这样调用函数时与传名参数是一致的。例如:

// 使用布尔型参数的版本
def boolAssert(predicate: Boolean) =
  if(assertionEnabled && !predicate)
    throw new AssertionError
// 布尔型参数版本的调用
boolAssert(5 > 3)

尽管byNameAssert和boolAssert在调用形式上是一样的,但是两者的运行机制却不完全一样。如果给函数的实参是一个表达式,比如“5 > 3”这样的表达式,那么boolAssert在运行之前会先对表达式求值,然后把求得的值传递给函数去运行。而myAssert和byNameAssert则不会一开始就对表达式求值,它们是直接运行函数,直到函数调用入参时才会对表达式求值,也就是例子中的代码运行到“!predicate”时才会求“5 > 3”的值。

4、偏函数

在Scala里,万物皆对象。函数是一等值,与整数、浮点数、字符串等等相同,所以函数也是一种对象。既然函数也是一个对象,那么必然属于某一种类型。为了标记函数的类型,Scala提供了一系列特质:Function0、Function1、Function2……Function22来表示参数为0、1、2……22个的函数。与元组很像,因此函数的参数最多只能有22个。当然也可以自定义含有更多参数的FunctionX,但是Scala标准库没有提供,也没有必要。

除此之外,还有一个特殊的函数特质:偏函数PartialFunction。偏函数的作用在于划分一个输入参数的可行域,在可行域内对入参执行一种操作,在可行域之外对入参执行其他操作。偏函数有两个抽象方法需要实现:apply和isDefinedAt。其中,isDefinedAt用于判断入参是否在可行域内,是的话就返回true,否则返回false;apply是偏函数的函数体,用于对入参执行操作。使用偏函数之前,应该先用isDefinedAt判断入参是否合法,否则可能会出现异常。

定义偏函数的一种简便方法就是使用case语句组。广义上讲,case语句就是一个偏函数,所以才可以用于模式匹配。一个case语句就是函数的一个入口,多个case语句就有多个入口,每个case语句又可以有自己的参数列表和函数体。例如:

val isInt1: PartialFunction[Any, String] = {
    
    
  case x: Int => x + " is a Int."
}
// 相当于
val isInt2 = new PartialFunction[Any, String] {
    
    
  def apply(x: Any) = x.asInstanceOf[Int] + " is a Int."
  def isDefinedAt(x: Any) = x.isInstanceOf[Int]
}

注意apply方法可以隐式调用。x.isInstanceOf[T]判断x是不是T类型(及其超类)的对象,是的话就返回true。x.asInstanceOf[T]则把x转换成T类型的对象,如果不能转换则会报错。

偏函数PartialFunction[Any, Any]是Function1[Any, Any]的子特质,因为case语句只有一个参数。[Any, Any]中的第一个Any是输入参数的类型,第二个Any是返回结果的类型。如果确实需要输入多个参数,则可以用元组、列表或数组等把多个参数变成一个集合。

在用case语句定义偏函数时,前述的各种模式类型、模式守卫都可以使用。最后的通配模式可有可无,但是没有时,要保证运行不会出错。

上述代码运行如下:

scala> isInt1(1)
res0: String = 1 is a Int.

scala> isInt2(1)
res1: String = 1 is a Int.

scala> isInt1.isDefinedAt('1')
res2: Boolean = false

scala> isInt2.isDefinedAt('1')
res3: Boolean = false

scala> isInt1('1')
scala.MatchError: 1 (of class java.lang.Character)
  at scala.PartialFunction$ $anon$1.apply(PartialFunction.scala:255)
  at scala.PartialFunction$ $anon$1.apply(PartialFunction.scala:253)
  at $anonfun$1.applyOrElse(<console>:12)
  at scala.runtime.AbstractPartialFunction.apply(AbstractPartialFunction.scala:34)
  ... 28 elided

scala> isInt2('1')
java.lang.ClassCastException: java.lang.Character cannot be cast to java.lang.Integer
  at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:101)
  at $anon$1.apply(<console>:13)
  at $anon$1.apply(<console>:12)
  ... 28 elided

三、类和对象相关

1、单例对象与伴生对象

在Scala里,除了用new可以构造一个对象,也可以用“object”开头定义一个对象。它类似于类的定义,只不过不能像类那样有参数,也没有构造方法。因此,不能用new来实例化一个object的定义,因为它已经是一个对象了。这种对象和用new实例化出来的对象没有什么区别,只不过new实例的对象是以类为蓝本构建的,并且数量没限制,而object定义的对象只能有这一个,故而得名“单例对象”。

如果某个单例对象和某个类同名,那么单例对象称为这个类的“伴生对象”,同样,类称为这个单例对象的“伴生类”。伴生类和伴生对象必须在同一个文件里,而且两者可以互访对方所有成员。在C++、Java等oop语言里,类内部可以定义静态变量。这些静态变量不属于任何一个用new实例化的对象,而是它们的公有部分。Scala追求纯粹的面向对象属性,即所有的事物都是类或对象,但是静态变量这种不属于类也不属于对象的事物显然违背了Scala的理念。所以,Scala的做法是把类内所有的静态变量从类里移除,转而集中定义在伴生对象里,让静态变量属于伴生对象这个独一无二的对象。

既然单例对象和new实例的对象一样,那么类内可以定义的代码,单例对象同样可以拥有。例如,单例对象里面可以定义字段和方法。Scala允许在类里定义别的类和单例对象,所以单例对象也可以包含别的类和单例对象的定义。因此,单例对象除了用作伴生对象,通常也可以用于打包某方面功能的函数系列成为一个工具集,或者包含主函数成为程序的入口。

“object”后面定义的单例对象名可以认为是这个单例对象的名称标签,因此可以通过句点符号访问单例对象的成员——“单例对象名.成员”,也可以赋给一个变量——“val 变量 = 单例对象名”,就像用new实例的对象那样。例如:

scala> class A {
    
     val a = 10 }
defined class A

scala> val x = new A
x: A = A@7e5831c4

scala> x.a
res0: Int = 10

scala> (new A).a
res1: Int = 10

scala> object B {
    
     val b = "a singleton object" }
defined object B

scala> B.b
res2: String = a singleton object

scala> val y = B
y: B.type = B$@4489b853

scala> y.b
res3: String = a singleton object

前面说过,定义一个类,就是定义了一种类型。从抽象层面讲,定义单例对象却并没有定义一种类型。实际上每个单例对象有自己独特的类型,即object.type。可以认为新类型出现了,只不过这个类型并不能用来归类某个对象集合,等同于没有定义新类型。即使是伴生对象也没有定义类型,而是由伴生类定义了同名的类型。后续章节将讲到,单例对象可以继承自超类或混入特质,这样它就能出现在需要超类对象的地方。例如下面的例子中,可以明确看到X.type和Y.type两种新类型出现,并且是不一样的:

scala> object X
defined object X

scala> object Y
defined object Y

scala> var x = X
x: X.type = X$@630bb67

scala> x = Y
<console>:17: error: type mismatch;
 found   : Y.type
 required: X.type
       x = Y
           ^

2、工厂对象与工厂方法

如果定义一个方法专门用来构造某一个类的对象,那么这种方法就称为“工厂方法”。包含这些工厂方法集合的单例对象,也就叫“工厂对象” 。通常,工厂方法会定义在伴生对象里。尤其是当一系列类存在继承关系时,可以在基类的伴生对象里定义一系列对应的工厂方法。使用工厂方法的好处是可以不用直接使用new来实例化对象,改用方法调用,而且方法名可以是任意的,这样对外隐藏了类的实现细节。例如:

// students.scala
class Students(val name: String, var score: Int) {
    
    
  def exam(s: Int) = score = s
  override def toString = name + "'s score is " + score + "."
}
 
object Students {
    
    
  def registerStu(name: String, score: Int) = new Students(name, score)
}

将文件students.scala编译后,并在解释器里用“import Students._”导入单例对象后,就能这样使用:

scala> import Students._
import Students._

scala> val stu = registerStu("Tim", 100)
stu: Students = Tim's score is 100.

3、apply方法

有一个特殊的方法名——apply,如果定义了这个方法,那么既可以显式调用——“对象.apply(参数)” ,也可以隐式调用——“对象(参数)”。隐式调用时,编译器会自动插入缺失的“.apply”。如果apply是无参方法,应该写出空括号,否则无法隐式调用。无论是类还是单例对象,都能定义这样的apply方法。

通常,在伴生对象里定义名为apply的工厂方法,就能通过“伴生对象名(参数)”来构造一个对象。也常常在类里定义一个与类相关的、具有特定行为的apply方法,让使用者可以隐式调用,进而隐藏相应的实现细节。例如:

// students2.scala
class Students2(val name: String, var score: Int) {
    
    
  def apply(s: Int) = score = s
  def display() = println("Current score is " + score + ".")
  override def toString = name + "'s score is " + score + "."
}
 
object Students2 {
    
    
  def apply(name: String, score: Int) = new Students2(name, score)
}

将文件students2.scala编译后,就能在解释器里这样使用:

scala> val stu2 = Students2("Jack", 60)
stu2: Students2 = Jack's score is 60.

scala> stu2(80)

scala> stu2.display
Current score is 80.

其中,“Students2(“Jack”, 60)”被翻译成“Students2.apply(“Jack”, 60)” ,也就是调用了伴生对象里的工厂方法,所以构造了一个Students2的对象并赋给变量stu2。“stu2(80)”被翻译成“stu2.apply(80)” ,也就是更新了字段score的数据。

4、主函数

主函数是Scala程序唯一的入口,即程序是从主函数开始运行的。要提供这样的入口,则必须在某个单例对象里定义一个名为“main”的函数,而且该函数只有一个参数,类型为字符串数组Array[String],函数的返回类型是Unit。任何符合条件的单例对象都能成为程序的入口。例如:

// students2.scala
class Students2(val name: String, var score: Int) {
    
    
  def apply(s: Int) = score = s
  def display() = println("Current score is " + score + ".")
  override def toString = name + "'s score is " + score + "."
}
 
object Students2 {
    
    
  def apply(name: String, score: Int) = new Students2(name, score)
}
 
// main.scala
object Start {
    
    
  def main(args: Array[String]) = {
    
    
    try {
    
    
      val score = args(1).toInt
      val s = Students2(args(0), score)
      println(s.toString)
    } catch {
    
    
      case ex: ArrayIndexOutOfBoundsException => println("Arguments are deficient!")
      case ex: NumberFormatException => println("Second argument must be a Int!")
    }
  }
}

使用命令“scalac students2.scala main.scala”将两个文件编译后,就能用命令“scala Start 参数1 参数2”来运行程序。命令里的“Start”就是包含主函数的单例对象的名字,后面可以输入若干个用空格间隔的参数。这些参数被打包成字符串数组供主函数使用,也就是代码里的args(0)、args(1)。例如:

PS E:\Microsoft VS\Scala> scala Start Tom
Arguments are deficient!
PS E:\Microsoft VS\Scala> scala Start Tom aaa
Second argument must be a Int!
PS E:\Microsoft VS\Scala> scala Start Tom 100
Tom's score is 100.

主函数的一种简化写法是让单例对象混入“App”特质(特质在后续章节讲解),这样就只要在单例对象里编写主函数的函数体。例如:

// main2.scala
object Start2 extends App {
    
    
  try {
    
    
    var sum = 0
    for(arg <- args) {
    
    
      sum += arg.toInt
    }
    println("sum = " + sum)
  } catch {
    
    
    case ex: NumberFormatException => println("Arguments must be Int!")
  }
}

将文件编译后,就可以如下使用:

PS E:\Microsoft VS\Scala> scala Start2 10 -8 20 AAA
Arguments must be Int!
PS E:\Microsoft VS\Scala> scala Start2 10 -8 20 8
sum = 30

四、for表达式与for循环

要实现循环,在Scala里推荐使用for表达式。不过,Scala的for表达式是函数式风格的,没有引入指令式风格的“for(i = 0; i < N; i++)”。一个Scala的for表达式的一般形式如下:

for( seq ) yield expression

整个for表达式算一个语句。在这里,seq代表一个序列。换句话说,能放进for表达式里的对象,必须是一个可迭代的集合。比如常用的列表(List)、数组(Array)、映射(Map)、区间(Range)、迭代器(Iterator)、流(Stream)和所有的集(Set),它们都混入了特质Iterable。可迭代的集合对象能生成一个迭代器,用该迭代器可以逐个交出集合中的所有元素,进而构成了for表达式所需的序列。关键字“yield”是“产生”的意思,也就是把前面序列里符合条件的元素拿出来,逐个应用到后面的“expression”,得到的所有结果按顺序产生一个新的集合对象。

如果把seq展开来,其形式如下:

for {
    
    
  p <- persons          // 一个生成器
  n = p.name            // 一个定义
  if(n startsWith "To")  // 一个过滤器
} yield n

seq是由“生成器”、“定义”和“过滤器”三条语句组成,以分号隔开,或者放在花括号里让编译器自动推断分号。生成器“p <- persons”的右侧就是一个可迭代的集合对象,把它的每个元素逐一拿出来与左侧的模式进行匹配(有关模式匹配请见后续章节)。如果匹配成功,那么模式里的变量就会绑定上该元素对应的部分;如果匹配失败,并不会抛出匹配错误,而是简单地丢弃该元素。

在这个例子里,左侧的p是一个无需定义的变量名,它构成了变量模式,也就是简单地指向persons的每个元素。大多数情况下的for表达式的生成器都是这么简单。定义就是一个赋值语句,这里的n也是一个无需定义的变量名。定义并不常用,比如这里的定义就可有可无。过滤器则是一个if语句,只有if后面的表达式为true时,生成器的元素才会继续向后传递,否则就丢弃该元素。这个例子中,是判断persons的元素的name字段是否以“To”为开头。最后,name以“To”为开头的persons元素会应用到yield后面的表达式,在这里仅仅是保持不变,没有任何操作。总之,这个表达式的结果就是遍历集合persons的元素,按顺序找出所有name以“To”为开头的元素,然后把这些元素组成一个新的集合。例如:

// test.scala
class Person(val name: String)
 
object Alice extends Person("Alice")
object Tom extends Person("Tom")
object Tony extends Person("Tony")
object Bob extends Person("Bob")
object Todd extends Person("Todd")
 
val persons = List(Alice, Tom, Tony, Bob, Todd)
 
val To = for {
    
    
  p <- persons          
  n = p.name            
  if(n startsWith "To") 
} yield n
 
println(To)
PS E:\Microsoft VS\Scala> scala test.scala
List(Tom, Tony, Todd)

每个for表达式都以生成器开始。如果一个for表达式中有多个生成器,那么出现在后面的生成器比出现在前面的生成器变得更频繁,也就是指令式编程里的嵌套的for循环。例如计算乘法口诀表:

 scala> for {
    
    
          |    i <- 1 to 9
          |    j <- i to 9
          |  } yield i * j

res0: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3, 4, 5, 6, 
7, 8, 9, 4, 6, 8, 10, 12, 14, 16, 18, 9, 12, 15, 18, 21, 24, 27, 16, 20, 24, 
28, 32, 36, 25, 30, 35, 40, 45, 36, 42, 48, 54, 49, 56, 63, 64, 72, 81)

如果只想把每个元素应用到一个Unit类型的表达式,那么就是一个“for循环”,而不再是一个“for表达式”。关键字“yield”也可以省略。例如:

scala> var sum = 0
sum: Int = 0

scala> for(x <- 1 to 100) sum += x

scala> sum
res0: Int = 5050

五、match表达式

match表达式的作用相当于“switch”,也就是把作用对象与定义的模式逐个比较,按匹配的模式执行相应的操作。例如:

scala> def something(x: String) = x match {
    
    
         |     case "Apple" => println("Fruit!")
         |     case "Tomato" => println("Vegetable!")
         |     case "Cola" => println("Beverage!")
         |     case _ => println("Huh?")
         |  }
something: (x: String)Unit

scala> something("Cola")
Beverage!

scala> something("Toy")
Huh?

六、隐式参数

函数最后一个参数列表可以用关键字“implicit”声明为隐式的,这样整个参数列表的参数都是隐式参数。注意,是整个参数列表,即使括号里有多个参数,也只需要开头写一个“implicit”。而且每个参数都是隐式的,不存在部分隐式部分显式。

当调用函数时,若缺省了隐式参数列表,则编译器会尝试插入相应的隐式定义。当然,也可以显式给出参数,但是要么全部缺省,要么全部显式给出,不能只写一部分。

要让编译器隐式插入参数,就必须事先定义好符合预期类型的隐式变量(val和var可以混用,关键在于类型)、隐式单例对象或隐式函数(别忘了函数也能作为函数的参数进行传递),这些隐式定义也必须用“implicit”修饰。隐式变量、单例对象、函数在当前作用域的引用也必须满足“单标识符”原则,即不同层次之间需要用“import”来解决。

隐式参数的类型应该是“稀有”或“特定”的,类型名称最好能表明该参数的作用。如果直接使用Int、Boolean、String等常用类型,容易引发混乱。例如:

// test.scala
class PreferredPrompt(val preference: String)
class PreferredDrink(val preference: String)
 
object Greeter {
    
    
  def greet(name: String)(implicit prompt: PreferredPrompt,
      drink: PreferredDrink) = {
    
    
    println("Welcome, " + name + ". The system is ready.")
    print("But while you work, ")
    println("why not enjoy a cup of " + drink.preference + "?")
    println(prompt.preference)
  }
}
 
object JoesPrefs {
    
    
  implicit val prompt = new PreferredPrompt("Yes, master> ")
  implicit val drink = new PreferredDrink("tea")
}
scala> Greeter.greet("Joe")
<console>:12: error: could not find implicit value for parameter prompt: PreferredPrompt
       Greeter.greet("Joe")
                    ^

scala> import JoesPrefs._
import JoesPrefs._

scala> Greeter.greet("Joe")
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
Yes, master>

scala> Greeter.greet("Joe")(prompt, drink)
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
Yes, master>

scala> Greeter.greet("Joe")(prompt)
<console>:15: error: not enough arguments for method greet: (implicit 
prompt: PreferredPrompt, implicit drink: PreferredDrink)Unit.
Unspecified value parameter drink.
       Greeter.greet("Joe")(prompt)

猜你喜欢

转载自blog.csdn.net/qq_39507748/article/details/113841301