keylongx3x0
keylongx3x0

Reputation: 98

Julia Function type annotation

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

Answers (3)

user18059419
user18059419

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

Bogumił Kamiński
Bogumił Kamiński

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:

  • no function in Julia has Function type;
  • every function in Julia has its own unique type that is subtype of Function abstract type;
  • every function type is only identified by function name and module in which the function was defined.
  • a single function type can have many methods that take different arguments and have different return value.

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

Andy Gardner
Andy Gardner

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

Related Questions