Julia语言反直觉的循环迭代作用域

趁着Julia 1.0发布的东风,今天(2018年08月13日)玩了一把Julia。总体感觉像是一个融合了PythonMATLAB中数值计算相关的便利、支持(但不强求)静态类型、没有MATLAB的历史包袱,舍弃了Python中为通用程序设计提供的一些特点(从而在做计算时比Python更方便),专注于数值计算和技术分析的语言。从目前的性能体验来看,广告中说的“不需要向量化代码也能达到类似C的速度”并不是吹牛。

缺点是目前1.0版本还是太新。大约半年到一年前的文档/教程们出现了大面积的与1.0不兼容的情况。绘图库虽然有不少,但居然有将近半数编译不出来(内置包管理器下载的是源码,需要编译),让人不得的怀疑这个1.0版本是不是赶工期了。

前面说了,Julia语法给人的感觉像是了PythonMATLAB的融合,但毕竟还是有不同的。很尴尬的一点是,正是由于学习的太自然,遇到问题时反而让人更难跳出以前的经验。比如下面这个循环:

y = 30;
for i=1:10
    println(y + i)
end

乍一看几乎看不来是什么语言,运行也很正常。然后,我们经常需要在循环中迭代修改某数的值:

y = 30;
for i=1:10
    y = y + i
end

简单朴素,直截了当地……出错了……

ERROR: LoadError: UndefVarError: y not defined
Stacktrace:
 [1] top-level scope at /Users/metorm/dev/Quasi1DSRMCFD/test.jl:3 [inlined]
 [2] top-level scope at ./none:0
 [3] include_string(::Module, ::String, ::String) at ./loading.jl:1002
 [4] (::getfield(Atom, Symbol("##120#124")){String,String,Module})() at /Users/metorm/.julia/packages/Atom/jJn7Y/src/eval.jl:117
 [5] withpath(::getfield(Atom, Symbol("##120#124")){String,String,Module}, ::String) at /Users/metorm/.julia/packages/CodeTools/8CjYJ/src/utils.jl:30
 [6] withpath at /Users/metorm/.julia/packages/Atom/jJn7Y/src/eval.jl:46 [inlined]
 [7] #119 at /Users/metorm/.julia/packages/Atom/jJn7Y/src/eval.jl:114 [inlined]
 [8] hideprompt(::getfield(Atom, Symbol("##119#123")){String,String,Module}) at /Users/metorm/.julia/packages/Atom/jJn7Y/src/repl.jl:76
 [9] macro expansion at /Users/metorm/.julia/packages/Atom/jJn7Y/src/eval.jl:113 [inlined]
 [10] (::getfield(Atom, Symbol("##118#122")){Dict{String,Any}})() at ./task.jl:85
in expression starting at /Users/metorm/dev/Quasi1DSRMCFD/test.jl:2
错误信息太复杂,这是目前的又一个缺点

y怎么能是未定义呢?前面一个循环运行的好好的,说明循环中按规则是可以访问到y的。难道还有只读什么的问题?但是读写问题那也不该报未定义错误。

再测试:

y = 30;
for i=1:10
    print(i)
    y = y + i
end

依然报错,但我注意到了一个细节:报错总是在循环中试图迭代式修改y,且第一次访问y的出现在修改y之前时候出现。比如上面的代码,第一次print(i)是可以执行的,而下面这样写就不会报错:

y = 30;
for i=1:10
    y=i
    print(i)
    y = y + i
end

翻文档。这么普遍的用法,文档里面居然没有专门说明。不过我还是找到了一句貌似相关的解释:

For 循环及 Comprehensions 有特殊的行为:在其中声明的新变量,都会在每次循环中重新声明。

这样解释起来,那就是我所以为的迭代式修改,并不是在修改y——按Julia的逻辑,y只是某个值的一个别名——而使用重新声明了一个名叫y的变量,在循环作用域内覆盖掉了外面的y的定义。由于变量的作用域是按块来的,这么一声明,造成了整个循环体中的y指代的都是这个新变量(恰好与外面的y重名而已,实际上完全无关)。这时候我再用那个迭代式赋值的话,就相当于让一个未定义的变量给自己赋值,自然会出错。

晕了的话,去上面再看看代码。

最后一版程序中,循环中同样新声明了一个y,但它的初始化是正常地用i进行的,所以不会出错。运行结果也证明,里面的y与外面的y完全没有关系:

y = 30;
for i=1:10
    y=i
    print(i)
    y = y + i
end
 
---
 
1
2
3
4
5
6
7
8
9
10
30

当然,最后一个写法,仅仅是没出错而已,根本没有达到迭代更新的目的。如果要实现本来预想的流程,可以用global语句:声明global x使得x引入当前作用域和更内层的作用域。

y = 30
for i=1:10
    global y;
    println(y)
    y = y + i
end
 
---
 
30
31
33
36
40
45
51
58
66
75

此外可以看出,Julia虽然看上去是一个脚本语言,是实际上并不是像MATLAB一样老老实实一行一行解析的,而是首先经过了一次从头到尾的预处理——否则运行到第一次读取y的时候,怎么就知道后面我又声明了一个y呢?

  • 最后更改: 2019/05/27 13:40