Function reusing in Julia without inheritance

Suppose I have

struct Var


function square(x::Float64)
    return x^2

To define a function for squaring a Var and returning a Var, I could write

function square(var::Var)
    y = square(var.x)
    return Var(y)

However, if I have many other functions like square (cube, square_root, etc), how can I avoid rewriting the function for every function that uses Float64 as an input? I could think of a way in an OOP language by inheriting from an abstract class with undefined function, but not sure how to do this in Julia.

1 Like

In a rather similar way - define an abstract supertype.

abstract type MyVars end

struct Var <: MyVars
    x:: Float64

square(var:: MyVars) = square(var.x)

I’m trying to figure out a way that doesn’t involve rewriting the same things.
Code generation might be the one I am looking for:

op = (:square, :cube)
for p in op
    @eval $p(var::Var) = Var($p(var.x))

If a function limits itself to Float64, it either

  • really only works on Float64 (because it manipulates them in a way that usually only works on Float64 specifically, like some bit-level manipulation) or
  • it’s limited unnaturally to Float64, even though it doesn’t require Float64 per se and rather depends on some other contract.

In the case you posted, square doesn’t actually care about Float64 at all - it only cares about having a pow method that takes as it’s first argument something and as its second argument an Integer. So in this case, square is erronously limited to Float64.

I’d solve this by

  1. not limiting my methods arbitrarily
  2. implementing ^ (and whatever else you may need, I think there are some packages that can do this kind of forwarding for wrapper types almost automatically)

Then, square(x) with typeof(x) === Var just works™. Moreover, because julia compiles specialized methods for each combination of input argument types, this will make your code much more composable and still keep performance. For example, not limiting your methods to ::Float64 would allow something like DifferentialEquations.jl to automatically take advantage of your code, by passing in a Dual number (which behaves like a float, i.e. has the same kinds of methods like ^ and * etc. implemented, but isn’t just a wrapper around Float64) to do automatic differentiation, enable execution tracing and a number of other things.

May I ask what kind of OOP you’re referring to? If you’re thinking Java-style where you’re inheriting not only behavior but also struct fields, you’re going to have a bad time trying to replicate it in julia. Method level type assertions on arguments should in my opinion only be used to distinguish different implementations in dispatch, not for limiting incoming arguments (there is no performance difference between a typed and untyped argument in a method, except in very, very rare cases you’re unlikely to run into).