気分次第です僕は 敵を選んで戦う少年

叶えたい未来も無くて 夢に描かれるのを待ってた

就让我再浪费一下午想一些没用的吧。成为最后几个没有offer的人,我的心态比想象当中好呢。

『怎么去写代码』,让我好好去思考这个问题的语言是Haskell,虽然我从来没用Haskell去写过正经代码,也请允许我厚着脸皮说出这一句,而让我开始学着去实践这一点的是Scala。我自认为也接触(浅尝则止)了很多语言从schemeclojure这些lisp系,在从c++gorust,从smlHaskell,再从pythonR这些动态类型语言。我只是在一个特殊的时候遇到了Scala,然后开始喜欢这种语言。我相信我应该没有『鼓吹』过Scala,因为我每次传销的时候都能记得Scala的一大堆缺点。

  • 较为复杂的语法。(别的不说,有C++在,我就只敢用较这个字)
  • 编译速度慢,而且二进制不兼容,启动速度慢,吃内存。(Benchmarks)
  • 泛型上要为JVM买单。本身类型推断也有一些坑。(比如SI2712)
  • sbt难用。
  • 反射库以及宏不好用。(面向语法树编程,那个数据结构也是有点乱,反正我是看了一会就不想看了,研究scala编译器还好,期待scala.meta)

其中一些在马上要出来的2.12版本中会得到改善,另一些会在DOT中得到改善,剩下一些就只能慢慢等着了。

除去这些剩下的,就是我在这姑且算是一年的接触Scala的过程中学到的,无关OOFP,无关任何准则,我自己的感受。

库设计基本等价于编程语言的设计

忘了在什么地方看到的大概是这个意思的话,当然这是一句很不严谨的话,不过我认为去认同这句话倒也不是大问题。

一般对于一门语言,我们在学习的时候,关注的主要在SyntaxSemanticSyntax强调要以什么样子写代码,Semantics可以看做一是type-checking类型验证,另一方面Evaluation即程序会如何运行。至于语言的标准库,基本是语言设计者对于语言的实践的体现,标准库的风格基本与语言的风格重合。Haskell中在强调范畴的各种概念,Scala的标准库也和语言一样,同时提供mutableimmutable两种风格。

而在库的设计上,同样存在相同的问题,语法和语义,库提供的功能应该是以尽可能简单的基本语义的基础上,通过可first class的对象compose得到的。这里强调的是compose的应该是类型。而不仅仅是函数或者对象。比如react之于传统的观察者模式,就在于其把eventsignal提升为可组合的first class,并且定义了严格的指称语义。这样,我们在监视多个信号源的时候就可以摆脱维护复杂的状态变量。

举个经典的例子,在面对gui的时候,我们需要同时监控鼠标和键盘的事件,只有鼠标位于某个范围内,键盘按下特定的按键,同时鼠标点击,才能出发技能,不过只是简单的注册事件回调的话,显然要依靠鼠标的回调函数中获取键盘的同时处于的状态(你可能还要反过来),这样在事件多的时候很容易混乱,而当Event变成first class并且是可组合的时候,只要诸如下面的代码

1
(pressKey zip mouse).filter(e => e._1.KeyEvent == Key && e._2.position in Windows).foreach(x => do some thing)

就可以表达出逻辑。

再到语法,我之前跟人家提起这个说法时,有人说过语法是语言规定的,你写个库也改变不了语法啊。其实我只能说,是也不是。我对语法的理解可大可小,小到Java常用的链式调用,大到宏、template haskellscala.meta,再大到编译器插件。有很多角度可以做这种事。很多时候,人们喜欢谈metaprograming或者设计DSL

Ruby喜欢谈duck typing,对于诸如RubyPython很多黑科技可以通过运行时修改内置数据结构来达到扩展语义的方法,比如通过修改__call____getattr__来实现动态捕获用户查询参数。再到Scalajs通过使用插件,以及宏,提供了直接在Scala代码中写Html的能力,在配上Intellij的补全,基本是无缝衔接。而且,我们经常发现,提供方便的dsl语法和实现库功能两者是正交的,但都基于对业务逻辑在类型系统上的建模。至于再进一步,使用free monad或者final tagless又是另一种方法了。

承认你自己无时无刻都在写库

这个完全是个人看法,只要你的代码有一丁点的可重用性,你就避免不了写一个库,而你在做的就是通过抽象和组合,尽可能把基本而繁琐的可能影响正常顺序逻辑的代码,隐藏在库当中,最后只剩下薄薄的一层,尽量没有副作用而且直观的代码,作为真正的业务逻辑。最后这一层的厚度应该是和库的厚度成正比的,库越厚,那你最后业务逻辑可以容忍的厚度也就越厚。

这个例子很多,也往往是Monad出现的机会,这里以future为例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
object ObjectRequestAsyncTest extends TestSuite {

@JsonCodec case class kancolle(name: String, kid: String)

val data = kancolle("Murasame", 81.toString)

val tests = this {
'ObjectCreateAndGet - {
for {
response <- ObjectCreateRequest("kancolle").run(data.asJson.noSpaces)
kan <- ObjectGetRequest("kancolle", response.objectId).get[kancolle]()
_ <- Future {
assert(kan.name == "Murasame")
}
_ <- {
val newData = kancolle("Murasameka", "81b")
ObjectUpdateRequest("kancolle", response.objectId)
.run(newData.asJson.noSpaces)
}
nkan <- ObjectGetRequest("kancolle", response.objectId).get[kancolle]("")
_ <- Future {
assert(nkan.name == "Murasameka")
}

} yield ()
}
}

这是我代码中粗糙的单测的一部分,所有的XXXRequest都是建立在REST API的通信商,所以,都可能有网络错误、服务器故障以及JSON返回错误,但是在这个工作流中,我们并没有不得不处理这些东西,但是我们并没有放弃处理这些异常,因为他们在最后返回的Future[Unit]中,中间一旦有一部分出现问题,接下来的部分其实并不会执行,你会得到你的第一错误,如果你直接用异常处理这些,将不可避免的让异常处理的代码打断你正常代码的逻辑。通过使用Future你可以获得异步的能力,同时在不抛弃特殊情况时不用分心去处理他们。而,业务逻辑最终这薄薄的一层就包裹在这个for里面。除此以外的都尽量是纯的,都是可以基于属性测试的。这种感觉在我用IOMonad中更为明显,就像多米诺骨牌,你做的只是推倒最后一张牌,剩下的都会绑定执行。所以,你一直都在写库,不过是给自己用。

It’s all about DataTypes

类型系统,越来越多的语言喜欢提这个词,我记得《Programming language pragmatics》这本书里面给出的定义应该是编程语言的基本类型以及类型的组合方式。

记得我刚开始接触SML的时候,看到它pair的类型写成int*int这种样子,想了一句,这还真是乘法类型。不同语言提供的基本类型大致相似,但是组合方式却不尽相同。我们先摆脱这些术语,先看看常见的一种方式,class。定义一个class其实就是定义一个新的类型,而它应该是由其他基本类型产生的,就是常见的成语变量,此外应该还有绑定到类型上的操作,一般常见的就是成员函数。平时的设计我们希望增强类的可扩展性,这里其实又涉及expression problem。以go为例子,go中的函数与类型的绑定是通过如

1
2
3
4
5
6
7
type Vertex struct {
X, Y float64
}

func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

这种语法绑定的,即为一个类型扩展操作并不需要打开这个类进行操作。其实了解C++实现也就知道成员函数并不在类里面,成员函数可以看成编译时传了一个this指针的函数,跟Pythonself同样。两种不同的扩展方式,在面临ep的时候,有不同的取舍。由此产生了各种库以及设计模式。

对于type class带来的方便的扩展能力,可以思考这个例子。对于一种数据结构,如果他本身满足二元操作上的结合律即op(a, op(b, c)) == op(op(a, b), c)始终成立,那么我们在一些遍历的情况下可以选择,

  • op(a, op(b,op(c, d)))
  • op(op(op(a, b), c), d)
  • op(op(a, b), op(c, d))

这三种组合方式,第三种因为可以并行化,所以更被我们所需要,对于已经存在的List类型,我们可以通过声明一个

1
2
3
4
def listMonoid[A] = new Monoid[List[A]] {
def op(a1: List[A], a2: List[A]) = a1 ++ a2
def zero = Nil
}

然后就可以获得

1
def foldMapV[A, B](v: List[A], m: Monoid[B])(f: A => B): B

然后把Monoid[List[A]]变成Monoid[Par[A]],从而自然而然的获得并行的能力。

使用高阶泛型类型以增强类型的表现力

谈论Haskelltypeclass给第一次接触的人解释的时候,总会说一句就像Javainterface除了一些地方不一样。这一些地方就有kindKind是比type更高阶一点的抽象。但是这个在Scala里面的支持不是很友好。索性,不妨碍使用诸如此类的概念建模。

为什么要尽可能的增强类型系统,从HaskellIdirs,从F#F*,为的就是一句话尽量让错误在编译时暴露。

静态类型语言的优势在于编译保证了很大一部分的程序的正确性,我相信很多人在写单测的时候会有一个直观的感受,想要达到一样的安心程度,动态类型语言的单测数目要比静态类型多几倍,并且不可避免使用诸如typeof或者isInstanceof这种语句来确定类型(当然我个人是不同意动态类型语言不适合写大项目的这个观点的)。所以,合理使用类型系统有利于提高代码的健壮性。其中一个例子就是effect system,比如STRef把变量的可变性限制在一个固定的作用域。这样你在其它地方改变它的时候就会报编译错误。

再或者,类型协变逆变的标示,用来保证你的泛型在继承体系上位置的正确性。还有之前写过的parser的例子,通过高阶类型来提供parsererror上的灵活性,诸如Parsers[ParseError, Parser[+_]]的这种类型,其实就是我们通过填入不同的类型,实现了不同parser的注入,而且,这个例子也能看出来协变逆变的重要性。

其实利用枚举来代替数字就是一个简单的利用类型来减少错误的例子,如果只用数字做参数,那么输入-1这种未期望的数字就是要到运行时才能检查,而通过声明枚举类型,就限制了能输入的数字的范围(在编译时),同时实际实现也只是使用数字,不没带来太大的运行时损失。进一步,用接口类型,代替函数指针等,其实这种思想和语言都无关。

FP or OO

两边我都没怎么写过正经代码,但是FP和OO在很多情况下是正交的东西,我见过最多的对这两者的误解就是

  • FP和OO是冲突的
  • FP就是纯函数
  • 不按OO的库没法用
  • FP效率低

FP和OO是不是冲突的,我相信ScalaOCaml以及F#这些语言已经证明了,很多情况下,两者并不冲突,之前面临的很多问题其实在于『常见』的OO在实现时,没有把对应的点变成first class,这个大多是限制于机器环境、效率等方面的问题。class不就是数据成员加上绑定的函数嘛。那种语言不是这么搞的?就因为长得不太一样,取舍不一样就『非我族类,必有异心』了。

FP就是纯函数,immutable,这个误解我真是见了不知道多少遍了,我第一次接触FP这个概念是erlang,当时我也感觉没有变量很神奇,其实那种actor模型,就是尽量用状态机,单次响应无状态,写起来自然对变量的需求小啊。但是这不是全部,为什么可以没有变量?因为函数式语言再设计库以及一些数据结构的时候,把常见的会耽误你正常思绪的那部分封装起来,让你看到不到了,你不用考虑出错,自然不用抛异常啊。纯函数、immutable只是个喜人的结果。

封装不按OO的样子没法用。恩,这种观点我也见过,这又是个典型认为自己见到的就是全世界的心理。你用OO库怎么写代码?难道真的是输入个.,然后等补全嘛?不看文档嘛?那些复杂的继承体系真的更省心嘛?如Haskell这种语言,确实不能像一般常见的OO语言那样看文档,有时候称为『面向函数类型编程』,为什么Hoogle会提供按照函数类型查询这种功能?因为你只要想我要一个什么样的类型函数,查一下,用了就好,因为没有副作用,所以一般不用担心那么多,要先用什么在用什么才不会出问题。最后,你看Scalaimplicit+typeclass不是也能搞出来像OO的语法嘛。

FP效率低,这个我持保留意见。你说吃内存,我认同,和c或者c++这种选手比是这样,不过剩下的就不好说了吧。大家都用gc,编程方式不同,所以gc的风格也不同,一般FP会有更多的闭包,甚至还有Haskell这种全都是lazy的,代码量不大的时候,感觉可能比较明显。不过,FP的数据结构,也不能这么说毕竟immutable的数据结构大家都在用了,本身在设计上就有很多的优化,就比如说ScalaVector其实是32分的前缀树,均摊的效率并不低,又照顾了headtail这种操作。

已经乱七八糟写了很多没用的东西了,也不是什么新东西,但是都是我在使用Scala时才开始认真注意到的东西。最后,给那些据守一种语言的人,一句话,很多时候,你感觉没有用只不过是因为你根本不懂而已,不去学习就连否定的资本都没有。就算不喜欢,不用,那种用无脑吹吹嘘的东西打无脑吹的脸的感觉不是。

哦,说好的年中Release 2.12呢。




X