参数传递模式

Julia的参数传递是“共享传递”(pass by sharing),
这样可以省去复制的开销。
如果参数是标量的数值、字符串、元组(tuple)这样的非可变类型,
参数原来的值不会被修改;
如果参数是数组这样的可变(mutable)数据类型,
则函数内修改了这些参数保存的值,传入的参数也会被修改。

例如,非可变类型不会被修改:

function f(n)
  println("Inside f() before changing, n=", n)
  n = -1
  println("Inside f() after changing, n=", n)
  return
end
function test()
  n = 1
  f(n)
  println("Out of f(), n=", n)
end
test()
## Inside f() before changing, n=1
## Inside f() after changing, n=-1
## Out of f(), n=1

改变了输入的数组(可变类型)的值的例子:

function double!(x)
    for i in eachindex(x)
        x[i] *= 2
    end
end
xx = [1, 2, 3]
double!(xx)
@show xx;
## xx = [2, 4, 6]

Julia函数的命名习惯是,
如果函数会修改自变量的值,
将函数名末尾加上一个叹号后缀。

在上面的例子中,
函数double!()改变了数组自变量x的元素的值。
如果直接对自变量赋值,
则相当于在函数中重新绑定x
不会对输入的原始数组造成影响,如:

function double_wrong(x)
    x = 2 .* x
    return x
end
xx = [1, 2, 3]
double_wrong(xx)
@show xx;
## xx = [2, 4, 6]

无名函数

Julia的函数也是所谓“第一类对象”(first class objects),
可以将函数本身绑定在一个变量上,
函数名并非必须,
允许有无名函数。
无名函数在“函数式编程”(functional programming)范式中有重要作用。

无名函数格式是: 参数表 -> 返回值表达式,
其中参数表即自变量表,
没有自变量时参数表写()
只有一个自变量时可以不写圆括号而只写自变量名,
有多个自变量时将自变量表写成一个元组格式。

比如,函数

f(x)=x2+1又可以写成

x -> x^2 + 1
## #12 (generic function with 1 method)

无名函数的另一种写法如

function (x)
    x^2 + 1
end
## #14 (generic function with 1 method)

这样就产生了一个无名函数。

利用无名函数可以制作“函数工厂”,
即返回值为函数的函数。
例如:

make_power(α) = x -> x^α
f2 = make_power(2)
f3 = make_power(3)
[f2(2), f3(2)] |> show
## [4, 8]

链式调用与函数复合

x |> f可以表示f(x),
这样,x |> f |> g就是复合调用g(f(x))
称为链式调用。
但是,不允许写成x |> f() |> g()
需要的话可以将每一步写成一个无名函数。
例如:

[1:5;] |> (x -> x .^ 2) |> sum
## 55

加点的运算,如g(f.(x))
可以写成x .|> f |> g
如:

[1:5;] .|> (x -> x ^ 2) |> sum
## 55

例子中的[1:5;]的写法类似于collect(1:5)
链式调用中的无名函数应该用()保护,
以免发生优先级疑惑。

g(f(x))也可以用“”运算符表示成g∘f(x)
”的结果仍是函数。
”输入方法为\circ<TAB>
这样的复合运算在有确定的函数名时当然没有必要,
但是如果用变量保存了函数,
以至于预先不知道要复合的函数,
就是需要的。
如:

funcs = [uppercase, lowercase, first]
fruits = ["apple", "banana", "carrot"]
fnew = funcs[1] ∘ funcs[3]
y = map(fnew, fruits);
@show y;
## y = ['A', 'B', 'C']

程序对每个单词取第一个字母并转换成大写。
等效于下面每一行:

map(uppercase∘first, fruits)
(uppercase∘first).(fruits)
uppercase.(first.(fruits))
fruits .|> first .|> uppercase 
[(uppercase∘first)(xi) for xi in fruits]
[uppercase(first(xi)) for xi in fruits]

示例:图像点阵下标与xy坐标转换

一个图像可以简单地看成一个

m×n矩阵,
矩阵元素是像素点,
一般取元素值。
为了对图像进行一些数学变换,
需要将图像看成是直角坐标平面上定义的二元函数,
而矩阵的行列下标与一般的实数值的x、y坐标还是有差别。
下面的函数来自MIT的计算思维导论公开课件,
用于将图像与x、y坐标上的二元函数相互转换。

下面是将图像从平面直角坐标定位转换回到整数行、列下标定位的函数,
输入为图像,一对

(x,y)坐标,
输出对应的

(i,j)下标。
认为坐标原点在图像正中心,超出范围的点返回白色或黑色。
这是一个标量函数,没有针对整个图像变换,
但输入整个图像。
因为Julia的函数自变量是引用传输而不是复制传输,
所以输入整个图像并不损失效率。

function transform_xy_to_ij(img::AbstractMatrix, x::Float64, y::Float64)
    rows, cols = size(img) # 行、列数
    m = max(cols, rows)
    
    # 平移变换
    translate(α,β)  = ((x, y),) -> [x+α, y+β]
    # 坐标互换
    swap(x,y) = [y, x]
    # 纵坐标颠倒
    flipy((x, y)) = [x, -y]
    # 伸缩变换
    scale(α) = ((x,y),) -> (α*x, α*y)

    # 从平面直角坐标到行、列下标的函数
    xy_to_ij =  translate(rows/2, cols/2) ∘ swap ∘ flipy ∘ scale(m/2)
    # 使用函数将输入的一对(x, y)值转换为整数
    i, j = floor.(Int, xy_to_ij((x, y))) 
    # 注意:返回值仅两个整数,不是向量化的。
end

上面程序的主要语句是xy_to_ij的定义。
该式从右向左解释:

  • 先从[-1,1]区间变换到[-m/2, m/2]区间;
  • 将y轴颠倒次序,这是因为用行下标代表纵轴,
    行下标的增长方向是从上到下方向,与纵轴方向相反;
  • 调换x, y,这是因为行下标i代表了上下方向,即y方向,
    列下标j代表了左右方向,即x方向;
  • 最后将i最小值变为0,j最小值变为0。

下面是从行列下标转换为平面直角坐标系坐标的函数,
输入一对

(i,j)下标,
和图像横、纵方向像素点数的最大值pixels
输出平面直角坐标系坐标

(x,y)。

function transform_ij_to_xy(i::Int, j::Int, pixels)
    # 从行、列下标转换为直角平面坐标系坐标的函数
    # pixels是横向、纵向的像素点数的最大值
    
    # 用来转换的函数。
    ij_to_xy =  scale(2/pixels) ∘ flipy ∘ swap ∘ translate(-pixels/2,-pixels/2)
    # 上式从右向左解读:记m=pixels,
    # 先将坐标范围从$[1,m] \times
   ij_to_xy([i,j])
end

上面函数的主要部分是函数ij_to_xy的定义。
因为有参数pixels,所以不直接使用此函数,
而是将其包裹在transform_ij_to_xy中。

ij_to_xy的定义也充分利用了函数复合,
从右向左解读如下(记pixels

m):

  • 将坐标范围从 [1,m]×[1,m]

    平移到 [−m/2,m/2]×[−m/2,m/2]

  • 交换行、列关系;
  • 将纵轴颠倒;
  • 将坐标范围限制在 [−1,1]×[−1,]

    之间。

还有一个配套的给定了图像和一个

(x,y)坐标,
取出对应的像素点的函数。
超出范围则给出一个纯黑像素点。

using Colors
function getpixel(img::AbstractMatrix,i::Int, j::Int)   
    rows, cols = size(img)
    m = max(cols,rows)
    black(c::RGB) = RGB(0,0,0)
    black(c::RGBA) = RGBA(0,0,0,0.75)
    
    if 1 < i ≤ rows && 1 < j ≤ cols
        img[i, j]
    else
        black(img[1,1])
    end
end

可变个数参数与元组实参

在自定义函数的自变量名后面加上三个句点作为后缀,如args...
则此函数可以有可变个数的位置参数,
args为一个元组。

如:

function f_vara1(x, args...)
    println("x=", x)
    println("其它参数:", args)
end
f_vara1(11, 1, 2, 3)
## x=11
## 其它参数:(1, 2, 3)

有时需要传递给一个多自变量函数的实参保存在了一个变量中,
比如,
函数max()求各个自变量中最大值,

如果要求最大值的数已经在一个元组或数组中如何利用max()求最大值?
可以用“展开”(splatting)的方法将一个变量中的多个值展开成函数的自变量,
方法是在作为实参的自变量名后面加三个句点后缀,如

x = [1, 3, 1, 4]
max(x...)
## 4

递归调用

函数定义时允许调用自己,
这使得许多本质上是递推的计算程序变得很简单,
比如,

n!=n(n−1)!,
用递归函数可以写成:

function myfact(n)
    if n==1
        return 1
    else
        return n*myfact(n-1)
    end
end
myfact(5)
## 120

递归程序必须有初始结果,
比如上面程序中

n=1时结果为1;
递归必须能逐步退回到初始结果的地方。

再比如Fibonacci序列,

F1=1,

F2=1,

Fn=Fn−1+Fn−2,
前几个数是1, 1, 2, 3, 5, 8, 13, 21, 34。
用递归调用写成

function myfib(n)
    if n <= 0
        return 0
    elseif n==1 || n==2
        return 1
    else
        return myfib(n-1) + myfib(n-2)
    end
end
show([(n, myfib(n)) for n=0:9])
## [(0, 0), (1, 1), (2, 1), (3, 2), (4, 3), 
##  (5, 5), (6, 8), (7, 13), (8, 21), (9, 34)]

递归程序只是把用递推表示的问题变得容易变成解决,
很多时候效率并不好。
比如,
为了计算

F5,
需要用到

F4和

F3,

F4的时候又需要重新计算

F3,
所以

F3被重复计算了2次,
计算

F3要计算

F2,

F4也要计算

F2,
所以

F2也被重复计算了

3次。
这都是不必要的额外计算,
比如改成如下的正向循环:

function myfib2(n)
  local f1, f2, f3
  
  if n <= 0
    return 0
  elseif n==1 || n==2
    return 1
  end 

  f1 = 1
  f2 = 1
  for i=3:n
    f3 = f1 + f2 
    f1, f2 = f2, f3 
  end 
  return f3
end
myfib2.(1:10) |> show 
## [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

闭包

可以在函数内定义内嵌函数并以此内嵌函数为函数返回值,
称这样的内嵌函数为闭包(closure)。
闭包的好处是可以保存定义时的局部变量作为一部分,
产生“有状态的函数”,
类似于面向对象语言中的方法,
而Julia并不支持传统的面向对象语言中那样的类。

例如,
某个函数希望能记住被调用的次数。
传统的方法无法解决问题,比如下面的版本是无效的:

function counter_old()
    n = 0
    n = n+1
    return n
end
println(counter_old(), ", ", counter_old())
## 1, 1

可以用如下的闭包做法:

function make_counter()
    n = 0
    function counter()
        n += 1
        return n
    end
end
my_counter = make_counter()
typeof(my_counter)
## var"#counter#22"
println(my_counter(), ", ", my_counter())
## 1, 2

递归函数在反复调用自身以后效率会很低,
比如,前面计算Fibonacci数的递归函数,
因为对每个

n都要多次调用自变量为

0,1,2,…的情形,
所以进行许多不必要的重复计算。
可以用闭包的方法将已有结果保存,
使得计算效率大大提高:

function makefib()
    saved = Dict(0=>0, 1=>1)
    function fib(n)
        if n ∉ keys(saved)
            saved[n] = fib(n-1) + fib(n-2)
        end
        return saved[n]
    end
end
myfibnew = makefib()
show(myfibnew.(0:9))
## [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

函数式编程

无名函数经常用在map(), filter()reduce()这样的函数式编程函数中。

map

map(f, x)将函数f作用到容器x的每个元素上,
有时可以用f.(x)代替,
但有些情况下不能使用广播(加点语法)。

如:

map(x -> x^2 + 1, [1,2,3])
3-element Array{Int64,1}:
  2
  5
 10

在调用map()函数时,
如果对每个元素执行的操作需要用多行代码完成,
用无名函数就不太方便。
例如,对数组的每个元素,
负数映射到0,
大于100的数映射到100,
其它数值不变,用有名函数可以写成:

function fwins(x)
    if x < 0
        y = 0
    elseif x > 100
        y = 100
    else
        y = x
    end
    return y
end
fwins.([-1, 0, 80, 120]) |> show
## [0, 0, 80, 100]

也可以写成map(fwins, [-1, 0, 80, 120])
如果要用无名函数的格式,
Julia还提供了map函数的一种do块格式,如

map([-1, 0, 80, 120]) do x
    if x < 0
        y = 0
    elseif x > 100
        y = 100
    else
        y = x
    end
    return y
end
4-element Array{Int64,1}:
   0
   0
  80
 100

注意这样调用map()时圆括号中仅有要处理的数据,
要进行的操作写在do关键字后面,
操作写成了不带->符号的无名函数格式。

对于多元函数,
计算是按对应元素进行的,如:

map((x,y) -> 2*x + y, [1, 2, 3], [10, 20, 30]) |> show
## [12, 24, 36]

filter

函数filter(f, x)f是返回布尔值的函数,
称这样的函数为示性函数(indicator functions),
x是向量,
filter(f, x)的结果是将f作用在x的每个元素上,
输出f的结果为真值的那些元素组成的数组。如

filter(x -> x>0, [-2, 0, 1,2,3]) |> show
## [1, 2, 3]

reduce

reduce(f, x)f是接受两个自变量的函数,
如加法、乘法,结果是将x中的元素用f反复按结合律计算结果。
称这种做法为约化计算。

比如,
x的元素和,
除了sum(x)函数,
也可以写成reduce(+, x):

对于多维数组,
可以指定沿哪一个维度方向进行简化计算

mat = reshape([1:9;], 3, 3)
3×3 Array{Int64,2}:
 1  4  7
 2  5  8
 3  6  9
1×3 Array{Int64,2}:
 6  15  24
3×1 Array{Int64,2}:
 12
 15
 18

有一些reduce的操作已经写成了内置函数,
sum, prod, minimum, maximum, all, any

可以将mapreduce合并为mapreduce()函数,如:

mapreduce(x -> x ^ 2, +, [1:3;])
## 14

accumulate

类似于sum()求总和而cumsum()给出累计求和的所有中间结果,
cumprod()计算连乘过程中的所有中间结果,
函数accumulate()可以将二元运算累计地计算并给出每一步的中间结果。
如:

cumsum(1:5) |> show
## [1, 3, 6, 10, 15]
accumulate(+, 1:5) |> show
## [1, 3, 6, 10, 15]

用于多维数组时可以指定在某个维方向上进行累计计算。如:

mat = reshape([1:9;], 3, 3)
accumulate(+, mat, dims=1)
3×3 Array{Int64,2}:
 1   4   7
 3   9  15
 6  15  24
accumulate(+, mat, dims=2)
3×3 Array{Int64,2}:
 1  5  12
 2  7  15
 3  9  18

map()filter()reduce()对非数值元素的数组也是适用的。

异常处理

只要是程序,
就难以避免会有出错的时候。
为了在程序出错的时候造成严重后果,
传统的办法是对程序增加许多判断,
确保输入和处理是处于合法和正常的状态。
这样的做法过于依赖于程序员的经验,
程序代码也过于繁复。

现代程序设计语言增加了“异常处理”功能。
程序出错时,
称为发生了异常(exception),
编程时可以用专门的程序“捕获”这些异常,
并进行处理。
这并不能保证程序不出错,
而是出错时能得到及时的、不至于造成严重后果的处理。

Julia中捕获异常并处理的基本结构是:

try
  可能出错的程序
catch 异常类型变量名
  异常处理程序
end

x = [2, -2, "a"]
for xi in x 
    try
        y = sqrt(xi)
        println("√", xi, " = ", y)
    catch ex 
        if isa(ex, DomainError)
            println("√", xi, ": 平方根函数定义域异常")
        else
            print("√", xi, ": 平方根函数其它异常")
        end
    end 
end
## √2 = 1.4142135623730951
## √-2: 平方根函数定义域异常
## √a: 平方根函数其它异常

异常处理代码会降低程序效率,
一些常见错误还是应该使用逻辑判断预先排除。

throw()函数可以当场生成(称为“抛出”)一个异常对象,
throw(DomainError())
Julia中内建了一些异常类型,
详见Julia的手册。

error(msg)可以直接抛出ErrorException对象,
使得程序停止,
并显示给出的msg字符串的内容。

try结构可以有一个finally部分,
表示不论有没有出错,
都需要在最后运行的语句。
框架如:

try
    可能出错的程序
catch 异常类型变量名
    异常处理程序
finally
    无论如何最后都要执行的程序
end