320. class Pair[+T]表示该类型是与T协变的,它与T按照同样的方向型变,比如Student是Person的子类型,那么Pair[Student]也就是Pair[Person]的子类型; 逆变(如trait Friend[-T])是类型变化的方向和子类型方向是相反的,比如Student是Person的子类型,但Friend[Student]是Friend [Person]的超类型;可以同时使用两种型变,比如单参数函数类型为Function[-A, +R]
321. 对于某个对象消费的值适用逆变,对于它产生的值适用协变,前者指函数的参数,后者指函数的返回值;如果一个对象同时消费和产出某值,那类型应该保持不变,如scala数组是不支持型变的,比如不能将Array[Student]转换成Array[Person]或者反过来,因为会不安全
322. 对象无法添加类型参数,即对象不能泛型,比如object Empty[T] extends List[T]是错误的,正确做法是 object Empty extends List[Nothing],这里用到的知识点是,Nothing是一切类型的子类型,而List是协变的,因此List[Nothing]可转成List[Int]等List[T],所以Empty能转成一切List[T];但事实上经过我测试,这样也是不行的,报illegal inheritance from sealed class List,原因是List是sealed abstract class List[+A],即它是密封类,不能被扩展了,除非写到源文件中
323. java中所有泛型类都是不变的,可以使用通配符改变它们的类型,例如? extends Person,也可以在scala中使用通配符,例如_<:Person;scala中对于协变的类,不需要通配符;可以对逆变使用通配符;类型通配符是用来指代存在类型的语法糖;注意:scala的类型通配符并不完善,比如有时候需要先type XX[T]=Comparable[_>:T],再引用XX[T],而不能写到一起,如直接def min[T <: Comparable[_>:T]](p: Pair[T])
324. 类中每个方法都返回this的作用是,可以将方法调用串联起来;如果存在继承,只返回this会出错,因为调用完第一个方法,可能返回的是父类,而父类中可能没有第二个方法,此时只需把方法声明的返回类型设为this.type
325. 方法要想传入单例对象(如Title)作为函数参数,需写为obj:Title.type,而不能写为obj:Title; 当然,前提是定义了 object Title 这个单例对象,这样就能执行book.set(Title).to("...")这句话了;总结一下:单例类型可用于方法串接和带对象参数的方法
326. 嵌套类从属于包含它的外部对象,如a.Member和b.Member是不同的类(其中a和b都是外部类Network的实例;且Member是Network的内部类);如果不希望有这个细粒度特性,可以将Member移到Network的外面,比如放到Network的伴生对象;而类型投影介于以上二者之间,类似内部类的结构,但在引用内部类时,加了修饰前缀,比如把Member改为Network#Member,表示任何Network类的Member内部类;类型投影不会被当做路径,也无法引入它(其余二者可以)
327. scala的路径要求,在最后的类型之前,路径的所有成分都必须是稳定的,即必须指定到单个有穷的范围,可以是包、对象、val、this/super/super[S]/C.this/C.super/C.super[S],而不能是类,也不能是var
328. 编译器会将所有嵌套的类型表达式如a.b.c.T都翻译成类型投影a.b.c.type#T
329. 类型投影对所有外部类的对象都包含了其内部类的实例
330. 可用type关键字创建类型别名;类型别名必须被嵌套在类或对象中,不能出现在scala文件的顶层;但REPL中可以在顶层,因为REPL的所有内容都隐式包含在顶层对象中
331. 结构类型是一组关于抽象方法、字段和类型的规格说明,比如方法的参数加上target:{ def append(str:String):Any } ;可以对任何具备该append方法的类的实例调用外层方法,比定义Appendable特质更灵活,因为特质不总是方便添加;幕后scala使用反射调用target.append(..);反射调用的开销比常规方法大的多,因此只有在无法共享一个特质的共通行为的时候才用结构类型
332. 结构类型和其他编程语言如js ruby的鸭子类型很相似,即变量没有类型,运行时才去检查变量是否具备特定方法,你不需要将obj声明为鸭子,只要它走起路来或叫起来像鸭子即可
333. 复合类型也叫交集类型,即实现了多个特质的类型;可以将结构类型的声明添加到简单类型或复合类型,如Shape with Serializable {def contains(p:Point):Boolean}
334. 中置类型是一个带有两个类型参数的类型,以中置语法表示,比如String Map Int,而不是Map[String,Int];定义type x[A,B]=(A,B),那么可以写String x Int,而不是(String,Int)了;中置类型操作符是左结合的,除非它们的名称以:结尾;String x Int x Int 等价于((String , Int), Int)
335. 中置类型的名称是任何除单*号外的操作符字符序列,是为了避免与变长参数声明T*混淆
336. 存在类型加入scala是为了与java的类型通配符兼容,它在类型表达式后面加forSome{...},其中花括号包含type和val的声明;Array[T] forSome {type T<:JComponent }等价于Array [_ <: JComponent];之前所学的scala的类型通配符只不过是存在类型的语法糖; Array[_]等价于Array [T] forSome {type T};Map[_,_]等价于Map [T,U] forSome {type T; type U}
337. forSome允许我们使用更复杂的关系,而不限于类型通配符的那些,例如Map [T,U] forSome {type T; type U<:T} ;也可以在forSome块中使用val声明,又因为val的变量可以有自己的嵌套类型,所以就会有以下这种,n.Member forSome {val n: Network };可以用类型投影Network#Member来代替以上代码; 对于更复杂的情况,就能体现出相对类型投影更强的功能,比如要接受相同网络的成员,而拒绝不同网络的成员
338. scala的类型系统,支持多种类型,其中方法类型是编译器内部使用的,格式为(T1…Tn)T,不带=>,例如def square(x:Int)=x*x得到的结果square: (x: Int)Int,而val triple=(x:Int)=>3*x得到的是triple: Int => Int,当然执行square _也能得到Int => Int,这是因为_把方法变为了函数
339. this:类型 是自身类型; 被自身类型声明的特质只能被混入给定类型的子类中;自身类型中的类型可以是复合类型,如this: T with U with ...=>;这里的this可以换成别的名字,如trait Group{ outer: Network => class Member {...} },这里的outer表示Group.this,当然Group特质要求被添加到Network的子类;注意:自身类型不会自动继承,如果需要,必须重复声明自身类型
340. scala中可以通过特质和自身类型来达到简单的依赖注入的效果;蛋糕模式是比简单的特质组合更好更自然的设计,同样能实现依赖注入
341. 类或特质可以定义一个在子类中被具体化的抽象类型,比如在trait Reader中定义type Contents,然后在其子类class StringReader中指定type Contents=String,这实际上等价于先定义trait Reader[C],然后定义class StringReader extends Reader[String];以上两种写法各有千秋;当类型在类被实例化时给出,则使用类型参数;当类型是在子类中给出,则使用抽象类型;使用抽象类型可以避免一长串类型参数,比如连续定义多个type,如type In=File、type Contents=BufferedImage,可以避免写扩展Reader[File, BufferedImage]的复杂代码;抽象类型还能描述类型的互相依赖,如type Event<:java.util.EventObject;对应的子类可以这样写type Event=java.awt.event.ActionEvent
342. scala风格的家族多态,可以基于类型参数和抽象类型这两种方式实现;其中使用类型参数会导致代码扩张厉害,而使用抽象类型,代码会更优雅更好看
343. List这样的泛型类型有时被称为类型构造器,因为List依赖类型T产出一个特定的类型,如给定Int,得到list[Int];而在此之上可以定义出更高级的、依赖于其他类型的类型的类型; 比如将Iterable[E]特质改造成Iterable[E,C[_]],就变成了高等类型,它依赖一个类型构造器(即C[_])来生成结果
344. scala的解析器库是很好的在scala语言中内嵌领域特定语言(DSL)的高级示例
345. 文法(grammar)指的是一组用于产出所有遵循某个特定结构的字符串的规则,比如算术表达式有它的文法规则; 文法通常以BNF的格式编写;文法的开头就包含了词法分析,具体是丢弃空白和注释,并形成词法单元,包括标识符、数字和符号;文法的非终结符号也叫做起始符号;要从起始符号开始,持续应用文法规则,直到所有非终结符都被替换,只剩下词法单元; 扩展的BNF,即EBNF允许给出可选元素和重复
346. 为使用scala解析器库,需要扩展Parsers特质,并定义由基本操作组合起来的解析操作;基本操作有匹配词法单元、两个操作间做选择、依次执行两个操作、重复一个操作、可选择执行一个操作;扩展RegexParsers特质可实现一个识别算术表达式的解析器,可用正则来匹配词法单元,主要方法有parse和parseAll(在我环境中Parsers和RegexParsers所在的包是scala-parser-combinators_2.12-1.1.2.jar;路径分别是scala.util.parsing.combinator.Parsers和RegexParsers)
347. 文法定义中的二选一、拼接、选项和重复在scala组合子解析器中对应为| ~ opt 和rep;但是也可以写p?而不是opt(p),写p*而不是rep(p)
348. 用^^来处理解析结果;例如number^^{_.toInt}中,^^将函数{_.toInt}应用到number对应的解析结果上;^^操作符没有特别的含义,只是恰巧比~的优先级低,比|的优先级高而已;在提供给^^的函数中使用模式匹配来将~结果拆开
349. 用~>和<~来丢弃那些在匹配后不再需要的词法单元;注意~>和<~的箭头指向被保留下来的部分,比如"*"~>factor的结果只是factor的计算结果;再如"("~>expr <~")"表示丢弃某个表达式外面的圆括号;在同一表达式中使用~ 、~> 、<~要格外小心,看清楚到底丢弃的是什么
350. 如果解析器函数在解析输入之前就调用自己,就会一直递归下去,这叫做左递归,是需要避免的,要重新表述文法,比如收集中间结果,按照正确的顺序组合
351. rep以及repsep组合子用于匹配零或多个重复项;其中repsep组合子处理常见的用分隔符分隔开的条目;log组合子帮助我们调试文法;into组合子可以存储先前组合子的信息到变量中供之后的组合子使用; 此外还有rep1、repN、chainl1、rep1sep、^^^、^?、guard、not、accept、success、failure、phrase、positioned等其他组合子;组合子其实就是算子或者说方法;详细见scala.util.parsing.combinator.Parsers
352. 记忆式解析器使用一个高效的解析算法,会捕获到之前的解析结果,能确保解析时间与输入长度成比例,能够接受左递归的语法;要使用记忆式解析器,需要混入PackratParsers特质,使用val或lazy val而不是def 来定义每个解析函数(因为def会每次返回不同的值),让每个解析方法返回PackratParsers[T]而不是Parsers[T],使用PackratReader并提供parserAll方法;详细见scala.util.parsing.combinator.PackratParsers
353. 回溯很低效,可以用~!操作符而不是~来表示不需要回溯,或者重新整理文法规则
354. Parser[T]是带单个参数的函数,参数类型为Reader[Elem],返回值类型为ParseResult[T]
355. RegexParsers是正则解析器,而JavaTokenParsers扩展了它,并给出了五个词法单元的定义
356. 基于词法单元的解析器使用Reader[Token]而不是Reader[Char],这里的Token是在scala. util. parsing. combinator. token. Tokens特质中;StdTokens子特质定义了四种常用的词法单元,Identifier、Keyword、NumericLit和StringLit;StandardTokenParsers类定义了产出以上词法单元的解析器
357. 当解析器不能接受某个输入,需要指出错误发生的位置,比如添加failure语句,由parseAll方法返回Failure结果,它有msg属性和next属性; 可以用positioned组合子将位置信息添加到解析结果,而返回结果的类型必须扩展Positional特质
358. actor提供了并发中与传统基于锁结构不同的选择,它避免锁和共享状态,更容易实现没有死锁和争用的程序;除了scala类库scala-actors.jar提供的actor模型实现,还有更高级的实现如akka
359. actor是扩展自Actor特质的类,带有抽象方法act,通常act方法带有消息循环;act方法和java的Runnable接口的run方法很相似,不同actor的act也是并行运行的,不过actor对消息响应做了优化;起动actor实例,只需调用start方法;可以创建临时actor而不是定义类,借助Actor伴生对象的actor方法; 使用Actor需要import scala.actors.Actor(但从Scala 2.11.0版本开始,Scala的Actor库scala-actors.jar已经过时了;早在Scala2.10.0的时候,默认的actor库即是Akka,见Scala Actors迁移指南 | Scala Documentation)
360. 要往actor发送消息,可以用actor!message,消息发送是异步的,发完就忘;好的做法是使用样例类作为消息,这样actor就可以使用模式匹配处理消息
361. 要接收消息,可以用receive或react,通常在循环中做;receive/react的参数是case代码块,技术上讲是一个偏函数;消息是异步的,顺序是未知的,不要依赖任何特定的消息投递顺序;如果receive没有消息,会阻塞,直到有消息抵达;如果有消息,但无法被偏函数匹配处理,也会阻塞;为了避免邮箱被不与任何case匹配的消息占满,可以添加case _来处理任意消息
362. actor运行在单个线程中;邮箱会自动串行化消息;无需担心actor代码中的争用问题;actor会安全的修改它自己的数据;只有当修改了不同actor之间的共享数据,才有可能会出现争用;因此不要在不同actor中使用共享对象,除非你确定该对象的访问是线程安全的
363. 由于不鼓励使用共享对象,当actor计算出结果后,应该立刻向另一个actor发送消息,那么如何知道往哪里发送,有以下设计选择:1. 使用全局actor,但伸缩性不好 2. actor构造带有指向一个或多个actor的引用 3. actor接收带有指向另一个actor引用的消息 4. actor返回消息给发送方
364. 当actor握有另一个actor引用时,应该只使用这个引用来发送消息,而不是调用方法
365. 除了对actor共享引用,还可以共享消息通道;消息通道是类型安全的,只能接受某个类型的消息,不会不小心调用到别的actor方法;消息通道可以是带!方法的OutputChannel 或者带receive/react方法的InputChannel;Channel类同时扩展了以上两个特质,即它是双向的;可以给Channel提供一个actor来构造消息通道,如果不提供,就会绑定到当前执行的actor上
366. actor可以发送消息并等待回复,用!?操作符即可,例如account !? Deposit(1000);接受方必须回复一个消息给发送方,如sender!Balance(balance),也可以写为reply(Balance(balance)) ;注意:同步很容易引发死锁,最好避免在act方法中执行阻塞调用
367. 可以使用receiveWithin来指定想要等待多少毫秒,而不是一直等待下去;如果在指定时间没收到消息,将收到一个Actor.TIMEOUT对象; react也有带时间的版本,叫做reactWithin
368. 除了等待对方返回结果,也可以选择接收一个future,用!!即可,例如account!!Deposit(1000)
369. 共享线程是指在一个线程中运行多个actor,前提是每个消息处理函数只需要做比较小规模的工作,就继续等待下一条消息,可以降低线程总开销(提示:所以actor不等于线程!); scala中可以用两个嵌套的react语句实现,即react-case-react-case;可以给react的偏函数命名来方便解释控制流转,比如f1、f2;总结:不同actor可以通过react而不是receive来共享线程,前提是消息处理器的控制流转足够简单
370. 由于react会退出,因此无法简单的将它放在while循环中,可以通过再次调用act方法解决,即用act无穷递归来代替while true的无穷循环; 有一些控制流转组合子可以自动产出这些循环,比如loop和loopWhile组合子,后者可以带条件;eventloop可以制作出无穷循环套react的简化版,前提是偏函数不会再次调用react
371. actor在以下条件会终止:act方法返回、act由于异常终止、actor调用exit,注意exit只能被Actor的子类调用;exit的重载版本可以接受一个参数来描述退出原因;不带参数exit等价于exit('normal);actor异常终止,退出原因就是UncaughtException样例类的实例,它的属性有actor、message、sender、thread、cause;所有未处理异常都以UnhandledException退出,可以重写它的exceptionHandler方法,该方法产出一个PartialFunction[Exception, Unit]
372. 如果将两个actor链接在一起,那么每个都会在另一个终止时得到通知,只需调用link(actor)即可;尽管双向,但不对称,不能将link(worker)替换为worker.link(self);默认情况下,互相链接的actor当有一个非正常退出,当前actor也会终止,且退出原因是一样的,但可以改变,做法是设置trapExit为true;在较大的系统中,可以将actor分组,分到不同区域,每个区域都有监管actor
373. 设计actor系统的建议:1. 避免使用共享状态 2. 不要调用actor的方法 3. 保持每个actor的简单 4. 将上下文数据包含在消息中 5. 最小化给发送方的回复 6. 最小化阻塞调用 7. 尽可能使用react 8. 建立失败区
374. 思考:actor不是线程,它和线程啥关系?actor运行在单个线程中;actor是一个处理异步消息的对象,actor是对象不是线程?Actor是一种模型,和编程语言无关?Actor模型的本质是啥?已知Actor模型最初被Erlang语言实现,并在爱立信的电信系统中获取了巨大成功;java能实现actor吗?有相关实现吗? actor和tcp、socket什么关系?actor和reactor啥关系?
375. Actor既是特质也是伴生对象,前者是trait Actor extends scala.AnyRef with scala.actors.InternalActor with scala.actors.ReplyReactor;后者是object Actor extends scala.AnyRef with scala.actors.Combinators with scala.Serializable,方法有actor、reactor、receive、receiveWithin、react、reactWithin、eventloop、sender、reply、link、unlink、exit等
376. scala.actors.ReplyReactor特质扩展自scala.actors.InternalReplyReactor,后者又扩展自scala. actors. Reactor [scala.Any];而Reactor特质的具体声明是trait Reactor[Msg >: scala.Null] extends scala.AnyRef with scala.actors.OutputChannel[Msg] with scala.actors.Combinators;其字段有mailbox、sendBuffer等;其方法有mailboxSize、!、act、send、forward、receiver、react、preAct、start、restart等
377. Channel类的声明为class Channel[Msg](val receiver : scala.actors.InternalActor) extends scala.AnyRef with scala.actors.InputChannel[Msg] with scala.actors.OutputChannel[Msg] with scala.actors.CanReply[Msg, scala.Any];方法有!、send、forward、receive、?、receiveWithin、react、reactWithin、!?、!!
378. 隐式转换函数是以implicit声明的带有单个参数的函数;隐式转换可用来丰富现有类库的功能;可将隐式转换函数放入某个类的伴生对象中;建议用source2Target这种约定俗成的命名
379. 利用隐式转换,可以达到为File类增加一个自定义的read方法的效果,通过定义一个RichFile类,以及一个file2RichFile隐式转换函数,在RichFile定义read,本质上用Source.fromFile实现
380. 在REPL中,键入:implicits可查看所有除PreDef外被引入的隐式成员;或者键入:implicits -v查看全部;当想选择特定的转换,可直接import它;当不想用它,可直接将它排除在外,如import ….{xxx=>_,_},即引入除xxx以外的所有成员
381. 隐式转换在以下情况会被考虑: 1. 当表达式类型与预期不符 2. 当对象访问一个不存在的成员 3. 当对象调用某个方法的参数与传入参数不匹配;以下情况不会尝试隐式转换: 1. 已经能通过编译 2. 不会尝试同时执行多个转换 3. 如果存在二义性的转换,就会报错;可执行scalac -Xprint: typer xx.scala查看编译器加入了哪些隐式转换
382. 方法在调用时,其隐式参数也可以显式给出,当它已有参数列表,那么就会出现柯里化;当略去隐式参数列表,编译器就会去以下两个地方查找隐式值:当前作用域内可以用单个标识符指代的满足类型要求的val和def,以及相关类型的伴生对象
383. 对于给定的数据类型,只能有一个隐式值,例如def quote(what:String)(implicit left:String, right:String)是行不通的,而def quote(what:String)(implicit delims: Delimiters)是可以的
384. 隐式参数可以当做隐式转换;利用隐式参数进行隐式转换,是指如def smaller[T](a:T,b:T)(implicit order: T => Ordered[T])=if (a
385. 类的上下文界定(如class Pair[T:Ordering])的隐式值(如Ordering[T]),可以被用在该类的方法中,如def smaller(implicit ord:Ordering[T])={...};另外注意Predef中确实定义了类型为Ordering[Int]的隐式值,所以Int满足上下文界定
386. 可以用Predef的implicitly函数直接获取隐式值,如implicitly[Ordering[T]];Predef的implicitly函数用于从冥界召唤隐式值,意思是隐式值生活在冥界,以一种不可见的方式被加入到方法中;也可以利用Ordered特质定义的从Ordering到Ordered的隐式转换,就可以直接使用关系操作符
387. =:=,<:<,<%<是带有隐式值的类,定义在Predef.scala对象中;如果需要处理约束implicit ev:String<:
388. @implicitNotFound注解告诉编译器在不能构造出带有该注解的类型的参数时给出错误提示
389. CanBuildFrom[From,E,To]特质提供类型证明,可以创建一个类型为To的集合,握有类型为E的值,并且和类型From兼容;CanBuildFrom特质带有apply方法,产出类型为Builder[E,To]的对象,Builder类型带有一个+=方法用来将元素添加到内部缓存,还有result方法用来产出所要求的集合
390. map是一个Iterable[A,Repr]的方法;这里的Repr是展现类型;scala类库中map实际上是定义在TravrsableLike[A,Repr]特质中
391. CanBuildFrom来自scala/collection/generic/CanBuildFrom.scala;它是一个特质,具体是trait CanBuildFrom[-From, -Elem, +To];它有@implicitNotFound注解,用于提示无法从From变为To;它是一个 base trait for builder factories;方法只有两个apply方法,返回Builder[Elem, To],其中Builder也是特质,具体是trait Builder[-Elem, +To] extends Growable[Elem];CanBuildFrom用处广泛,被用在Future的sequence、traverse方法的隐式参数中;以及List、Map、Vector等的伴生对象中;另外在spark的org/apache/spark/storage/BlockManagerMaster.scala也出现了一次!!
392. 延续是这样一种机制,让你回到程序中之前的一个点;一个应用场景是读取文件异常,可以捕获并重新读取,通过直接跳回失败的点;捕获延续,通常使用一个shift结构,比如shift {k:(Unit => Unit) => cont=k };延续可以不带参数和返回值(可以是Unit类型),也可以带参数和返回值;跳回shift那个点,需要执行延续,简单调用cont即可;scala中延续是定界的,只能延续到给定的边界,边界由reset{…}给出;当调用cont,程序就从shift开始到reset块的边界
393. scala中要启用延续插件才能编译延续程序,如scalac -P:continuations:enable xxx.scala
394. 可以把shift块想象成reset块中的一个洞,当执行延续时,可以将一个值传到那个洞,运算继续,就好像shift本就是那个值一样
395. reset/shift的控制流转有些困惑,它们都有双重的职责,定义延续函数,捕获延续函数;shift之前的代码不是延续的一部分,延续从包含shift的表达式(即那个洞)开始,一直延展到reset的末尾
396. 如果reset块退出是因为执行了shift块,那么得到的值就是shift块的值,如果reset执行到它自己的末尾的话,它的值就是reset块的值,即块中最后一个表达式的值
397. reset和shift都是带有类型参数的方法,分别是reset[B,C]和shift[A,B,C],其中A是延续的参数类型,B是延续的返回类型,C是预期的reset表达式的类型;如果reset块有可能返回类型为B或C的值,由于分支或循环的原因,那么B必须是C的子类型;注意这些类型是很重要的,因为编译器可能无法推断出它们,比如A和B是Unit,而C是String,就会报错
398. 为了在jvm中提供延续,scala编译器对reset块中的代码进行cps(延续传递风格)的变换;经过变换的代码和常规scala代码很不一样,对于方法而言,区别尤为明显,如果方法包含shift,它将不会被编译为常规的方法,必须将它注解为被变换的方法;要调用包含了shift[A,B,C]的方法,必须被注解为@cpsParam(B,C),当B和C相同时,可用@cps[B];除了用@cps[Unit]外,还可以用@suspendable,但它更长,也没有更容易理解;任何位于reset和shift之间的方法都必须加上注解;这些注解不是让普通开发人员使用,而是帮助类库设计者产出特殊的控制流转结构的,有关延续的细节应该隐藏起来,不要让类库的用户觉察到它们的存在
399. 在递归中使用延续,有一个场景,那就是遍历子目录的所有文件,只想看前100个,一般递归无法停止,使用延续的话,每发现一个文件就跳出递归,如果需要更多结果,再跳回去;需要用while来代替for,因为for会被翻译为对foreach的调用,而后者没有被注解为cps,但是外层的processDirectory方法要加cps,因此无法调用它
400. reset和shift应该被包含在类库中,对使用者不可见,比如reset应该被放到迭代器的构造器中,这样对cont的调用就可以放在next方法了
401. 基于延续的一个很有前景的应用例子是撤销GUI或web编程中的控制反转
402. cps变换会产出一些对象,这些对象指定如何处理包含余下的运算的函数,比如shift{函数}会返回如下对象ControlContext [A,B,C](函数); 控制上下文(ControlContext )描述了如何处理延续函数,使用方式new ControlContext(k1 => fun(a=>k1(f(a))));ControlContext类中的map、flatMap方法和scala集合中常见的map、flatMap方法遵从同一组名为单子法则(monad laws)的规则
403. 注意:在scala 2.11.8中存在scala.util.continuations包(我的cmd),而在scala 2.12.15则不存在(我的idea);不过事实上,这个包不属于scala语言本身,而是单独的库,需要额外导入,如scala-continuations-library_2.11-1.0.2.jar和scala-continuations-plugin_2.11.8-1.0.2.jar
404. continuations包对象定义了type cps[A] = scala.util.continuations.cpsParam[A, A],以及shift、reset、shiftUnitR、shiftR等方法,而这些方法源码都是内置于jvm的compiled code
405. ControlContext类的具体声明为final class ControlContext[+A, -B, +C](val fun : scala. Function2[scala.Function1[A, B], scala.Function1[scala.Exception, B], C], val x : A) extends scala.AnyRef with scala.Serializable;方法主要有map、flatMap、foreach、foreachFull等
406. cpsParam类的具体声明是class cpsParam[-B, +C]() extends scala.annotation.Annotation with scala.annotation.StaticAnnotation with scala.annotation.TypeConstraint
407. 执行import scala.util.continuations._后,就能使用reset、shift了,如reset { shift { k : (Int=> Int) => k(8) } + 1 }
408. 用-Xprint:selectivecps编译可看到cps变换生成的代码;是否对应scala.tools.selectivecps中的SelectiveCPSTransform和SelectiveCPSPlugin呢?