Reputation: 98
I`m new to Julia, so this might be a stupid question.
I have a type, with a function as an attribute, like:
struct exampleStruct
someAtrribute::someType
theFunction::Function
end
Is there a proper way to further specify the type of theFunction
with regards to argument types and return types? In my case I know for sure that the instances of theFunction
should only take arguments of type Float64
and have return values of type Float64
, e.g. I could write something like theFunction::Function{Float64,...}{Float64}
.
I guess you could maybe make a subtype of Function
, but I feel like that would require digging a fair bit deeper.
Upvotes: 4
Views: 1701
Reputation:
Yes there will certainly be a way to do that if you dig deep, but the question is: will you be using Julia that way? Then it'd be best if you stick to Python (or whatever language you're coming from), because you won't be utilising the full power Julia can offer you.
I have a question for your source code: is exampleStruct
the only type on "earth" that works with a function called theFunction
? If the answer is no, the point is why bother tying thefunction
to exampleStruct
. Follow the language's style if you want to enjoy it and use MULTIPLE DISPATCH.
Define theFunction
outside and implement it for exampleStruct
. Then when you need/have another type that uses theFunction
, then go back and simply extend theFunction
via multiple dispatch; this way you're in sync with Julia and would save yourself "unnecessary" complexity the language wasn't designed for.
A case in point is the iterate
function in the Base module of Julia.
julia> Base.iterate
iterate (generic function with 230 methods)
Yes, you saw that right: 230 methods for just one function. So you see your question was not a foolish one, it only seeks to reveal the beauty of Julia.
The iterate
function is dispatched for 230 different types. This is so easy to manage and scale; if you have a bug, just go to the type implemented by iterate
that's causing the bug and then fix it. This way, you can easily add new types and then extend the iterate
function. Could you have done this so easily if you were doing it in a classical OOP-style - yes, only if you're a veteran and would spend a month managing inheritance, dependencies and all those stuffs, just to fix a bug that's likely being caused by just one type.
So your question is a wise one; your exampleStruct
would work perfectly on theFunction
when its defined inside the type; but then when you want to scale, multiple dispatch
would be your best bet.
Upvotes: 0
Reputation: 69829
In Julia every function has its own unique type. For example if you take function sin
:
julia> typeof(sin)
typeof(sin) (singleton type of function sin, subtype of Function)
you can see that it has type that is displayed as typeof(sin)
.
Additionally you get an information that this type is a subtype of abstract type Function
. You can check this with:
julia> supertype(typeof(sin))
Function
Conversly, you can find all subtypes of Funcion
type in your current Julia session by writing:
julia> subtypes(Function)
11259-element Vector{Any}:
ArgTools.var"#1#11"
ArgTools.var"#10#20"
ArgTools.var"#2#12"
ArgTools.var"#21#31"
⋮
typeof(⊉) (singleton type of function ⊉, subtype of Function)
typeof(⊊) (singleton type of function ⊊, subtype of Function)
typeof(⊋) (singleton type of function ⊋, subtype of Function)
(this is the output on a fresh Julia 1.7.2 session)
Now it is crucial to understand that function type is associated with its name and the module in which the function was defined. For example let me import
the Statistics
module:
julia> import Statistics
julia> typeof(Statistics.mean)
typeof(Statistics.mean) (singleton type of function mean, subtype of Function)
Actually you can extract out type name and module where the function type was defined:
julia> Base.nameof(sin)
:sin
julia> Base.parentmodule(sin)
Base
julia> Base.nameof(Statistics.mean)
:mean
julia> Base.parentmodule(Statistics.mean)
Statistics
(this distinction is important as you can have in Julia many functions having the same name, but be defined in different modules)
So you can think of function type as being defined by function name and module. As you can see there is no argument types or return value type in definition of function type. Why is it so? The reason is that one function in Julia can have many methods. Function type itself does not provide any functionality - it is just a name and module in which the function name is defined. Only after you add methods to a function you have something that can be executed. Since you can have many methods of a given function each of this methods can have different types of arguments and different return value.
Let us check for Statistics.mean
what methods it has defined:
julia> methods(Statistics.mean)
# 5 methods for generic function "mean":
[1] mean(r::AbstractRange{<:Real}) in Statistics
[2] mean(A::AbstractArray; dims) in Statistics
[3] mean(itr) in Statistics
[4] mean(f, A::AbstractArray; dims) in Statistics
[5] mean(f, itr) in Statistics
And you can add methods to a function later. Here is an example:
julia> function f end
f (generic function with 0 methods)
julia> methods(f)
# 0 methods for generic function "f":
julia> Base.nameof(f)
:f
julia> Base.parentmodule(f)
Main
julia> f(x::Integer) = 10x
f (generic function with 1 method)
julia> methods(f)
# 1 method for generic function "f":
[1] f(x::Integer) in Main at REPL[26]:1
julia> f(x::String) = x^10
f (generic function with 2 methods)
julia> methods(f)
# 2 methods for generic function "f":
[1] f(x::Integer) in Main at REPL[26]:1
[2] f(x::String) in Main at REPL[28]:1
(in the example I show you that you can even define a function, with a new type that initially has no methods defined)
In summary:
Function
type;Function
abstract type;For these reasons in Julia it is impossible to specify the type of function with regards to argument types and return types (you can only specify it up to name and module where the function type was defined).
Now regarding the comment of @jling.
Your definition:
struct exampleStruct
someAtrribute::someType
theFunction::Function
end
is correct and will accept any function as theFunction
argument. However, what is suggested that you create a parametric type instead:
struct exampleStruct{T<:Function}
someAtrribute::someType
theFunction::T
end
This recommendation is related to later performance of using the exampleStruct
. If field type of theFunction
is Function
this is an abstract type and using abstract types as parameters in containers will degrade performance of your code as is explained in this section of the Julia Manual. However, if your code is not performance critical then you should be fine with your original definition.
As a final note, bear in mind that in Julia, there are callables that are not a subtype of Function
type. Two most important cases are type constructors, e.g. you can call Int
constructor, but Int
is not a subtype of Function
, and the second are functors, as explained in this section of the Julia Manual.
This means that in your definition of exampleStruct
, if you restrict the second field type to Function
you will disallow passing some callables as its arguments. Maybe this is not a problem in your case, but it is a common issue that is encountered when users try to create data structures that store functions as their fields.
Upvotes: 4
Reputation: 61
I'm fairly new to Julia, too. And I'm still learning to think in terms of multiple dispatch vs O-O ways. Types aren't objects exactly. You can make a struct callable like a function, but it doesn't support multiple function specifications in a struct like objects in O-O approaches. Instead, the paradigm is to write functions that take the type as an argument. In your case, something like:
function theFunction{x::exampleStruct, y, etc}
Upvotes: 2