3

Scala基础


3.1 数据结构39
3.2 循环,条件,Comprehensions45
3.3 方法与函数50
3.4 类和Trait53

for (i <- Range.inclusive(1, 100)) {
  println(
    if (i % 3 == 0 && i % 5 == 0) "FizzBuzz"
    else if (i % 3 == 0) "Fizz"
    else if (i % 5 == 0) "Buzz"
    else i
  )
}
</> 3.1.scala

Snippet 3.1: 用Scala实现的“FizzBuzz”编程挑战

本章快速浏览Scala语言。我们现在聚焦Scala基本知识,你可能发现这部分和其他主流编程语言是相似的。

本章的目标是让你熟悉Scala,能够像你已经熟悉的其他语言一样毫不费力地编写相同功能的代码。本章并不会涉及很多Scala编程风格和语言特点:这些会留待 Chapter 5: Scala特性 介绍。

本章我们将在Ammonite Scala REPL中编写代码:

$ amm
Loading...
Welcome to the Ammonite Repl 2.2.0 (Scala 2.13.2 Java 11.0.7)
@
</> 3.2.bash

3.1 数据结构

3.1.1 原始类型

Scala有下列原始类型:

Type Values
Byte -128 to 127
Short -32,768 to 32,767
Int -2,147,483,648 to 2,147,483,647
Long -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
Type Values
Boolean true, false
Char 'a', '0', 'Z', '包', ...
Float 32-bit Floating point
Double 64-bit Floating point

这些类型和Java原始类型一致,和其它静态类型编程语言C#、C++等也相似。每种类型支持典型的操作,比如booleans支持布尔逻辑 || &&,numbers支持算术运算 + - * / 和位运算 | &等等。所有数据结构支持 == 判断相等,!= 判断不等。

Numbers默认32位 Int。算术运算优先级同其它编程语言一样:*/ 优先于 +-,括号里的表达式优先计算。

@ 1 + 2 * 3
res0: Int = 7

@ (1 + 2) * 3
res1: Int = 9
</> 3.3.scala

Int是有符号的,可能溢出。带 L 后缀的是64位 Long,支持更大数值范围,不会轻易溢出:

@ 2147483647
res2: Int = 2147483647

@ 2147483647 + 1
res3: Int = -2147483648
</> 3.4.scala
@ 2147483647L
res4: Long = 2147483647L

@ 2147483647L + 1L
res5: Long = 2147483648L
</> 3.5.scala

java.lang.Integerjava.lang.Long 除了支持基本的操作符,还有很多有用的方法:

@ java.lang.Integer.<tab>
BYTES                    decode                   numberOfTrailingZeros
signum                   MAX_VALUE                divideUnsigned
getInteger               parseUnsignedInt         toBinaryString
...

@ java.lang.Integer.toBinaryString(123)
res6: String = "1111011"

@ java.lang.Integer.numberOfTrailingZeros(24)
res7: Int = 3
</> 3.6.scala

1.0 是64位 Double,支持类似的算术运算。你可以用 1.0F 声明这是一个32位 Float 数字:

@ 1.0 / 3.0
res8: Double = 0.3333333333333333

@ 1.0F / 3.0F
res9: Float = 0.33333334F
</> 3.7.scala

32位 Float 类型只占64位 Double 类型一半存储,但是在算数运算中更容易发生四舍五入错误。在 FloatDouble 上可以执行类似像 java.lang.Floatjava.lang.Double 所支持的操作。

3.1.2 字符串

String 在Scala中是16位 Char 数组:

@ "hello world"
res10: String = "hello world"
</> 3.8.scala

.substring 用于对 String 进行切片,+ 用于拼接 String。前缀 s"..." 可做字符串插值,在其中用 $ 或者 ${...} 来引用变量或表达式完成插值:

@ "hello world".substring(0, 5)
res11: String = "hello"

@ "hello world".substring(5, 10)
res12: String = " worl"
</> 3.9.scala
@ "hello" + 1 + " " + "world" + 2
res13: String = "hello1 world2"

@ val x = 1; val y = 2

@ s"Hello $x World $y"
res15: String = "Hello 1 World 2"

@ s"Hello ${x + y} World ${x - y}"
res16: String = "Hello 3 World -1"
</> 3.10.scala

3.1.3 局部变量

你可以用 val 关键字命名局部变量:

@ val x = 1

@ x + 2
res18: Int = 3
</> 3.11.scala

val 变量的地址不可变:你不可以重新分配 val x 来引用其它值。你只能使用 var 关键字重新分配局部变量。

@ x = 3
cmd41.sc:1: reassignment to val
val res26 = x = 3
              ^
Compilation Failed
</> 3.12.scala
@ var y = 1

@ y + 2
res20: Int = 3

@ y = 3

@ y + 2
res22: Int = 5
</> 3.13.scala

通常来讲,你应该尽可能使用 val:你的程序中大多数有名变量不需要重新分配,使用 val 预防你意外地重分配它们。当你确信会重分配它们时,才使用 var

valvar 都可以直接注解类型,这有利于阅读你的代码,而且当你意外地分配了不匹配的类型时,编译器能够捕获该错误。

@ val x: Int = 1

@ var s: String = "Hello"
s: String = "Hello"

@ s = "World"
</> 3.14.scala
@ val z: Int = "Hello"
cmd33.sc:1: type mismatch;
 found   : String("Hello")
 required: Int
val z: Int = "Hello"
             ^
Compilation Failed
</> 3.15.scala

3.1.4 元组

元组是固定长度的变量集合,这些变量可以是不同类型:

@ val t = (1, true, "hello")
t: (Int, Boolean, String) = (1, true, "hello")

@ t._1
res27: Int = 1

@ t._2
res28: Boolean = true

@ t._3
res29: String = "hello"
</> 3.16.scala

上面,我们用语法 (a, b, c) 将一个元组存储到局部变量 t,用 ._1 ._2 ._3 抽取其中的字段值。元组中的字段是不可变的。

局部变量 t 的类型可以注解为一个元组类型:

@ val t: (Int, Boolean, String) = (1, true, "hello")

你同样可以使用 val (a, b, c) = t 一次性抽取所有字段,并赋予名称:

@ val (a, b, c) = t
a: Int = 1
b: Boolean = true
c: String = "hello"
</> 3.17.scala
@ a
res31: Int = 1

@ b
res32: Boolean = true

@ c
res33: String = "hello"
</> 3.18.scala

元组长度从1支持到22:

@ val t = (1, true, "hello", 'c', 0.2, 0.5f, 12345678912345L)
t: (Int, Boolean, String, Char, Double, Float, Long) = (
  1,
  true,
  "hello",
  'c',
  0.2,
  0.5F,
  12345678912345L
)
</> 3.19.scala

大元组变量让人迷惑:使用 ._1 ._2 ._3 时可能还好,但你使用 ._11 ._13 时很容易混淆不同字段。如果你发现自己使用大元组,就该考虑定义一个 (3.4) ,或者使用在 Chapter 5: Scala特性 中介绍的Case Class。

3.1.5 数组

数组用 Array[T](a, b, c) 来实例化,数组中第n个元素用 a(n) 检索:

@ val a = Array[Int](1, 2, 3, 4)

@ a(0) // first entry, array indices start from 0
res36: Int = 1

@ a(3) // last entry
res37: Int = 4

@ val a2 = Array[String]("one", "two", "three", "four")
a2: Array[String] = Array("one", "two", "three", "four")

@ a2(1) // second entry
res39: String = "two"
</> 3.20.scala

方括号 [Int] [String] 是类型参数,决定了数组类型。括号 (1, 2, 3, 4) 中的参数决定了数组的初始内容。通过索引查询数组应使用括号 a(3),而不是像其它常见语言那样使用方括号 a[3]

你可以省略类型参数,让编译器推断数组的类型。或者使用 new Array[T](length) 创建一个指明类型的空数组,然后为每个索引位置分配值:

@ val a = Array(1, 2, 3, 4)
a: Array[Int] = Array(1, 2, 3, 4)

@ val a2 = Array(
    "one", "two",
    "three", "four"
  )
a2: Array[String] = Array(
  "one", "two",
  "three", "four"
)
</> 3.21.scala
@ val a = new Array[Int](4)
a: Array[Int] = Array(0, 0, 0, 0)

@ a(0) = 1

@ a(2) = 100

@ a
res45: Array[Int] = Array(1, 0, 100, 0)
</> 3.22.scala

new Array 创建的数组,所有元素使用默认值初始化,数值型数组用 0 填充,Boolean 数组用 false 填充,String 和其它类型用 null 填充。Array 长度固定但其元素可变,你可以修改每个元素的值,但是不能添加或者删除元素。我们会在 Chapter 4: Scala集合 看到如何创建长度可变的集合。

Scala也支持多维数组,即数组的数组:

@ val multi = Array(Array(1, 2), Array(3, 4))
multi: Array[Array[Int]] = Array(Array(1, 2), Array(3, 4))

@ multi(0)(0)
res47: Int = 1

@ multi(0)(1)
res48: Int = 2

@ multi(1)(0)
res49: Int = 3

@ multi(1)(1)
res50: Int = 4
</> 3.23.scala

多维数组用于代表网格,矩阵,以及其它类似数据结构。

3.1.6 Options

Scala Option[T] 代表一种可能存在,也可能不存在的值。Option[T] 子类型 Some(v: T) 指示其值存在,None 指示其值不存在:

@ def hello(title: String, firstName: String, lastNameOpt: Option[String]) = {
    lastNameOpt match {
      case Some(lastName) => println(s"Hello $title. $lastName")
      case None => println(s"Hello $firstName")
    }
  }

@ hello("Mr", "Haoyi", None)
Hello Haoyi

@ hello("Mr", "Haoyi", Some("Li"))
Hello Mr. Li
</> 3.24.scala

上述例子展示了如何用 SomeNone 构造 Option对象,以及如何匹配它们。Scala的许多API依赖 Option 而不是 null 来表达可有可无的值。通常,Option 强迫你同时处理“出现”和“缺失”两种场景,而使用 null 容易让你忘记该值是否可能为null,在运行时碰到令人迷惑的 NullPointerException 错误。我们会在 Chapter 5: Scala特性 深入介绍模式匹配。

Option 包含一些有用的方法,可以轻松处理空值,比如 getOrElse,如果 OptionNone,就返回你提供的值:

@ Some("Li").getOrElse("<unknown>")
res54: String = "Li"

@ None.getOrElse("<unknown>")
res55: String = "<unknown>"
</> 3.25.scala

Option 很像大小是 0 或者 1的集合。你可以像普通集合一样循环遍历它们,或者使用 .map 这样的标准集合操作来处理它们。

@ def hello2(name: Option[String]) = {
    for (s <- name) println(s"Hello $s")
  }

@ hello2(None) // does nothing

@ hello2(Some("Haoyi"))
Hello Haoyi
</> 3.26.scala
@ def nameLength(name: Option[String]) = {
    name.map(_.length).getOrElse(-1)
  }

@ nameLength(Some("Haoyi"))
res60: Int = 5

@ nameLength(None)
res61: Int = -1
</> 3.27.scala

上面,我们结合 .map.getOrElse ,当值存在时打印name的长度,否则打印 -1。我们会在 Chapter 4: Scala集合 学习更多关于集合的操作。

See example 3.1 - Values

3.2 循环,条件,Comprehensions

3.2.1 For循环

Scala的For循环类似其他语言中的“foreach”循环:它们直接遍历集合元素,而不用维护自增索引。如果你想遍历索引范围,可以使用 Range,比如 Range(0, 5)

@ var total = 0

@ val items = Array(1, 10, 100, 1000)

@ for (item <- items) total += item

@ total
res65: Int = 1111
</> 3.28.scala
@ var total = 0

@ for (i <- Range(0, 5)) {
    println("Looping " + i)
    total = total + i
  }
Looping 0
Looping 1
Looping 2
Looping 3
Looping 4

@ total
res68: Int = 10
</> 3.29.scala

在循环头部放置多个 <-,你可以遍历嵌套的 Array

@ val multi = Array(Array(1, 2, 3), Array(4, 5, 6))

@ for (arr <- multi; i <- arr) println(i)
1
2
3
4
5
6
</> 3.30.scala

循环可以使用 if 语法添加守卫从句作为过滤条件:

@ for (arr <- multi; i <- arr; if i % 2 == 0) println(i)
2
4
6
</> 3.31.scala

3.2.2 If-Else

if-else 条件语句和其它编程语言的类似。有一点需要注意,Scala if-else 可以作为表达式使用,这有点像其它语言中的 a ? b : c 三目运算符。Scala 没有单独的三目运算符语法,但 if-else 可以直接写在如下所示 total += 的右侧。

@ var total = 0

@ for (i <- Range(0, 10)) {
    if (i % 2 == 0) total += i
    else total += 2
  }

@ total
res74: Int = 30
</> 3.32.scala
@ var total = 0

@ for (i <- Range(0, 10)) {
    total += (if (i % 2 == 0) i else 2)
  }

@ total
res77: Int = 30
</> 3.33.scala

3.2.3 Fizzbuzz

现在我们了解了Scala基本语法,让我们看看常见的“Fizzbuzz”编程挑战:

Write a short program that prints each number from 1 to 100 on a new line.

For each multiple of 3, print "Fizz" instead of the number.

For each multiple of 5, print "Buzz" instead of the number.

For numbers which are multiples of both 3 and 5, print "FizzBuzz" instead of the number.

我们可以如下实现:

@ for (i <- Range.inclusive(1, 100)) {
    if (i % 3 == 0 && i % 5 == 0) println("FizzBuzz")
    else if (i % 3 == 0) println("Fizz")
    else if (i % 5 == 0) println("Buzz")
    else println(i)
  }
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
...
</> 3.34.scala

既然 if-else 是一个表达式,我们也可以这样实现:

@ for (i <- Range.inclusive(1, 100)) {
    println(
      if (i % 3 == 0 && i % 5 == 0) "FizzBuzz"
      else if (i % 3 == 0) "Fizz"
      else if (i % 5 == 0) "Buzz"
      else i
    )
  }
</> 3.35.scala

3.2.4 Comprehensions

除了使用 for 定义循环来执行操作,你也可以结合 yield 转换一个集合,得到一个新集合:

@ val a = Array(1, 2, 3, 4)

@ val a2 = for (i <- a) yield i * i
a2: Array[Int] = Array(1, 4, 9, 16)

@ val a3 = for (i <- a) yield "hello " + i
a3: Array[String] = Array("hello 1", "hello 2", "hello 3", "hello 4")
</> 3.36.scala

和循环类似,你可以在括号中使用 if 守卫从句,过滤出最终集合:

@ val a4 = for (i <- a if i % 2 == 0) yield "hello " + i
a4: Array[String] = Array("hello 2", "hello 4")
</> 3.37.scala

Comprehensions支持输入多个数组,如下 ab,得到一个打平的 Array,类似于使用嵌套for循环:

@ val a = Array(1, 2); val b = Array("hello", "world")

@ val flattened = for (i <- a; s <- b) yield s + i
flattened: Array[String] = Array("hello1", "world1", "hello2", "world2")
</> 3.38.scala

如果你想用多行展开嵌套循环,以便于阅读代码,你可以使用花括号 {} 替换括号 ()。注意嵌套comprehension的 <- 顺序很重要,就像嵌套循环的顺序如何影响循环动作发生顺序一样:

@ val flattened = for{
    i <- a
    s <- b
  } yield s + i
flattened: Array[String] = Array("hello1", "world1", "hello2", "world2")

@ val flattened2 = for{
    s <- b
    i <- a
  } yield s + i
flattened2: Array[String] = Array("hello1", "hello2", "world1", "world2")
</> 3.39.scala

我们可以使用comprehension编写一个不立即打印结果到控制台的FizzBuzz版本,而是返回一个 Seq (“sequence”的简写):

@ val fizzbuzz = for (i <- Range.inclusive(1, 100)) yield {
    if (i % 3 == 0 && i % 5 == 0) "FizzBuzz"
    else if (i % 3 == 0) "Fizz"
    else if (i % 5 == 0) "Buzz"
    else i.toString
  }
fizzbuzz: IndexedSeq[String] = Vector(
  "1",
  "2",
  "Fizz",
  "4",
  "Buzz",
...
</> 3.40.scala

然后我们可以任意使用这个 fizzbuzz 集合:把它保存在一个变量中,或者传递到方法中,又或者用其他方式处理。我们会在 Chapter 4: Scala集合 介绍怎样操作这个集合。

3.3 方法与函数

3.3.1 方法

你可以用 def 关键字定义方法:

@ def printHello(times: Int) = {
    println("hello " + times)
  }

@ printHello(1)
hello 1

@ printHello(times = 2) // argument name provided explicitly
hello 2
</> 3.41.scala

传递错误类型的参数,或者缺失必要参数时,会得到一个编译错误。如果参数有默认值,可以不传递它。

@ printHello("1") // wrong type of argument
cmd128.sc:1: type mismatch;
 found   : String("1")
 required: Int
val res128 = printHello("1")
                         ^
Compilation Failed
</> 3.42.scala
@ def printHello2(times: Int = 0) = {
    println("hello " + times)
  }

@ printHello2(1)
hello 1

@ printHello2()
hello 0
</> 3.43.scala

3.3.1.1 方法返回值

方法执行动作除了打印,还可以返回值。花括号 {} 代码块里的最后一个表达式的估值结果就是方法的返回值。

@ def hello(i: Int = 0) = {
    "hello " + i
  }

@ hello(1)
res96: String = "hello 1"
</> 3.44.scala

你可以调用方法打印其返回值,或者执行其它计算:

@ println(hello())
hello 0

@ val helloHello = hello(123) + " " + hello(456)
helloHello: String = "hello 123 hello 456"

@ helloHello.reverse
res99: String = "654 olleh 321 olleh"
</> 3.45.scala

3.3.2 函数

你可以使用 => 语法定义函数。函数与方法类似,你可以带参调用它们,执行一个动作,或者返回一个值。不同于方法,函数本身是一个value:你可以当参数传递它们;或者用变量存储起来,稍后再调用它们。

@ var g: Int => Int = i => i + 1

@ g(10)
res101: Int = 11

@ g = i => i * 2

@ g(10)
res103: Int = 20
</> 3.46.scala

注意不同于方法,函数不能有可选参数(即带默认值的参数),也不支持语法 [T] 定义类型参数。当方法转为函数时,可选参数必须被传递,类型参数也要固定为具体类型。函数是匿名的,这使得涉及它们的栈踪迹相较于方法而言,阅读起来不太方便。

通常,你应该优先使用方法而非函数,除非你需要灵活地将其作为参数传递,或者存储在变量中。

3.3.2.1 带函数参数的方法

一个常见用例就是把函数作为参数传递到方法中。这样的方法称为“高阶方法”。下文中,我们有一个类 Box,它有一个方法 printMsg,打印的内容是一个 Int 变量,另外还有一个独立方法 update,输入一个类型是 Int => Int 的函数,用来更新 x。你可以传递一个函数字面量给 update 以改变 x 的值:

@ class Box(var x: Int) {
    def update(f: Int => Int) = x = f(x)
    def printMsg(msg: String) = {
      println(msg + x)
    }
  }
</> 3.47.scala
@ val b = new Box(1)

@ b.printMsg("Hello")
Hello1

@ b.update(i => i + 5)

@ b.printMsg("Hello")
Hello6
</> 3.48.scala

简单的函数字面量,比如 i => i + 5 可以简写为 _ + 5,下划线 _ 代表函数参数。

@ b.update(_ + 5)

@ b.printMsg("Hello")
Hello11
</> 3.49.scala

函数字面量中的占位符语法对多参数函数同样适用,比如 (x, y) => x + y 可以写作 _ + _

把函数作为参数的方法,可以接收方法引用,只要其签名符合函数类型要求,本例中其类型是 Int => Int

@ def increment(i: Int) = i + 1

@ val b = new Box(123)

@ b.update(increment) // Providing a method reference

@ b.update(x => increment(x)) // Explicitly writing out the function literal

@ b.update{x => increment(x)} // Methods taking a single function can be called with {}s

@ b.update(increment(_)) // You can also use the `_` placeholder syntax

@ b.printMsg("result: ")
result: 127
</> 3.50.scala

3.3.2.2 多参数列表

方法可以接收多个参数列表。这对于编写控制结构类的高阶方法很有用,比如下列方法 myLoop

@ def myLoop(start: Int, end: Int)
            (callback: Int => Unit) = {
    for (i <- Range(start, end)) {
      callback(i)
    }
  }
</> 3.51.scala
@ myLoop(start = 5, end = 10) { i =>
    println(s"i has value ${i}")
  }
i has value 5
i has value 6
i has value 7
i has value 8
i has value 9
</> 3.52.scala

把函数字面量传递到方法的能力在标准库中得到了很好的应用,实现了简洁地在集合上执行变换,我们将在 Chapter 4: Scala集合 看到更多例子。

3.4 类和Trait

你可以使用 class 关键字定义类,并用 new 实例化。默认情况下,所有传递给类构造器的参数在类的所有方法中都是可访问的:下面的(x: Int) 定义了类构造器的私有字段,因此 x 可在 方法 printMsg 中被访问,但是你不能在类外访问它:

@ class Foo(x: Int) {
    def printMsg(msg: String) = {
      println(msg + x)
    }
  }
</> 3.53.scala
@ val f = new Foo(1)

@ f.printMsg("hello")
hello1

@ f.x
cmd120.sc:1: value x is not a member of Foo
Compilation Failed
</> 3.54.scala

使用 val 修饰它,可以让它在类外也可访问;使用 var 不仅让它可以公开访问,也可修改:

@ class Bar(val x: Int) {
    def printMsg(msg: String) = {
      println(msg + x)
    }
  }
</> 3.55.scala
@ val b = new Bar(1)

@ b.x
res122: Int = 1
</> 3.56.scala
@ class Qux(var x: Int) {
    def printMsg(msg: String) = {
      x += 1
      println(msg + x)
    }
  }
</> 3.57.scala
@ val q = new Qux(1)

@ q.printMsg("hello")
hello2

@ q.printMsg("hello")
hello3
</> 3.58.scala

你可以在类里使用 val 或者 var 来存储数据。这些变量在类实例化时会计算一次:

@ class Baz(x: Int) {
    val bangs = "!" * x
    def printMsg(msg: String) = {
      println(msg + bangs)
    }
  }
</> 3.59.scala
@ val z = new Baz(3)

@ z.printMsg("hello")
hello!!!
</> 3.60.scala

3.4.1 Traits

trait 类似传统面向对象语言中的 interface:一系列方法的封装,通过类来继承,这些类实例之间可以交换使用。

@ trait Point{ def hypotenuse: Double }

@ class Point2D(x: Double, y: Double) extends Point{
    def hypotenuse = math.sqrt(x * x + y * y)
  }

@ class Point3D(x: Double, y: Double, z: Double) extends Point{
    def hypotenuse = math.sqrt(x * x + y * y + z * z)
  }

@ val points: Array[Point] = Array(new Point2D(1, 2), new Point3D(4, 5, 6))

@ for (p <- points) println(p.hypotenuse)
2.23606797749979
8.774964387392123
</> 3.61.scala

上面,我们定义了 Point trait,其拥有一个方法 def hypotenuse: Double。子类 Point2DPoint3D 有各自不同的参数集。因此我们可以把 Point2DPoint3D 都放进 points: Array[Point] 来一视同仁,这些对象都包括一个方法 def hypotenuse,而不用管对象的实际类是什么。

3.5 总结

本章,我们快速浏览了Scala语言核心。尽管特定的语法对你可能是新鲜的,但是它们的概念你应该是熟悉的:原始类型,数组,循环,条件,方法,以及类,这几乎是每种编程语言必有的一部分。接下来,我们会接触Scala核心标准库:Scala集合。

Exercise: Define a def flexibleFizzBuzz method that takes a String => Unit callback function as its argument, and allows the caller to decide what they want to do with the output. The caller can choose to ignore the output, println the output directly, or store the output in a previously-allocated array they already have handy.

@ flexibleFizzBuzz(s => {} /* do nothing */)

@ flexibleFizzBuzz(s => println(s))
1
2
Fizz
4
Buzz
...
</> 3.62.scala
@ var i = 0

@ val output = new Array[String](100)

@ flexibleFizzBuzz{s =>
    output(i) = s
    i += 1
  }

@ output
res125: Array[String] = Array(
  "1",
  "2",
  "Fizz",
  "4",
  "Buzz",
...
</> 3.63.scala
See example 3.5 - FlexibleFizzBuzz

Exercise: Write a recursive method printMessages that can receive an array of Msg class instances, each with an optional parent ID, and use it to print out a threaded fashion. That means that child messages are print out indented underneath their parents, and the nesting can be arbitrarily deep.

class Msg(val id: Int, val parent: Option[Int], val txt: String)
def printMessages(messages: Array[Msg]): Unit = ...
</> 3.64.scala
TestPrintMessages.scprintMessages(Array(
  new Msg(0, None, "Hello"),
  new Msg(1, Some(0), "World"),
  new Msg(2, None, "I am Cow"),
  new Msg(3, Some(2), "Hear me moo"),
  new Msg(4, Some(2), "Here I stand"),
  new Msg(5, Some(2), "I am Cow"),
  new Msg(6, Some(5), "Here me moo, moo")
))</> 3.65.scala
expected.txt#0 Hello
    #1 World
#2 I am Cow
    #3 Hear me moo
    #4 Here I stand
    #5 I am Cow
        #6 Here me moo, moo</> 3.66.output
See example 3.6 - PrintMessages

Exercise: Define a pair of methods withFileWriter and withFileReader that can be called as shown below. Each method should take the name of a file, and a function value that is called with a java.io.BufferedReader or java.io.BufferedWriter that it can use to read or write data. Opening and closing of the reader/writer should be automatic, such that a caller cannot forget to close the file. This is similar to Python "context managers" or Java "try-with-resource" syntax.

TestContextManagers.scwithFileWriter("File.txt") { writer =>
  writer.write("Hello\n"); writer.write("World!")
}
val result = withFileReader("File.txt") { reader =>
  reader.readLine() + "\n" + reader.readLine()
}
assert(result == "Hello\nWorld!")</> 3.67.scala

You can use the Java standard library APIs java.nio.file.Files.newBufferedWriter and newBufferedReader for working with file readers and writers. We will get more familiar with working with files and the filesystem in Chapter 7: Files and Subprocesses.

See example 3.7 - ContextManagers
Discuss Chapter 3 online at https://www.handsonscala.com/discuss/3