在这个 2020 年初的特殊时期。
Scala 是一门现代化,多范式的 JVM 语言。
传送门(可能需要🚀)
W1
case class 和模式匹配
用 Scala 来表示 JSON 对象可以这样
1 | abstract class JSON |
所以,
1 | { |
可以表示为
1 | val data = JObj(Map( |
比如说我要实现一个打印 JSON 对象的方法,就可以
1 | def show(json: JSON): String = json match { |
上述的 show(data) 就输出
1 | res1: String = {"firstName": "John", "lastName": "Smith", "address": {"streetAddress": "21 2nd Street", "state": "NY", "postalCode": 10021.0}, "phoneNumbers": [{"type": "home", "number": "212 555-1234"}, {"type": "fax", "number": "646 555-4567"}]} |
第 6 行代码里面 map 传入的函数是,我们知道,在这里 map 函数的签名是 def map[B](f: (A) => B): List[B]
那么第六行的 f
就是 case (key, value) => "\"" + key + "\": " + show(value)
。 这个函数如果单独拎出来,其实就是
1 | val f: (String, JSON) => String = { case (key, value) => key + ”: ” + value } // 注意看 JObj 定义 |
所以用 case class 最方便的地方在于模式匹配中的 decompose
函数即对象
任何情况下,函数 A => B
其实就是
1 | scala.Function1[A, B] |
的简写。
普通函数
根据 Function1 的定义
1 | trait Function1[-A, +R] { |
例如,
1 | val f1: Int => Int = (x: Int) => x + 1 |
f1 和 f2 等价。
偏函数
对于偏函数 PartialFunction 定义为
1 | trait PartialFunction[-A, +R] extends Function1[-A, +R] { |
例如,
1 | val f1: PartialFunction[String, String] = { case "ping" => "pong" } |
f1 和 f2 等价。
ps: 以上,「等价」的意思是两个函数只要输入相同,输出一定相同。
W2
LazyList
下面代码
1 | def f(x: Int): Boolean = { |
v1 和 v2 等价。计算 v1 的时候会打印 1 2 3...9999 100000
, 而计算 v2 的时候打印 1 2 3 ... 21 22
。这是因为对于 v2,(1 to 100000).to(LazyList)
产生的是 scala.collection.immutable.LazyList
。其中官方文档对其有个简单的描述
This class implements an immutable linked list that evaluates elements in order and only when needed
也就是当 LazyList 里面的元素被需要的时候,才会进行有序计算,并且计算会终止知道所需的元素计算结束。这也就是为什么会有上述的输出了。
惰性求值 Lazy Evaluation
在 Scala 里面,Lazy 意味着两点
- 尽量延后求值计算
- 只计算一次
例如
1 | def expr = { |
这样的输出为 xzyz
Infinite Sequences
利用 LazyList 的特性,可以构造出无限序列。比如
1 | scala> def from(n: Int): LazyList[Int] = n #:: from(n + 1) // 无限序列 |
埃拉托斯特尼筛法
埃拉托斯特尼筛法是列出所有小素数最有效的方法之一,其名字来自于古希腊数学家埃拉托斯特尼,并且被描述在另一位古希腊数学家尼科马库斯所著的《算术入门》中。-维基百科
用 LazyList 实现会非常简单,
1 | def from(n: Int): LazyList[Int] = { |
题目:倒水问题
给几个知道容量,但是没有刻度的杯子,有个可以取之不尽的水源用来给杯子装满水和将杯子的水全部倒出。我们需要做的就是盛出给定容量的水。我们能做的操作有三个
- empty 清空
- fill 放满水
- pour(a, b) 把 a 的水全部往 b 倒直至 b 杯满了或者 a 杯没水了
现在需要找到取得容量为 N 的水时,需要的操作。
附:1. Scala 完整代码 2. python 解法
W3
下面会用 Scala 进行加法器模拟。对于计算机中,两个数如何相加,可以提前阅读 维基百科-加法器,最好能够做一下 位运算实现加法。
通过以上内容,我们需要知道的最重要的点在于:两个一位二进制数相加,将会产生两个输出,其中一个是当前位相加的值 S,另一个是相加后的进位信息 C。其中 S 可以用异或门,进位可以用与门。两个半加器用或门组合为一个全加器。
根据上图,我们至少需要实现的是
电线:用来传导信号,信号可以用 boolean 类型表示 0 和 1
1
2
3
4
5
6
7
8
9class Wire {
private var sigVal = false
def getSignal: Boolean = sigVal
def setSignal(s: Boolean): Unit =
if (s != sigVal) {
sigVal = s
actions foreach(_())
}
}与门:输入两个电信号,输出 & 对应的信号
1 | def andGate(in1: Wire, in2: Wire, output: Wire): Unit = ??? |
- 或门:输入两个电信号,输出 | 对应的信号
1 | def orGate(in1: Wire, in2: Wire, output: Wire): Unit = ??? |
- 逆变器:输入电信号,输出 ! 对应的信号
1 | def inverter(in: Wire, output: Wire): Unit = ??? |
在此基础上,实现出半加器和全加器
1 | def halfAdder(a: Wire, b: Wire, s: Wire, c: Wire): Unit = { |
1 | def fullAdder(a: Wire, b: Wire, cin: Wire, sum: Wire, cout: Wire): Unit = { |
实现了全加器后。加入模拟信号的输入输出即可,完整代码 模拟加法器
W4
Functional Reactive Programming (FRP)
在 MVC 模型中,FRP 可以让 model 在产生变化的同时 view 自动变化。这样的模型一般也就是 pub-sub 模型或者叫观察者模型 (observers)。
1 | Subscriber1 subscribe ___________ |
通用的说比如 a = f(b)
里面。当 b 在发生变化的时候 a 也会跟着变。实现这一点,
可以把「变化」归化为「event」,而自动检查这些「event」的东西叫做 Signal。在 Scala 里面通常把这样的 Signal 实现为一个类型,通过 apply() 给其赋值。比如
1 | val s = Signal(1) |
同时,为了给 Signal 再次赋值,可以扩展新增一个 update 方法。比如 s.update(5)
,顺带可以写成 s() = 5
。这样的话,实现一个扩展了 Signal 名为 Var
的类型,让其拥有 update
方法。
在这样的条件下,
1 | // (1) |
(1) 里面的 twice 变为 4,(2)里面的 twice 还是 2。上述体现的 FRP 里面,num 是 publisher,twice 是 subscriber。num 产生了 event,twice 接受这样的 event。并且所有的变量本身都是 immutable。
实现 FRP
Var 和 Signal
我们知道 Signal 和 Var 应该是这样
1 | // signal |
维护订阅关系
有了这些方法,就可以做到赋值和再次赋值。还需要的是,当一个 publisher 产生了 event 的时候,如何自动通知到对应的 subscriber,并且让其重新计算(re-evaluate)。
例如,s 是 Var 类型,并且进入到了表达式 expr 里,形如 expr s
,那么当表达式 expr s
,作为参数传递给了某个 Signal 形如 val t = Signal(expr s)
的时候。就需要给 s 维护一个 subscriber。同理,当有多个 subscribers 的时候就给 s 维护一个 subscriber 的集合。比如
1 | // val set = Set() |
触发的条件达到了,可是如何把 Signal(num() * 3)
这样的对象传递给 num 的 set ?
这个时候就需要在 Object Signal 里面新增一块用于维护方法调用的数据结构,没做就是 stack
1 | object Signal { |
StackableVariable 在当前的上下文中多个 Signal/Object 会共享。当调用 class Signal 的 apply() 方法的时候就去这里面找到当前的调用方。
维护调用栈
调用栈里面存放有序的 Signal。例如,有下面测试代码
1 | test("multi subscribers") { |
第三行开始,包含表达式的 Signal 进入调用栈,并不断被带入 subscriber 的集合。
1 | class StackableVariable[T](init: T) { |