Function in type definition


#1

Hi folks,
this is something that has been in my mind for few days now, and that finally want to know. The question is: is it possible to include a function as part of a type definition? I mean, something of the form:

type koly
  x::Int16
  y::Float64
  V::x-> x^2
end

I hope you understand what I mean. Even better, assume I have an already defined function

function V(x::Float64)
  return x*x
end

could/how can I make some attribute defined in type koly point to function V?

Thanks for your help,

Ferran.


#2

I’ve discovered I can declare in koly a variable of type Any and then do the assignment

type koly
  x::Int16
  y::Float64
  f::Any
  koly() = new()
end

and then

A = koly()
A.x = 16
A.y = 14.0
A.f = V

such that

A.f(3.0)

returns 9.0 as expected… but is this the right way to do it? Is’t declaring something as Any going against performance?

Thanks again,

Ferran.


#3

Is this what you want?

type koly
    x::Int16
    y::Float64
    V::Function
end

a = koly(1,1.0,x->x^2)

Or d you want that function to be related to the first field x ?
This would be achieved like this

type koly
    x::Int16
    y::Float64
    V::Int16
    koly(x::Int16,y::Float64) = new(x,y,x^2)
end

a = koly(2,1.0)

#4

Overloading of getfield and setfield! is coming in 1.0, which will allow you to do things like

getfield(k::koly, f::Symbol) = (f == :V) && (return V)  # this is not a great way of doing this but it works

k = koly(1, 1.0)

k.V(x)

However, this is somewhat in violation of the spirit of multiple dispatch. The recommended procedure would be to instead define

V(k::koly, x) = k.x + x^2  # this is just an example, in case you'd want V to involve koly somehow

If you’re coming from more traditional object oriented languages, this may at first seem like a bizarre anti-pattern. Once you get a bit used to it, however, you will probably learn to love and embrace it. There is very rarely any reason for objects to “own” functions in this way, it is simply an historical artifact of C++. Your example demonstrates that quite well (even though I’m sure it was simplified for demonstration purposes): if you just want to square a float, there is no reason for the function doing this to have any special relation to koly. koly is a type, not a namespace. If you are looking for a namespace, module serves that purpose.

I come from C++ originally, but now I would find it very painful to go back to the traditional object-oriented way of doing things. See also this.

Despite this, as @favba pointed out, there is nothing stopping you from storing function references as properties of structs, however I’d urge you to contemplate whether this is really what you want to do.

By the way, I may have been a little confused by exactly what you were looking to do, perhaps it was

V(x::Real) = x^2
V(k::koly) = V(k.x)

#5

Function is a valid type, so you can define V as V::Function


#6

That would only do it at construction time. It would be better to do call overloading here. For example, you can do:

(p::koly)() = p.x^2

and now the instances act as a function on the internal variable:

a = koly(2,1.0)
a() # Returns 4

It is, but it’s also an abstract type. You in some cases you may want to use a type-parameter instead in order to strictly type it:

type koly{F}
  x::Int16
  y::Float64
  V::F
end

without that it won’t inline well, and can be the root of some type inference issues. Another thing to look into is FunctionWrappers.jl.


#7

Oh, thanks for all the quick replies :slight_smile: I didn’t knew Function was a valid data type, so this solves my problem. Do I undertand that it does not matter if this Function returns Int, Float, an Array of Int’s etc…? Is this still performance-wise?
Thanks again,
Ferran.


#8

Calling functions by reference usually performs pretty well in Julia and does not require you to know the argument or return types, but there are some exceptions. I recently had a situation in which I needed to store a couple of hundred functions in an array, access them at random, and call them very quickly. In that case, Array{Function} was inadequate for my needs, however the previously mentioned FunctionWrappers.jl solved that problem quite nicely, at the cost of needing to specify the argument and return value types.


#9

Function is an abstract type. So for example Vector{Function} is like a Vector{Any}, or Vector{Integer}: Julia just can’t infer the results. This can lead to type-instabilities, depending on how you’re using it. FunctionWrappers.jl allows you to make this all better typed without having to make everything specific to each function. Parameterizing the type compiled a special version of the type dependent on the function you put in there.

There’s a tradeoff: every new function you pass the parameterized version into will need to recompile, but specializing can have performance benefits (with this example, x^2 will likely inline and avoid field access and function call costs). Pick your method depending on the problem.


#10

What if I want to store a bunch of functions in a field, e.g. in an NTuple?
edit: I guess that’s FunctionWrappers.


#11

Again, a tuple will explicitly type the whole thing, so that will work with a type parameter, but may be over-specializing. If you don’t need that amount of specialization, then use FunctionWrappers.jl (which is actually a staging repo for thing that likely will be in Base “soon”)


#12

Is that true with NTuples?


#13

An NTuple (by definition) contains homogeneous elements. Different functions are not the same type and you would therefore have the element type of the tuple would be Function which is not concrete so will not get specialization.


#14

Ok that’s what I thought. How can I see the effect of this?


#15

Dynamic dispatch when accessing the function stored in the tuple. Will be a performance hit if the function only does a little work.


#16

Is it possible to see this with e.g. @code_warntype?


#17

What I wrote before is actually wrong. NTuple or not does not matter so @ChrisRackauckas is right that the tuple will always specialize.


#18

Right now, I have something like this (simplified):

immutable Liouvillian{N} <: AbstractParameterizedFunction{true}
    fₙ::NTuple{N,Function}
end

It does seem like this would not be specialized.


#19

It will not be specialized in this sense:

julia> immutable F{N}
           fs::NTuple{N, Function}
       end

julia> f = F((x -> x, x->2*x))
F{2}((#1, #2))

julia> get_fs(f::F) = f.fs
get_fs (generic function with 1 method)

julia> @code_warntype get_fs(f)
Variables:
  #self#::#get_fs
  f::F{2}

Body:
  begin 
      return (Core.getfield)(f::F{2}, :fs)::Tuple{Function,Function}
  end::Tuple{Function,Function}

#20

Yes, that won’t specialize. But you can put a tuple in for:

immutable Liouvillian{F} <: AbstractParameterizedFunction{true}
    fₙ::F
end

and fₙ will specialize. You need to be really careful when doing this though. If fₙ is a tuple and A is the type, indexing with literals (A.fₙ[3]) is type-inferrable/stable. However, indexing with a variable (i = 3; A.fₙ[i]) is not. Julia is able to specifically handle indexing heterogeneous tuples with literals, but once the exact index is runtime information instead of compile-time information you lose the ability to infer.

julia> immutable F{N}
                  fs::N
              end

julia> f = F((x->x,x->2x))
F{Tuple{##1#3,##2#4}}((#1, #2))

julia> get_fs(f::F) = f.fs
get_fs (generic function with 1 method)

julia> @code_warntype get_fs(f)
Variables:
  #self#::#get_fs
  f::F{Tuple{##1#3,##2#4}}

Body:
  begin
      return (#1, #2)
  end::Tuple{##1#3,##2#4}

julia> get_fs2(f::F) = f.fs[2]
get_fs2 (generic function with 1 method)

julia> @code_warntype get_fs2(f)
Variables:
  #self#::#get_fs2
  f::F{Tuple{##1#3,##2#4}}

Body:
  begin
      return $(QuoteNode(#2))
  end::##2#4

julia> get_fsi(f::F,i::Int) = f.fs[i]
get_fsi (generic function with 1 method)

julia> @code_warntype get_fsi(f,2)
Variables:
  #self#::#get_fsi
  f::F{Tuple{##1#3,##2#4}}
  i::Int64

Body:
  begin
      return (Base.getfield)((#1, #2), i::Int64)::Union{##1#3, ##2#4}
  end::Union{##1#3, ##2#4}

This may seem like magic at first, but just think carefully about “what information does the compiler have, and what can it possibly infer?” Julia makes use of literals, but once you pass the integer it becomes runtime information, and there’s no way for the compiler to handle that.