读Julia文档小记

很久之前就在关注Julia的发展,待到1.0版本决定入坑,现在终于有时间刷一刷文档
对于编程语言来说,后发者总是试图对老一辈的设计有全面深刻的突破,但这些突破有时不得要领,且老一辈依靠沉淀下来的代码也不会被轻易击倒。

读Julia的文档确实让人有很多惊喜。即使不用Julia,通过读文档了解现在正在用的编程语言(其实主要是Python)的不足也可以加深对编程语言的理解。

文档阅读

变量

  • 有限制的内置函数Monkey patch。这个到底有没有用说不好,但确实避免了使用一些特别灵活的动态语言可能犯的低级错误。

整数和浮点数

  • 整数和浮点数的数据类型仍然显示了底层的数据类型,当然BigInt也通过包装GMP得到头等(First Class)支持。初看这导致了一些不很一致的行为。如0xFFFF_FFFF_FFFF_FFFF * 2得到的是无符号64位整数的结果0xfffffffffffffffe,而0xFFFF_FFFF_FFFF_FFFF_FFFF * 2得到的是BigInt的结果0x000000000001fffffffffffffffffffe。但实际上类似的情况对于从Int32到Int64也是一样的,所以无伤大雅。
  • 因为暴露了底层数据类型,浮点数的Machine epsilon得到了头等支持,只需eps(1.0)即可得到相应精度。这对于数值计算来说是非常贴心的。你甚至可以用nextfloat这样的函数,有点6。
  • 头等的任意精度浮点数支持。以前我都不知道这是可能的。这是通过允许用户设置一个允许的浮点数误差实现的。这个feature有点酷。
  • 杰出的数学表达式支持。同样是抛弃编程语言的惯例,2x2^x这样的表达式可以用来直接表达其数学含义了。甚至3(4+5)也可以直接得到27了!当然,如此设计带来了一些语法上的冲突,如括号的使用不允许与函数调用混淆,但总的来说是不错的尝试。

数学运算和基本函数

  • 头等的向量化运算支持。只需在算符前加上.即可使操作向量化。

复数和有理数

  • 头等的有理数支持。这个不算一个新特性,但是3/9返回0.3333而3//9返回1//3对于数值计算来说是一个一致性非常好的设计,体现出了对整数相除得整数的彻底反叛。看得出来Julia的设计者试图比Python和Matlab更进一步,将RELP做的更像一个通用计算器。

函数

  • 对函数式编程更友好的语法支持。当使用函数作为参数时可以使用Do语句块更清晰地编写匿名函数。粗看起来可能可以达到比Python的列表推导式更好的效果,同时还有替代上下文管理器的野心。
  • 对于基于位置的参数和基于关键字的参数进行了更严格的划分和定义。基于位置的参数可以有默认值,但不能通过关键字的方法传参。这一点有些反直觉
  • 函数组合与管道。一方面在多重函数嵌套时更清晰,不会被几个连在一起的右括号搞混,另一方面可能减少堆栈操作进行一些极限优化。不过适用场景可能比较有限,像map(first ∘ reverse ∘ uppercase, split("you can compose functions like this"))这样的例子实在太少,平时调用函数哪个不会带两三个参数呢?
  • 疯狂的向量化。Julia对向量化的支持可以说是丧心病狂,体现出了数值计算对向量化深深的执念。

控制流

  • C/C++中的三元运算符死灰复燃,更可恨的是Julia竟然把这样的例子放进了官方教程:

    1
    2
    3
    4
    5
    function fact(n::Int)
    n >= 0 || error("n must be non-negative")
    n == 0 && return 1
    n * fact(n-1)
    end

    竟然还恬不知耻地声称:

    This behavior is frequently used in Julia to form an alternative to very short if statements

    真是醉了。什么语言没有短路求值?又有哪种语言提倡这种写法了?究其原因,可能是因为Julia中条件判断需要一个if一个end至少三行,读起来也是非常难读,对于Julia新手可能是用||&&难看一些,但是对于熟悉||&&的老手还是三行的条件判断比较烦人。根本原因上来说这是Julia语法设计的失误,想要降低从Matlab来的同学的门槛,但实际上将门槛大大加高了。

变量作用域

  • 变量作用域比较类似Python,虽然做了些改进,比如循环变量是local的了,但总体上还是比较混乱,需要global之类的关键字。

类型

  • 类(struct)默认是不可变的,当需要可变的数据结构时需要声明mutable struct。对编译器友好/可读性强与灵活性永远是一对矛盾。不可变类是一个大胆的偏向前者的尝试。
  • 弱化的类系统。类的方法包括构造函数由通过定义函数进行定义。尽管有接口(interface),但只能从抽象类中继承。
  • 从C++中搞来了完整的模板系统,并有所增强。

元编程

  • Lisp和Nim中强大的元编程也被引入了Julia。元编程是(过分)强大的功能,常常被视为双刃剑。Julia在这一点上比较激进,在模板系统之外又有着非常完全的元编程系统,并可以想象借此造出了许多语法糖。我不是特别欣赏元编程,不知道这是不是当今语言的发展大势。

多维数组

  • 头等支持的多维数组可以存放任意对象。从这一点上来说是C/C++的风格。数组的操作语法主要是受Matlab影响,和NumPy区别较大。这很可能是由Julia的类型系统决定的。如多维数组没有shape字段,但是可以通过size函数获取。

缺失值

  • 头等的缺失值支持,显然是受了当今统计火热的影响。暂时没看出和nan有什么本质区别。不知道有没有什么特殊实现,用在内置的array中可以比较高效解决整数型的缺失值问题。

总结

  • 本来我期望这是一门语法简单的语言,结果大跌眼镜。Julia有着各种各样奇怪的关键字和语法糖,复杂的类型系统和变量作用域。学完Julia其实相当于Lisp、C++和Python都学了。Julia其实是设计给有相当编程基础的用户的,就这一点来说可以说是一个非常失败的设计。可以预见在未来Julia的推广仍然会遇到相当高的门槛。

试用体会

  • 简单试了一下,发现Julia是真的慢,特别是安装包以及运行文件。运行文件主要是必须要编译才能运行,这和非常强调interactive的数值计算不符合。Julia貌似希望能够在一个交互式环境里完成编码过程,省去一部分编译(如在Notebook和Juno中)。不得不说这种想法太偏科研和数值计算路数了,对于大型项目来说几乎是不现实的。
  • 任意一门新语言想战胜老语言,必须要克服老语言的两大法宝,一是沉淀代码,二也是不太容易想到的是强大的IDE和debug环境。做好一个IDE说起来容易但是工程上其实是非常复杂的。Julia现在在这一点上非常弱,在有转机之前不认为Julia可以用于大型项目。