打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Julia语法基础:函数进阶

Julia语言作为一个以动态语言为主的语言,又没有传统的面向对象技术,函数就变得格外的重要。如果可以把Julia语言作为函数式语言来理解,那么编写函数的水平就决定了你编写代码的水平。

一切起点:函数是一等对象

后面提到的各种函数式编程的技术都源于函数是一等对象(first-class objects,也被称为一等公民(first-class citizens)这一前提。

作为“一等对象”必须满足下面的条件:

  • 在运行时创建

  • 能赋值给变量或数据结构中的元素

  • 能作为参数传给函数

  • 能作为函数的返回结果

julia> foo(x) = x * xfoo (generic function with 1 method)julia> f = foofoo (generic function with 1 method)julia> f(5)25julia> foo(5)25julia> function call_function(fun, x)           fun(x)       endcall_function (generic function with 1 method)julia> call_function(f, 5)25

可以这么说,普通对象(比如变量、字符串)能做的任何事,函数也能做。函数只是一个把参数元组映射到返回值的对象。唯一特殊之处时它是可调用的。

高阶函数

高阶函数(higher-order function)就是操作函数的函数,它接收一个或多个函数作为参数,或者返回一个新函数。在函数式编程语言中,最广为人知的高阶函数有mapfilterreduceapply在Python3和Julia中都不再内置)。

不过,有了列表推导,mapfilter就显得没那么重要了。

计算阶乘

julia> map(fact, range(0, stop=5))6-element Array{BigInt,1}:   1   1   2   6  24 120

可以使用列表推导获得更好的可读性:

julia> [fact(n) for n = 0:5]6-element Array{BigInt,1}:   1   1   2   6  24 120

配合filter,只提取0!到5!中奇数的阶乘列表:

julia> map(fact, filter(x->x % 2 == 1, range(0, stop=5)))3-element Array{BigInt,1}:   1   6 120

注意,Julia中,布尔值和整数不能互换,例子中的匿名函数不能写成x->x % 2

当然,列表推导也是可以解决这个问题的:

julia> [fact(n) for n = 0:5 if n % 2 == 1]3-element Array{BigInt,1}:   1   6 120

在Julia中,还内置了reduce,这个函数的思想是把某个操作连续应用到序列的元素上,并累计之前的结果。比如可以用reduce,直接定义出阶乘:

julia> function fact(n)           reduce(*, 1:n)       endfact (generic function with 1 method)julia> fact(5)120

不过,Julia为了性能考虑,并没有自动提升类型。所以,你会发现,这种写法很快就溢出了.......fact(100)是计算不出的。

reduce最常用来求和。不过,强烈建议使用sum

julia> reduce( , 1:5)15julia> sum( , 1:5)15

闭包

不少人会混淆闭包和匿名函数,这也情有可原,毕竟在函数内部定义函数不常见,直到有了匿名函数才会这样做。只有涉及了嵌套函数,才会有闭包问题。

闭包延伸了函数的作用域,其中包含函数定义体中、引用不在定义体中定义的非全局变量。函数是不是匿名没有关系,关键是它能访问到函数定义体之外的非全局变量。

比如,有个名为avg的函数,它是用来计算不断增加的系列值的均值,可以用来计算某支股票的整个历史上的平均收盘价。由于会不断增加新的价格,因此平均值要考虑目前为止的所有价格。

我们希望avg是这样的:

julia> avg(10)10.0julia> avg(11)10.5julia> avg(12)11.0

avg要怎么做才能保存历史值?如果是面向对象语言,很容易想到类。但是,Julia没有类的概念,我们可以使用高阶函数来实现。

julia> function make_averager()           count = 0           total = 0                     function averager(new_value)               count = 1               total = new_value               return total / count           end                     return averager       endmake_averager (generic function with 1 method)

接下来,就可以这样使用这个高阶函数:

julia> avg = make_averager()(::getfield(Main, Symbol('#averager#3'))) (generic function with 1 method)julia> avg(10)10.0julia> avg(11)10.5julia> avg(12)11.0

counttotalmake_averager的局部变量,这没错。但是,在avg(10)的时候,make_average函数已经返回了,它的本地作用域已经不在了。所以在average函数中,counttotal是自由变量(free variable),并没有在本地作用域中绑定。

如果要在Python 3中使用这种写法,需要用nolocalcounttotal声明为自由变量,否则会被认为是局部变量。

闭包,本质上就是一种函数。它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是它仍然能使用这些绑定。

注意:只有嵌套在其它函数中的函数才可能处理不在全局作用域中的外部变量。

方法

Julia允许根据给定的参数数量以及函数参数的类型来选择调用哪个方法。与传统的面向对象语言不同,传统的面向对象语言仅支持基于第一个参数进行调度。第一个参数通常具有特殊的参数语法,有时候是隐含而没有显式给出参数。(Python的self,Java或者C 的this

使用函数的参数来决定调用哪个方法,称为多分派(multiple-dispatch)。

一个简单的例子

julia> f(x::Float64, y::Float64) = 2x yf (generic function with 1 method)julia> f(2.0, 3.0)7.0julia> f(5, 2.0)ERROR: MethodError: no method matching f(::Int64, ::Float64)Closest candidates are:  f(::Float64, ::Float64) at REPL[6]:1Stacktrace: [1] top-level scope at none:0

通常可以在特定类型上实现优化,并提供一个一般化方法接收剩余情况。对于数字类型,可以考虑提供一个Number的一般化参数。

julia> f(x::Number, y::Number) = 2x - yf (generic function with 2 methods)

多分派与函数重载的区别

可能你会好奇,多分派这和函数重载有什么区别?怎么说呢,还是有一点差别的。

首先,重载函数通常还是具有固定类型的隐式接收器(implicit receiver)。Julia更偏向于是函数式语言,所以会分派所有的参数。

举一个discourse上的例子,假如你编写一个Asteroids™游戏,你就处理两个物体(对象)碰撞的时候会发生什么。如果是Java,你可能会这样写:

public class Asteroid {  public void collideWith(Asteroid other) { /*...*/ }  public void collideWith(Ship other) { /*...*/ }}public class Ship {  public void collideWith(Asteroid asteroid) { /*...*/ }}

等到你出了续集,Moar Asteroids™,你的飞船可以射击,你可能就要添加一个Bullet的子弹类。

public class Asteroid {  /*...*/  public void collideWith(Bullet bullet) { /*...*/ }}public class Ship {  /*...*/  public void collideWith(Bullet bullet) { /*...*/ }}public class Bullet {  public void collideWith(Asteroid asteroid) { /*...*/ }  public void collideWith(Ship ship) { /*...*/ }  public void collideWith(Bullet other) { /*...*/ }}

为了这个新类型,你就要添加5个新方法。假如,你的续集也很成功,要出第三部,Space Rocks™,在这个版本中还有导弹......那你要新加8个方法。

碰撞都是相互的,利用Julia的多分派就可以比较巧妙的实现:

# Asteroids™ abstract type GameObject endstruct Asteroid <: GameObject endstruct Ship <: GameObject end# 碰撞是相互的collide(a::GameObject, b::GameObject) = collide(b, a)collide(first::Asteroid, second::Asteroid) =  #...collide(asteroid::Asteroid, ship::Ship) =  #...''

等到你出续集的时候,只要继续写:

# 为续集Moar Asteroids添加™struct Bullet <: GameObject endcollide(first::Bullet, second::Bullet) =  #...collide(bullet::Bullet, asteroid::Asteroid) =  #...collide(bullet::Bullet, ship::Ship) =  #...# etc...

你可能会说,如果是C 可以在外面重载一个全局的函数,这是没什么大问题的。Java就似乎不行了,毕竟一切都在类中,你不能跳出类来定义函数。

除此之外,Julia可以在多个参数类型上进行运行时分派。C 只能在运行时通过使用虚函数来实现,而虚函数只分派到类实例的类型。

查看方法

要查看函数定义了多少方法,只需要输入对应名称:

julia> ff (generic function with 2 methods)

如果你想要找出这些方法的签名,可以使用methods方法:

julia> methods(f)# 2 methods for generic function 'f':[1] f(x::Float64, y::Float64) in Main at none:1[2] f(x::Number, y::Number) in Main at none:1

方法歧义

如果定义了一组函数方法,可能会出现歧义。

julia> g(x::Float64, y) = 2x yg (generic function with 1 method)julia> g(x, y::Float64) = x 2yg (generic function with 2 methods)julia> g(2.0, 3)7.0julia> g(2, 3.0)8.0julia> g(2.0, 3.0)ERROR: MethodError: g(::Float64, ::Float64) is ambiguous. Candidates:  g(x, y::Float64) in Main at REPL[20]:1  g(x::Float64, y) in Main at REPL[19]:1Possible fix, define  g(::Float64, ::Float64)Stacktrace: [1] top-level scope at none:0

在歧义的情况下,Julia抛出了MethodError而不是任意选择一个方法。建议对两个的交集提供适当的方法,可以避免模糊:

julia> g(x::Float64, y::Float64) = 2x   2yg (generic function with 3 methods)julia> g(2.0, 3.0)10.0

参数方法

参数方法通过指定参数必须是相同类型。

julia> same_type(x::T, y::T) where {T} = truesame_type (generic function with 1 method)julia> same_type(x, y) = falsesame_type (generic function with 2 methods)julia> same_type(1, 2)truejulia> same_type(1, 2.0)false

在0.6x,参数方法是same_type{T}(x::T, y::T),但有时会引起歧义和可读性问题。0.7版本以后使用了where进行后置指定。

还能进一步约束子类型:

julia> same_type_numeric(x::T, y::T) where {T <:Number} = truesame_type_numeric (generic function with 1 method)julia> same_type_numeric(x::Number, y::Number) = falsesame_type_numeric (generic function with 2 methods)julia> same_type_numeric(1, 2)truejulia> same_type_numeric(1, 2.0)false

仿函数

方法和类型是相关联的。可以通过对类型添加方法使任意的Julia变得“可调用”。

julia> struct Polynomial{R}           coeffs::Vector{R}       endjulia> function (p::Polynomial)(x)           v = p.coeffs[end]           for i = (length(p.coeffs) - 1) : -1: 1               v = v * x p.coeffs[i]           end           return v       end

请注意,该函数是由类型而不是按名称指定的。p将引用被调用的对象。

julia> (p::Polynomial)() = p(5)julia> z = Polynomial([1, 10, 100])Polynomial{Int64}([1, 10, 100])julia> z(3)931julia> z()2551

这个机制也是类型构造函数和闭包在Julia中工作的关键。

总结

注意,函数是一等对象以及高阶函数、闭包并不仅仅限于Julia,这部分内容在Python也是通用的。

  1. 函数在Julia中是一等对象;

  2. Julia有很明显的函数式特征;

  3. 多分派可以给Julia带来更多的表现力;

  4. 避免出现方法歧义,可以通过提供两个函数形参的交集的方法。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Julia机器核心编程.多重分配
碾压 Python!为什么 Julia 速度这么快?
为什么Julia比Python快?因为天生理念就更先进啊
作为数据科学家,我并没有选择 Python,而是从 R 到了 Julia!
Julia编程07:Function
在WebView中如何让JS与Java安全地互相调用
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服