尽管Julia的概念缺少带有类和方法的“经典”面向对象编程,但该语言提供了抽象工具,类型系统和功能性编程元素在其中发挥了关键作用。 让我们更详细地考虑第二点。
Julia中的函数概念可能与Lisp家族的语言(更确切地说是Lisp-1分支)最相似,并且可以从三个层次来考虑函数:作为子程序,作为特定动作序列的抽象以及表示该抽象的数据。
级别1.作为例程起作用
自史前时代以来,子程序的分配及其名称的分配一直在进行,那时Fortran被认为是高级语言,而C尚未出现。
从这个意义上说,Julia产品是标准的。 可以将“功能”称为在语法上没有划分过程和功能的事实。 无论是调用子例程以获取某个值还是仅对该数据执行某些操作,该子例程都称为函数。
函数的定义以关键字function开头,后跟参数列表,方括号中的命令序列,单词
end
结束定义:
""" sum_all(collection) Sum all elements of a collection and return the result """ function sum_all(collection) sum = 0 for item in collection sum += collection end sum end
语法的区别在于从Lisp继承的行为:对于从函数“正常”返回值的情况,不需要单词
return
:返回
end
之前计算的最后一个表达式的值。 在上面的示例中,将返回变量
sum
的值。 因此,
return
可以用作函数特殊行为的标记:
function safe_division(number, divisor) if divisor == 0 return 0 end number / divisor end
对于定义简短的函数,语法类似于数学符号。 因此,斜边沿腿长的长度的计算可以定义如下:
hypotenuse(a, b) = sqrt(a^2 + b^2)
使用三元运算符的“安全”除法可写为:
safe_division(number, divisor) = divisor == 0 ? 0 : number / divisor
如您所见,没有必要为函数参数指定类型。 考虑到Julia JIT编译器的工作方式,鸭子类型输入不会总是导致性能下降。
正如我在
上一篇文章中试图证明的
那样 ,Julia编译器可以通过输入参数的类型来推断返回结果的类型。 因此,例如,
safe_division
函数需要最少的修改才能实现快速操作:
function safe_division(number, divisor) if divisor == 0 return zero(number / divisor) end number / divisor end
现在,如果两个参数的类型在编译阶段都是已知的,则返回的结果的类型也将明确显示,因为
zero(x)
函数返回与其参数类型相同的零值(根据
IEEE 754 ,除以零的值具有浮点数格式的可完美表示的值)。
函数可以具有固定数量的位置参数,具有默认值的位置参数,命名参数和可变数量的参数。 语法:
级别2。作为数据的功能
该函数的名称不仅可以在直接调用中使用,而且还可以用作与获取值的过程相关联的标识符。 例如:
function f_x_x(fn, x) fn(x, x) end julia> f_x_x(+, 3) 6
带有功能参数的“经典”功能是
map
,
reduce
和
filter
。
map(f, x...)
将函数
f
应用于
x
(或i元素的元组)中所有元素的值,并将结果作为新集合返回:
julia> map(cos, [0, π/3, π/2, 2*π/3, π]) 5-element Array{Float64,1}: 1.0 0.5000000000000001 6.123233995736766e-17 -0.4999999999999998 -1.0 julia> map(+, (2, 3), (1, 1)) (3, 4)
reduce(f, x; init_val)
将集合“还原”为单个值,“扩展”链条
f(f(...f(f(init_val, x[1]), x[2])...), x[end])
:
function myreduce(fn, values, init_val) accum = init_val for x in values accum = fn(accum, x) end accum end
由于无法真正确定数组在约简过程中将以什么顺序通过,或者是否将
fn(x, accum)
fn(accum, x)
或
fn(x, accum)
,因此只有使用可交换或关联运算符(例如加法
fn(x, accum)
时,约简才会给出可预测的结果。或乘法。
filter(predicate, x)
返回满足谓词
predicate
的
x
元素的数组:
julia> filter(isodd, 1:10) 5-element Array{Int64,1}: 1 3 5 7 9 julia> filter(iszero, [[0], 1, 0.0, 1:-1, 0im]) 4-element Array{Any,1}: [0] 0.0 1:0 0 + 0im
使用高阶函数对数组进行操作而不是编写循环具有多个优点:
- 代码越来越短
map()
或reduce()
显示正在执行的操作的语义 ,那么您仍然需要了解循环中正在发生的事情的语义map()
允许编译器理解数组元素上的操作是独立于数据的,这允许应用其他优化
级别3。作为抽象的功能
通常在
map()
或
filter()
您需要使用尚未分配其自身名称的函数。 在这种情况下,Julia允许您表达对参数的操作的
抽象 ,而无需为此序列输入自己的名称。 这种抽象称为
匿名函数或
lambda函数 (由于在数学传统中,此类函数用字母lambda表示)。 该视图的语法为:
命名函数和匿名函数都可以分配给变量并作为值返回:
julia> double_squared = x -> (2 * x)^2
可变范围和词汇闭包
通常,他们试图以这样的方式编写函数:通过形式参数(即形式参数)获得计算所需的所有数据。 主体中出现的任何变量名称要么是形式参数的名称,要么是函数体内引入的变量的名称。
function normal(x, y) z = x + y x + y * z end function strange(x, y) x + y * z end
关于
normal()
函数,我们可以说所有变量名在主体中都是
相关的 ,即 如果我们到处(包括参数列表)都将“ x”替换为“ m”(或其他任何标识符),将“ y”替换为“ n”,将“ z”替换为“ sum_of_m_and_n”,则表达式的含义不会改变。 在
strange()
函数中,名称z是
不相关的 ,即 a)如果将该名称替换为另一名称,则含义可能会更改;并且b)函数的正确性取决于调用函数时是否定义了名称为“ z”的变量。
一般来说,
normal()
函数也不是那么干净:
- 如果在函数外部定义了名为z的变量,会发生什么情况?
- 实际上,+和*字符也是无关的标识符。
对于第2点,除了达成共识外,别无他法-系统中使用的所有功能的定义必须存在,这是合乎逻辑的,我们希望它们的真正含义符合我们的期望。
点1不像看起来那么明显。 事实是答案取决于函数的定义位置。 如果是全局定义的,则
normal()
z
将是局部变量,即 即使存在全局变量
z
其值也不会被覆盖。 如果函数的定义在代码块内部,则在此块中有
z
的较早定义,它将是要更改的外部变量的值。
如果函数主体包含外部变量的名称,则此名称与创建函数的环境中存在的值相关联。 如果函数本身是从此环境中导出的(例如,如果它是从另一个函数返回的值),则它将“捕获”内部环境中的变量,该变量在新环境中将不再可用。 这称为词汇闭包。
闭包主要在两种情况下有用:当您需要根据给定的参数创建函数时,以及当您需要具有某种内部状态的函数时。
考虑一个封装内部状态的函数的情况:
function f_with_counter(fn) call_count = 0 ncalls() = call_count
案例研究:所有相同的多项式
在
上一篇文章中,考虑了将多项式表示为结构。 特别地,存储结构之一是从最小的系数开始的系数列表。 为了计算点
x
处的多项式
p
,建议调用函数
evpoly(p, x)
,该函数根据Horner方案计算多项式。
完整的定义代码 abstract type AbstractPolynomial end """ Polynomial <: AbstractPolynomial Polynomials written in the canonical form --- Polynomial(v::T) where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}) Construct a `Polynomial` from the list of the coefficients. The coefficients are assumed to go from power 0 in the ascending order. If an empty collection is provided, the constructor returns a zero polynomial. """ struct Polynomial<:AbstractPolynomial degree::Int coeff::NTuple{N, Float64} where N function Polynomial(v::T where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}) coeff = isempty(v) ? (0.0,) : tuple([Float64(x) for x in v]...) return new(length(coeff)-1, coeff) end end """ InterpPolynomial <: AbstractPolynomial Interpolation polynomials in Newton's form --- InterpPolynomial(xsample::Vector{<:Real}, fsample::Vector{<:Real}) Construct an `InterpPolynomial` from a vector of points `xsample` and corresponding function values `fsample`. All values in `xsample` must be distinct. """ struct InterpPolynomial<:AbstractPolynomial degree::Int xval::NTuple{N, Float64} where N coeff::NTuple{N, Float64} where N function InterpPolynomial(xsample::X, fsample::F) where {X<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}, F<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}} if !allunique(xsample) throw(DomainError("Cannot interpolate with duplicate X points")) end N = length(xsample) if length(fsample) != N throw(DomainError("Lengths of X and F are not the same")) end coeff = [Float64(f) for f in fsample] for i = 2:N for j = 1:(i-1) coeff[i] = (coeff[j] - coeff[i]) / (xsample[j] - xsample[i]) end end new(N-1, ntuple(i -> Float64(xsample[i]), N), tuple(coeff...)) end end function InterpPolynomial(fn, xsample::T) where {T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}} InterpPolynomial(xsample, map(fn, xsample)) end function evpoly(p::Polynomial, z::Real) ans = p.coeff[end] for idx = p.degree:-1:1 ans = p.coeff[idx] + z * ans end return ans end function evpoly(p::InterpPolynomial, z::Real) ans = p.coeff[p.degree+1] for idx = p.degree:-1:1 ans = ans * (z - p.xval[idx]) + p.coeff[idx] end return ans end function Base.:+(p1::Polynomial, p2::Polynomial)
以结构形式表示的多项式并不完全对应于其对数学函数的直观理解。 但是通过返回函数值,多项式也可以直接指定为函数。 原来是:
struct Polynomial degree::Int coeff::NTuple{N, Float64} where N function Polynomial(v::T where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}})
我们将此定义转换为一个函数,该函数采用一个系数数组/元组,并返回计算多项式的实际函数:
function Polynomial_as_closure(v::T where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}})
同样,您可以为插值多项式编写函数。
一个重要的问题:在先前的定义中,新定义是否丢失了某些内容? 不幸的是,是的-将多项式设置为结构为编译器提供了提示,对我们而言,也为该结构提供了重载算术运算符的功能。 Julia,Julia没有提供如此强大的类型系统的功能。
幸运的是,在这种情况下,我们可以充分利用两个方面的优势,因为Julia允许您创建所谓的可调用结构。 即 您可以将多项式指定为结构,但可以将其称为函数! 在上一篇文章的结构定义中,您只需添加:
function (p::Polynomial)(z::Real) evpoly(p, z) end function (p::InterpPolynomial)(z::Real) evpoly(p, z) end
使用函数参数,您还可以为由一组点构造的某个函数添加插值多项式的外部构造函数:
function InterpPolynomial(fn, xsample::T) where {T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}} InterpPolynomial(xsample, map(fn, xsample)) end
我们验证定义 julia> psin = InterpPolynomial(sin, [0, π/6, π/2, 5*π/6, π])
结论
与纯命令式样式相比,从Julia的函数式编程中借用的可能性提供了更具表达力的语言。 以功能形式表示结构是更方便自然地记录数学概念的一种方式。