关于理论和实践的一些碎碎念

(似乎是blog里面最长的一篇文章,不过也算是好久没有这么畅快地把想说的话说出来了)

更新:一位朋友(DC)对这篇文章的回应

总是看到关于培训班对比科班生,选择Java还是算法的讨论。除此之外,无论是在中文网络还是法国网络,时至今日,感觉一个新手(中学生或者想要学编程的人)提问探询怎么入门学习编程,下面总能看到鼓吹Java、Spring、Angular的言论。

很想吐槽一句话:

一见编程,立刻想到Java,立刻想到Spring,立刻想到Java EE,立刻想到MyBatis, 立刻想到MVC,立刻想到面试八股文,立刻想到低代码SaaS平台。Javaer的想象惟在这一层能够如此跃进。 --鲁迅(不是)

但是CS(计算机科学)是不是真的就只有Java(或者它的远房亲戚JavaScript,或者PHP,之类的)这条路呢?

并不是吧,倒不如说,这些都只是计算机世界里面数不尽的研究领域的应用而已。而哪怕是应用层面,也不是只有Java吧。哪怕,退一步说,就在Java的宇宙里,我想,可以讨论的东西,也远远不止Spring和CRUD。

大模型、高铁、核电站、无人机、操作系统、3A游戏,恐怕都不是构建在Java的基础上的。而Java也可以做很多事情,比如Android,或者分布式大数据的工具。又或者Minecraft或者IntelliJ IDEA。真要说的话,Spring,CRUD或者低代码SaaS,恐怕只是这庞大的生态圈中的一小部分。

也许鼓吹Spring等框架的人群会说,框架是学习IT技能的捷径,很多东西都只能在实践中学会。最后一句话也许不假,但是一直看各社区的讨论下来的感觉,反而是框架更多限制了程序员的学习思路了呢。其实前面那句吐槽就很好的体现了这一点。

之前也跟人就「学习编程语言」这件事情吵过。这里先不提编程语言也可以指代PLT而不是「学习某门编程语言」,哪怕是学习某门具体的编程语言,例如Java,把语言吃透了也是很重要的。这里说的吃透并不是说像某些人形字典一样把API全背下来或者对八股文滚瓜烂熟,而更多是明白语言设计的意图和思路,知道标准库的方法和类型的作用,明白一些基本概念,例如线程、线程池、Lambda之于Java。换言之,就是「核心」这个部分,例如Java的话就是Core Java[1]。这里的Core,自然是和「Enterprise」也就是框架相对应的,理论知识。

Enterprise Java,或者说,框架,也许看起来像是一条诱惑人的捷径,通向梦想中的IT职场。但是,也经常遇到企业级程序员的一些让人感到匪夷所思的想法,给人的感觉就是基本功都不扎实。

一个例子就是那些半吊子的「Java函数式编程」或者「多线程」教程里面,对着一个Optional或者Future直接进行get(),甚至在没有if包裹的前提下,的迷惑行为。毕竟,哪怕是没学过Monad的人,只要明白了null和异步的基本概念,都知道这种get()是多么不安全,而用if环绕的get(),又和直接判断一个变量是否null有什么区别呢?

这个「业务代码中,别用多线程」就是另一个缺乏理论知识的典例了。说到底就是背压的问题,是并发模型里面典型的设计问题,却叫人不要用多线程,确实有种Enterprise Java的美感呢。

另一个例子,是这位「代码写的比较好所以做了CTO」的仁兄,的「99%的程序都是字符串变来变去」和「编程语言就是一个人机交互界面,没有那么重要,门槛也低」暴论,还有拿「开发一个blog程序」来证明GPT可以替代程序员的预言。

其实也能理解这些想法的出发点,毕竟没有理论知识累积和指导,框架开发能带来的经验,更多是相似的任务的重复叠加,基于这些任务而总结出来的经验也更多是杂乱而缺乏条理和系统归纳的。

理论知识,例如Core Java,的第一个作用,就是提供编码和设计的依据,让人明白为什么这段代码要这么写,而不能那么写。

前几天讨论YAGNI的时候,我举了关于线程、协程的例子,大概就算是一个很好的例子。尤其是对于Java这样复杂的语言。从框架和实践的角度讲的话,也许程序员会接触一系列的类型,例如FutureCompletableFutureSchedulerExecutorServiceReentrantLockConcurrentLinkedDeque,再比如synchronized关键词,但是对它们之间的内在联系总是缺乏感知的。反过来说,如果一开始学的是Core Java,或者更近一步说,计算机科学的基础知识,那么就会明白,线程对于并行和并发的作用,也知道线程是昂贵的,所以人们发明了线程池来反复回收利用线程。那么,该用什么来调度池子里面的线程呢?再进一步的,会明白多个线程访问同一个变量,会出现竞态的问题。这时候,「可不可以在同一个时空段只允许一个读写访问呢?」就会成为一个很自然的直觉,然后就会意识到锁的概念。但是再进一步,锁也有自己的问题,这其中一个典型的麻烦就是死锁[2]。这时候,也许你就会开始思考该怎么应对这些问题了。

不过,我们的从Spring开始学到Java EE而对理论知识闭口不谈的同僚们,在这时候会怎么做呢?

也许会把字符串锁了

另一个很经典的例子就是这个函数式null

我想,到了这里,从Core Java一路啃下来的你,应该对大名鼎鼎的J.U.C.(说实话我不喜欢这个缩写)里面的这些类型,都有种或多或少的直觉,因为它们实现的,正是你在课堂和教科书上学到的计算机概念的经典实现。到这里,你需要的只是在实践和项目中熟悉这些具体类型的API和它们的用法,但你的锁字符串的同事们能做到的事情,你都能做到。

那么让我们来看看,理论知识可以给你带来的,而Enterprise Java做不到的事情。

现在你明白了并行和并发的概念,毕竟,这也是所有大学本科的必修课的内容。你会发现,当你锁住一个变量的时候,其他进程在做空循环。自然,你会好奇这些空循环是不是就是在浪费CPU时间,有没有办法把它们去掉。就这样,你接触了异步[3],你发现之前C语言课上讲的看起来很鸡肋的函数指针,或者你一直搞不懂的Java的Lambda,在这里派上用场了。你也会明白,异步的时候计算任务会被指派到下层的线程上,所以,永远不要阻塞[4]……可是,锁本身不也是一种阻塞吗?

到这里,也许要先转换一下思路,于是你明白了「任务」(或者Promise/Future/Deferred)的原语[5],你明白了锁和过程式的Java代码是不可组合的,所以首先要把它们转化为「一个在未来返回结果的计算」,然后把它们组合起来,来表达并发的语义。也许你甚至会知道Future还有个叫Continuation[6]的亲戚,不过我们还是先不要管它了。

就这样,你来到了无锁并发的世界,发现这世界上除了锁,还有CAS(Compare and swap)、STM(Software transactional memory)、Actor Model[7]。也许这些新玩意对你来说还很陌生,尤其是那个看起来就跟Monad一样都是从令人生畏的数学世界里莫名跑出来的一样。但是,走到这里,你应该也会很快就能明白Actor Model可以在Java里面映射到一系列具体类型和算法的排列组合上,包括消息队列和线程池。

也许你也意识到了,回调式的写法并不是最舒服最适合程序员的,也许你会发现有一种叫做协程[8]的东西,也许你不知道它就跟世界上的第一门编程语言一样古老,但是当你写了上千行回调后,发现在JavaScript或C#里,你的回调可以用async/await[9]来组合在一起的时候,你会觉得如释重负吗,还是会后悔没早了解这个概念呢?也许,你学习Kotlin的时间就跟学习Java的时间一样长,但是到现在一直没懂suspend fun[10]到底该怎么玩。大概现在的你,对Kotlin和这些Coroutine的实现,也有了更多期待了吧。

从进程到线程,到协程(或者Fiber/绿色线程),你会发现,每一层的抽象,都是在细化并发控制的粒度,提高组合性[11]。你手上可以用于抽象和建模的工具箱更多了,另一方面,你也明白怎样可以把,比如说一个Java EE项目写好,而不是满足于能跑就行。毕竟,哪怕是Java 11,也能够写出现代的软件架构,虽然可能过程不一定让人愉快。到了这里,你也许会对编程语言里面的「组合性」,也有了更明晰的了解了。

看到这里,也许曾经也期待Project Loom[12]的你,会更现实的看待这个Java的新功能,而不是像营销号一样满大街尬吹Loom多么惊天地泣鬼神吧。因为,Loom并没有给Java的并发带来组合性,至少目前为止。嗯,作为一个Core Java的追随者,了解Java的最新发展,知道Lambda、Amber、Loom、Valhalla是什么,也算是基本技能吧。当然,也许你也会卷入「Java泛型[13]和C#泛型哪个才是真的」的大讨论,不过我想,作为Core Javaer的你,比起无脑复读八股文的Enterprise Javaer,应该是能够冷眼旁观这些喧嚣,明白泛型背后的Parametric Polymorphism对一门OOP语言到底意味着什么的。

也许曾经你对C#和.NET(WinForm、ASP.NET MVC)不屑一顾,也许你也会为C#和.NET(C#9、WPF、Avalonia、跨平台)感到兴奋。但现在的你,也许也明白C#和Java各有长处,而具体使用哪个还是因地制宜的事情[14]了吧。

当然,也许你正在为学校的编译作业而烦恼,在发愁怎么把一行if语句或者for循环展开为虚拟机的机器码或者生成LLVM所要的IR。不过,顺着上面的这些走一圈,你又会不会有想要在你的学校大作业里顺便实现自己的协程的想法呢?或者,也许你会好奇,泛型和类型又是怎么实现的呢?当然这就跑得有点远了。

说了这么多,不知道能不能讲清楚理论知识的重要性呢。

再比如说我之前也喜欢说的「复杂度只能被转移,而不能被消除」,实际上这也是计算复杂度的一个比较直球的应用了。虽然说这里并没有很直观的推论就是了。但是,虽然作为程序员,明白软件设计的意图不是消除而是对复杂性进行管理[15],知道抽象、封装、重构这些工具,并不能让一个项目的复杂度下降,总还是一个很重要的事情。

Java的JVM也提供了一个很经典的例子,因为JVM实现的JIT机制,大型的Java程序启动后都有一个暖机过程,在跑了一段时间后性能才会逐渐上升。说到底,这也和Java里面缺乏较为底层的控制手段(例如struct,或者inline fun)有关。Java通过逃逸分析[16]来替代程序员自动判断变量什么时候分配在栈上,什么时候在堆上。好的程序员会致力于让自己的代码更容易让JVM推断出来,或者利用linter来辅助自己判断,而不是随便乱newclone

当然,框架程序员也许会说,他的变量都是框架注入的。这种时候我只能说,你喜欢就好。不过,哪怕是使用框架,Core Java程序员总是可以进一步挖掘框架的实现细节的。你会发现,刚才说的这么多东西,都是Spring、Angular之类的框架不会直接告诉你的,哪怕框架的底层实现也依赖于这些知识。显然的,培训班和Enterprise Javaer,也不会告诉你计算机世界里面还有这些东西,更不会告诉你,除了够用就好,还可以在计算机里走的更远。

说到这里,前面提到的关于「字符串变来变去」、「人机交互界面」和「GPT替代程序员」的暴论该怎么去反驳,我想,到这里,也不言而喻了。

编程和代码,绝对不仅仅是「把字符串变来变去」。如何设计一个系统,对一个复杂项目进行开发和部署,是一个非常复杂的课题,不是「人机交互界面」可以概括的。

而GPT替代程序员更是天方夜谭。

你会发现,国产操作系统、信创、国产编程语言、低代码,这些东西到底有多可笑。

你也会发现设计模式里面哪些是精华,哪些是糟粕(比如Null Object模式)。

这也是我为什么之前要反驳YAGNI的原因了。

理论知识的另一个作用就是,提供一种可以把不同的经验和积累贯通起来的可能性。

如果你有幸(或者不幸)在函数式编程的世界走得太远,那么你还会发现Monad[17](好像在哪里见过……),而你会发现,前面提到的Future就是Monad的一个典型的例子。另一个更典型的例子,是Optional(或者Maybe)。Enterprise Javaer们照搬设计模式后,会发明一个叫做Null Object的东西。现在你会发现,这个东西简直就像笑话一样。

当然,在这之前你可能需要折腾很长一段时间,并且受到例如Haskell,OCaml,Scala甚至Coq等奇奇怪怪的语言的打击。但是,你会发现这一切都是值得的。

说来,最近也学习了一下Rust。其实也不是第一次学Rust了。不过总的来说,还是一口气把Rustlings啃完了。

大概也是因为在这之前,这几年里,先后学了Swift,Haskell,OCaml还有C++的std::move[18]的原因。学了Swift后,if let?的语法看起来就很自然了。学了Haskell,明白了Monad的概念后,再回过头看Swift的optional,也是顺理成章的事情。甚至也会好奇,Rust和Swift的let是不是跟OCaml学的。

而接触了C++的智能指针和std::move后,虽然对左右值的概念还是不太清晰,但是至少明白Rust语言想要解决的问题是什么,也更好理解Rust引入的生命周期和所有权是怎么工作的了。不过,只会把C++当成C with class的人,或者只会写Enterprise Java的人,大概会觉得无所谓吧。

另一方面,哪怕左右值没那么好理解,但是至少Rust的编译器可以绝大部分时候给出足够的信息,给编码提供帮助。虽说这看起来很trivial,但是也算是把各种知识和经验累积起来的结果。

毕竟,我也不是什么聪明的人,倒不如说我自己在程序员里面算是比较愚笨的了。在转学计算机之前,我也自学过Python。总共是五次吧,但没有一次学下来的。

甚至于Scala,Rust也是,在反复的学习-放弃之间重复。连我最喜欢的Kotlin也是,也不止一次自学失败呢。

所以才觉得理论知识很重要。另一方面,我自己每次的学习都是从Core Language开始,而不是急着去写一个TODO MVC或者某GPT API之类的程序。也许正因为如此,所以每次学习都有新的体会吧。

经年累月积累下来,也是不少收获呢。

其实说到底,理论知识的重要性,也就是科班生之于培训班的优势了。Core Java或者理论知识,固然可以完全自学(而且我认识的朋友也不止一个通过自学啃下来的),但是计算机本科研究生课程,从大学数学开始,再依次展开讲解算法、编译原理、操作系统、网络,明显比只教框架的培训班,更能让学生完成基础和理论知识的积累。反过来说,急于求成而选择培训班的人,又有多少能耐得下寂寞从VB.NET,PHP,Go,又或者WinUI开始啃计算机呢。

我觉得,这大概就是为什么计算机科学叫做Computer Science,而不是Information Technology的原因。

毕竟,也许培训班能让人衣食无忧,但总有人想要探索更遥远的星空。

  1. https://docs.oracle.com/javase/tutorial/ ↩︎
  2. https://zh.wikipedia.org/zh-cn/哲学家就餐问题 ↩︎
  3. https://stackoverflow.com/questions/748175/asynchronous-vs-synchronous-execution-what-is-the-difference ↩︎
  4. https://vertx.io/docs/vertx-core/java/#_dont_block_me ↩︎
  5. Futures and promises ↩︎
  6. https://www.zhihu.com/question/61222322 ↩︎
  7. https://doc.akka.io/docs/akka/current/typed/guide/actors-intro.html ↩︎
  8. Coroutines ↩︎
  9. https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/ ↩︎
  10. https://kotlinlang.org/docs/coroutines-basics.html#scope-builder ↩︎
  11. https://www.zhihu.com/question/34819931/answer/482024102 ↩︎
  12. https://wiki.openjdk.org/display/loom ↩︎
  13. https://www.zhihu.com/question/28665443/answer/1873474818 ↩︎
  14. https://www.zhihu.com/question/569023091/answer/2778472671 ↩︎
  15. https://ferd.ca/complexity-has-to-live-somewhere.html ↩︎
  16. https://zhuanlan.zhihu.com/p/588932823 ↩︎
  17. https://www.adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html ↩︎
  18. https://stackoverflow.com/questions/3106110/what-is-move-semantics ↩︎

关于理论和实践的一些碎碎念
http://inori.moe/2023/11/19/some-ideas-about-theories-and-practises/
作者
inori
发布于
2023年11月19日
许可协议