Function Julia函数的类型
考虑以下代码位:Function Julia函数的类型,function,types,julia,Function,Types,Julia,考虑以下代码位: julia> function foo(x::Float64)::Float64 return 2x end foo (generic function with 1 method) julia> typeof(foo) typeof(foo) typeof(foo)不返回更有意义的内容肯定有原因,例如(Float64->Float64)。这是什么 我在查看时发现了这一点。函数定义末尾的::Float64只是一个类型断言(以及
julia> function foo(x::Float64)::Float64
return 2x
end
foo (generic function with 1 method)
julia> typeof(foo)
typeof(foo)
typeof(foo)
不返回更有意义的内容肯定有原因,例如(Float64->Float64)
。这是什么
我在查看时发现了这一点。函数定义末尾的
::Float64
只是一个类型断言(以及转换,如果可能的话)-它在任何方面都不会直接帮助编译器
为了理解这一点,让我们考虑以下功能:
f(a, b) = a//b
g(a,b) = b != 0 ? a/b : a
对于一对Int
值,这将返回一个Rational{Int}
。让我们检查一下Julia编译器可以做什么:
julia> code_warntype(f, [Int,Int])
Variables
#self#::Core.Compiler.Const(f, false)
a::Int64
b::Int64
Body::Rational{Int64}
1 ─ %1 = (a // b)::Rational{Int64}
└── return %1
或对于其他一对输入:
julia> code_warntype(f, [Int16,Int16])
Variables
#self#::Core.Compiler.Const(f, false)
a::Int16
b::Int16
Body::Rational{Int16}
1 ─ %1 = (a // b)::Rational{Int16}
└── return %1
您可以看到编译器可以根据输入类型计算输出类型。因此,Julia代码中的唯一规则是“始终编写类型稳定的代码”。
考虑函数:
f(a, b) = a//b
g(a,b) = b != 0 ? a/b : a
让我们测试一下类型:
julia> code_warntype(g, [Int,Int])
Variables
#self#::Core.Compiler.Const(g, false)
a::Int64
b::Int64
Body::Union{Float64, Int64}
1 ─ %1 = (b != 0)::Bool
└── goto #3 if not %1
2 ─ %3 = (a / b)::Float64
└── return %3
3 ─ return a
在Julia REPLUnion{Float64,Int64}
中显示为红色,这意味着代码的类型不稳定(返回int或float)
最后但并非最不重要的一点是,也可以询问函数输出类型:
julia> Base.return_types(f,(Int8,Int16))
1-element Array{Any,1}:
Rational{Int16}
julia> Base.return_types(g,(Int,Float64))
1-element Array{Any,1}:
Union{Float64, Int64}
总之:
- 不需要声明Julia函数的输出类型-这是编译器的工作
- 定义自己的输出类型与类型断言类似(或可用于结果类型转换-参见Bogumil的答案)
- 为了确定Julia中函数的输出类型,需要所有输入类型(不仅仅是方法名称)
- 当您编写
typeof(foo)
时,您询问的是函数的类型。此函数可以有多个方法(类型是否稳定,但这是另一个问题),这些方法具有不同的签名(参数类型),对于其中一些方法,编译器可能能够推断返回类型,而对于其他方法,则可能无法推断返回类型(而且你不应该完全依赖它——只要假设大部分时间编译器都做对了工作)
给出一个例子,考虑这个代码(1个函数,2个方法):
现在参考您所写的返回值规范:f(x)::Float64 = x
它们不仅仅是断言。朱莉娅实际上做了两件事:
- 它将返回值转换为请求的类型
- 转换成功的断言是否正确
f
中,您可以编写:
julia> f(1)
1.0
julia> f(true)
1.0
您可以看到转换(不仅仅是断言)发生了
这种风格在类型不稳定的代码中非常相关(例如,当使用类型不稳定的数据结构DataFrame
时),因为这样的断言可以帮助您的代码中“打破类型不稳定链”(如果您的代码中有一些部分不是类型稳定的)
编辑
例如,对于
f1
,使用Union{Int,Bool}->Int
?以及类似的类型不稳定性?我想这将需要为所有输入类型编译代码,因此失去JIT的优势
<如果>对于<代码> int >代码>返回一个<代码> int <代码>,对于<代码> BOOL 返回一个<代码> BOOL >考虑例如<代码>标识< /代码>函数。< /P>
还可以举下面的例子:
julia> f(x) = ("a",)[x]
f (generic function with 1 method)
julia> @code_warntype f(2)
Variables
#self#::Core.Compiler.Const(f, false)
x::Int64
Body::String
1 ─ %1 = Core.tuple("a")::Core.Compiler.Const(("a",), false)
│ %2 = Base.getindex(%1, x)::String
└── return %2
julia> Base.return_types(f)
1-element Array{Any,1}:
Any
@code\u warntype
正确地识别出,如果这个函数返回某个东西,它保证是String
,但是,正如Przemysław建议的那样,return\u types
告诉您它是Any
。因此,您可以看到这是一件困难的事情,您不应该盲目地依赖它——只要假设它取决于编译器来决定它可以推断出什么。特别是,出于性能原因,编译器可能会放弃进行推断,即使在理论上是可能的
在您的问题中,您可能指的是Haskell提供的内容,但在Haskell中有一个限制,即函数的返回值可能不取决于传递的参数的运行时值,而仅取决于参数的类型。Julia中没有此类限制。让我向您展示一些比较:
julia> struct var"typeof(foo)" end
julia> const foo = var"typeof(foo)"()
var"typeof(foo)"()
julia> (::var"typeof(foo)")(x::Float64) = 2x
julia> (::var"typeof(foo)")(x::String) = x * x
julia> foo(0.2)
0.4
julia> foo("sdf")
"sdfsdf"
julia> typeof(foo)
var"typeof(foo)"
这大约是内部发生的情况。当您编写函数foo
时,编译器会生成一个类似于具有内部名称typeof(foo)
和实例foo
的异常单例结构。然后,所有方法或“分派组合”都会在其类型上注册
你看,给foo
一个类似Float->Float
或String->String
的类型是没有意义的——函数只是单例值,它的类型是结构的类型。结构对它的方法一无所知(当然,在真正的编译器中,它在内部是这样做的,但不是以类型系统可以访问的方式)
从理论上讲,你可以设计一个系统,其中一个函数的所有方法都收集在某种联合类型中,但由于函数类型在其余域中是协变的,而在其域中是逆变的,因此这一系统变得非常庞大和复杂。因此,还没有完成。也就是说,人们使用语法来引用类型o方法实例本身,这正是您所想的。我认为返回类型规范在某些情况下有助于编译器,正如我在回答中所评论的那样。这不能通过使用
Union{Int,Bool}来解决吗->Int
?例如,对于f1
?以及类似的类型不稳定性?我猜这将要求为所有输入类型编译代码,因此失去JIT的优势?问题可以通过子类型解决,但它在许多方面变得复杂。我们可以使用Intersect{Int->Int,Bool->Int}
。但我们希望它是的子类型,例如,Intersect{Integer->Real,String->Ref,Bool->Number}
。或Integer->Real
。或