一个简单的Julia教程

2016-11-02

当前版本 v0.5

因为在知乎和这个blog上写的量子计算札记会涉及到使用Julia语言的数值模拟,同时随着中国的Julian越来越多,而之前几个在JuliaCN活跃的老司机(额也包括我)最近一直没时间翻译文档,然后最近好像也没声明人给贡献翻译OTZ。所以呢,先写一个简单的教程给大家用。有很多地方参考了英文的标准文档。感兴趣的同学可以直接戳英文文档,也欢迎大家给中文文档贡献翻译。

Hello World!

使用Julia的解释器

我们可以新建一个脚本文件hello.jl,在里面写

1
print("hello world\n")

我们先不要管这行命令具体是怎么回事,你知道它是用来打印Hello World的就可以啦。然后在命令行里用Julia的解释器执行它

1
2
$ julia hello.jl
hello world

使用REPL环境

或者让我们打开Julia的REPL

  • Linux/Mac:在命令行里输入julia
  • Windows:双击Julia的可执行文件或者快捷方式

然后你就进入到了一个交互式的窗口中,输入这个打印Hello World的命令

1
2
julia> print("hello world!\n")
hello world!

那么这个print函数是干嘛的呢?我们可以在REPL中输入?+print+enter来获得它的文档

1
2
3
4
5
6
7
8
julia>
help?>print
search: print println print_joined print_escaped print_shortest print_unescaped
print(x)
Write (to the default output stream) a canonical (un-decorated) text
representation of a value if there is one, otherwise call show. The
representation used by print includes minimal formatting and tries to avoid
Julia-specific details.

那么我们就知道了print是说打印输入的变量的意思

-e选项执行代码

安装好Julia之后,按Ctrl+Shift+T打开命令行(Windows用户请按Ctrl+X,点击运行输入cmd,打开命令行),可以直接运行Julia命令。

1
2
$ julia -e 'print("hello world!\n")'
hello world!

这是Julia的第一种执行命令的方式,-e 选项后面用'括起你要执行的Julia代码,但注意因为回车会让命令行执行这行命令所以你必须把所有的代码写成一行。一般我们只用这种方式执行一些很短的,很简单的代码。

变量

让我们打开一个REPL,在里面测试这一小节的内容。首先,Julia的变量不需要特别的声明,变量类型会由你所给它的值自动确定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 将10赋值给变量x
julia> x=10
10
# 对变量x进行运算
julia> x+1
11
# 重新给x赋值
julia> x= 1+1
2
# 但是请不要用数字作为变量名的开头,因为Julia中默认数字开头代表了乘法
julia> 2x
4
# 也可以给x赋其它类型的值
julia> x = "hello world!"
hello world

Julia的变量名称支持大部分的Unicode(有极少数的不能支持,但我们不需要管),所以你可以这样

1
2
3
4
julia> δ = 0.00001
1e-5
julia> 你好 = "hello"
hello

这些数学符号可以使用LaTeX语法输入,中文的话用你自己的输入法就可以啦,Julia支持了包括UTF-8在内的多种编码。在REPL中可以这样输入一个希腊字母\delta+Tab = δ, 而在编程的时候我们当然不能用命令行,所以有这样几个编辑器支持这种补全(如果你发现还有别的好用的编辑器不妨在评论里告诉我):

  • Atom+Juno 目前最好用的Julia IDE
  • sublime+SublimeLinter-contrib-julialint

Julia有一些自带的常量,这些常量是允许重载的,但是一般不建议重载它们。比如

1
2
3
4
5
6
7
julia> pi
π = 3.1415926535897...
julia> pi = 3
WARNING: imported binding for pi overwritten in module Main
3
julia> π
3

整数和浮点数

整数从8位整数支持到128位整数,而浮点数从16位支持到64位浮点数,如果有高精度计算需求还可以使用内建的高精度浮点数BigFloat(256位),整数Int默认跟随系统位数,比如在32位系统上为32位整数类型Int32,但64位系统就是Int64

Tips:

  • 浮点数的精度可以用函数eps求出,比如eps(Float32)
  • 比较长的数字可以用_来分割,比如100_1011000_1010,相当于计数法中的逗号:100,101 1000,1010

复数和分数

在Julia中,虚数单位\(i\)im来表示,Julia为虚数对象定义了一些基本的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
julia> 1+2im
1 + 2im
# 实部
julia> real(1+2im)
1
# 虚部
julia> imag(1+2im)
2
# 共轭
julia> conj(1+2im)
1 - 2im
# 取模
julia> abs(1+2im)
2.23606797749979
julia> norm(1+2im)
2.23606797749979
# 求幅角
julia> angle(1+2im)
1.1071487177940904

运算符

Julia中定义了一些常用的运算符,它们有

数学算符和基本函数

表达式 名称 描述
+x 单元加 单位算符
-x 单元减 将值映射为加法逆元
x+y 二元加法 加法运算
x-y 二元减法 减法运算
x*y 乘法 乘法运算
x/y 除法 除法运算
x\y 除法(颠倒的) y/x相同
x^y 幂运算 xy
x%y 取余 rem(x,y)相同

表达式 名称
== 等于
!= 不等于
< 小于
<= 小于等于
> 大于
>= 大于等于

Tips:

  • Julia中可以使用更加自然的不等式表达方式,比如1<x<2,而不用写成x>1 and x<2(python) 或者 (x>1)&&(x<2)(类C)

语句

Julia的语句可以默认以一行结尾,比如

1
2
x+1
y+2

这里x+1y+1就是分割开的两条语句,但是我们也可以用;分割语句,比如

1
2
x+1;y+1
z+2

Julia的代码块都以end关键字结尾,类似于Pascal,相当于C/C++中的{}声明代码块的方式。也相当于Python中使用空格对齐来分割代码块。

类型系统

Julia的类型系统是很有特点的,也常常被认为是Julia的速度快的原因之一。Julia的类型系统不支持类似于Python/Cpp那样以对象的成员函数和继承构成的OOP编程范式。Julia中面向对象的编程使用了Multiple Dispatch这一特性,而禁止了类型(对象)中使用成员函数。这种特性继承自Lisp,称为OO,而多重派发是实现OO的一个重要手段。

具体关于OO技术的好处可以参看这篇博客:浅谈OO编程中必不可少的手段:Multimethod,关于类似于Cpp这种面向对象编程的弊端可以参见这个知乎问题:面向对象编程的弊端是什么

Julia中总共提供了四种类型,它们的关键字分别是:typeimmutable,abstractbittype,这里我们暂时只介绍前三种(最后)

复合类型(对象)

在Julia中使用type关键字声明复合类型,以end关键字结尾。在typeend之间声明此类型所包含的变量

1
2
3
4
type Animal
weight
sex
end

当然也可以用分号来分割语句

1
2
3
type Animal
weight;sex
end

当然你也可以什么都不写,但这种时候建议你最好考虑一下后面介绍到的抽象类型。

1
2
type Animal
end

type声明的类型在没有说明的时候默认以指针赋值,或者说默认使用引用的方式来赋值。除非使用copy函数显式说明是深度拷贝。

复合类型中的元素可以通过.运算符来访问

1
2
3
4
julia> alpaca = Animal("肥的","母的")
Animal("肥的","母的")
julia> alpaca.sex
"母的"

指定类型和限定类型

Julia是一个强类型语言,我们可以用::符号来声明(或者标注)变量类型,比如

1
2
3
4
5
julia> (1+2)::AbstractFloat
ERROR: TypeError: typeassert: expected AbstractFloat, got Int64
julia> (1+2)::Int64
3

在复合类型中

1
2
3
4
type Animal
weight::Float64
sex::Int
end

指定类型往往能够让Julia程序的运行效率更高。

抽象类型

抽象类型使用abstract关键字声明,声明抽象类型一般有什么用呢?声明抽象类型可以很好的帮助你控制接口,具体的例子我们在后面的多重派发介绍。

1
abstract AbstractAnimal

抽象类型可以作为其它类型的父类型出现,比如

1
abstract Monkey <: AbstractAnimal

这里<:运算符代表:左边是右边的子类型,它可以在声明的时候使用,也可以用来做类型判断

1
2
julia> Monkey <: AbstractAnimal
true

复合类型也可以作为抽象类型的子类型,但不能作为复合类型的子类型。

1
2
3
type Donkey <: AbstractAnimal
alive::Bool
end

Tips:

  • 那么所有Julia类型的父类型是什么呢?是Any

不可变类型

1
2
3
4
immutable Complex
real::Float64
imag::Float64
end

不可变类型是一种类似于type的类型,但是相对type有一些不同:

  • 有些时候性能更好这个具体是说当变量所占内存较少的时候,比如像上面的Complex,相对type声明的类型能够更有效的打包进数组中,在一些时候编译器能够避免为整个immutable类型分配内存。
  • immutable中的变量是不能够被改变的
  • 使用immutable的代码更容易理解

注意,不可变类型中的元素如果是可变类型的,比如数组,那么这个元素依然是可变的

1
2
3
immutable Animals
list_of_animals::Array
end

这里list_of_animals是可以改变的,但是Animals不能够被改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
julia> animals = Animals([1,2,3])
Animals([1,2,3])
julia> another_list = [2,3,4]
3-element Array{Int64,1}:
2
3
4
# Animals的元素不能改变
julia> animals.list_of_animals = another_list
ERROR: type Animals is immutable
# 但是Animals元素的元素可以改变
julia> animals.list_of_animals[2] = 100
100
julia> animals.list_of_animals
3-element Array{Int64,1}:
1
100
3

产生类型的实例

复合类型type和不可变类型immutable都可以用如下的方法产生实例

1
2
3
4
5
6
7
8
9
10
11
12
13
type foo1
a
b
end
foo1(1,2)
immutable foo2
a
b
end
foo2(2,3)

参数输入的顺序是每个类型的元素在类型中的顺序

参数类型

Julia的类型也有类似于C++的模板(template)的功能,比如假如现在有Monkey,Donkey,Pig这样三个类型,都是AbstractAnimal的子类型。但是我想有一个Pet(宠物)类型,我可以养猴子,也能养驴,还能养猪当宠物,所以不能给Pet的成员指定某个类型,但我又不希望它被用来装别的类型。所以这个时候就需要参数类型。

1
2
3
4
5
6
7
8
type Monkey<:AbstractAnimal
end
type Donkey<:AbstractAnimal
end
type Pig<:AbstractAnimal
end
1
2
3
type Pet{T<:AbstractAnimal}
p::T
end

这样在生成这个类型的实例的时候,编译器就会生成相对应的类型,比如

1
2
julia> Pet{Monkey}(Monkey())
Pet{Monkey}(Monkey())

注意参数类型在产生实例的时候需要指定参数,编译器不会自己判断,当然我们有办法让编译器自己判断,这个在后面介绍。

Tips

除了类型意外,参数类型的参数还可以是别的东西,比如一个数字

1
2
3
4
5
type foo{N}
end
julia> foo{2}()
foo{2}()

函数

在Julia中函数使用function关键字来声明

1
2
3
function add(a,b)
return a+b
end

返回值由return关键字指定,或者由最后一个变量指定

1
2
3
4
5
6
function foo()
"foo"
end
julia> foo()
"foo"

实际上对于比较短的函数,你还可以这样声明

1
2
3
4
f(x) = x^2
julia> f(2)
4

构造函数

构造函数是用于定义如何构造对应类型的方法的函数。类似于其它面向对象语言,Julia的复合类型也有构造函数,并且是唯一可以在类型内声明的函数。构造函数默认是下面这样的,也就是需要按照内部元素的顺序来构造一个新的foo对象。

1
2
3
4
5
6
type foo
a::Int
b::Float64
end
f = foo(1,1.0)

实际上它相当于

1
2
3
4
5
6
type foo
a::Int
b::Float64
foo(a::Int, b::Float64) = new(a, b)
end

Julia的构造函数分为两种,一种是类型内的构造函数,一种是类型外的构造函数。类型外的构造函数实际上都是调用默认的构造函数创建对象,与一般的函数类似。而类型内的构造函数略有不同,类型内的构造函数可以访问new函数来为新对象的元素绑定值。new是一个用来给类型元素绑定值的函数,注意这个函数仅有在类型内的构造函数声明才能够访问。

lambda 表达式

lambda表达式可能会是我们很常用的一种函数,它也往往称为匿名函数,匿名函数的声明有两种方式

1
2
3
4
5
6
# 第一种
x->x^2+1
# 第二种
function (x)
return x->x^2+1
end

第一种声明方式后面需要跟一整个代码块,julia中可以使用begin,end来声明一段代码块(不过这个应该要算在metaprogramming的部分了?)

所以你可以这样

1
2
3
4
5
6
7
x->begin
if x == "蛤?"
return 1
else
return "excited"
end
end

而很多时候,我们都需要将匿名函数作为某个函数的第一个变量输入,比如像map,sum这种的。这个时候再写

1
2
3
4
5
6
7
map(x->begin
if x == "蛤?"
return 1
else
return "excited"
end
end, [1, "蛤?"2])

未免有些不美观,影响可读性,尤其是里面这个匿名函数比较复杂的时候。不用担心,julia为这种比较长的匿名函数提供了一个可读性更好的声明方式

1
2
3
4
5
6
7
map([1, "蛤?"2]) do x
if x == "蛤?"
return 1
else
return "excited"
end
end

do关键字会产生将后面的代码块(到end结束)创建成一个匿名函数然后传递给do前面的函数的第一个变量,比如map。有了这个特性,类似于Python的with关键字,我们在Julia中操作文件的时候也可以让它自动关闭

1
2
3
open("outfile", "w") do io
write(io, data)
end

循环和判断

和其它的编程语言类似,尤其是和python类似,julia的forwhile也有很多语法糖。

比如

1
2
3
for i = 1:10
@show i
end
1
2
3
for i in [1,2,3,4,5,6,7,8,9]
@show i
end

等等。然后ifelse语句类似于Python,但Python的elif变成了elseif

1
2
3
4
5
6
7
if x>0
println(1)
elseif x == 0
println(0)
else
println(3)
end

同时也支持expr?a:b形式的简写,意思是exprtrue则执行afalse执行b

模块和包管理

Julia的名字空间是通过模块module来进行管理的。

1
2
3
4
5
6
module foo
export foo1, foo2
############
# content
############
end

moduleend关键字中的代码块就会记在这个module下,使用的时候必须先将相应的module所开放的(export)的名字引入,方法有两种,一种是using,一种是import

using会将后面的模块中所有开放的变量暴露在当前的空间中,而import会引入相应的模块名称。举个例子,我使用QuDynamics这个模块的时候,如果

using QuDynamics,那么其中所开放的函数我都可以直接访问,比如propagate这个函数,我就可以直接使用它。而相应的import QuDynamics,就需要用.运算符访问模块的元素,也就是这个模块所开放的接口,例如QuDynamics.propagate

而Julia的package都是会由一个module进行封装,在安装的时候需要知道相应包的名字,比如Plots,然后使用Pkg.add命令进行安装。例如Pkg.add("Plots")。之后Julia的编译器就会访问在METADATA.jl中注册了的相应包的地址,然后从github上clone相关依赖。当然由于某些原因,这有时候会造成一些用户出现无法安装,或者下载缓慢的问题。这在第三版本的包管理器里也许会得到解决。

The End