Dan Greenwald
Dan Greenwald

Reputation: 33

Writing clean Julia code with many subcases without member functions

I am trying out the Julia language (coming from a Python background) and am interested in the most natural way to tackle an issue I would have solved using objects in Python.

Basically, I am trying to program a function that will evaluate some simple 1-d mathematical bases and then combine them to approximate a multidimensional function. In Python I would have written something like

basis_1d_values = scheme_1d.evaluate(points)

for each of my scheme_1d objects. I would use a parent Scheme1d class, and then have subclasses for the various types of 1-d schemes (linear, Chebyshev polynomial, etc.) that would know how to run their separate "evaluate" functions.

What is the most natural way to do this in Julia? Obviously a long if statement like

if scheme_1d_type == "linear"
    basis_1d_values = evaluate_linear(scheme_1d, points)
elif scheme_1d_type == "chebyshev"
    basis_1d_values = evaluate_chebyshev(scheme_1d, points)
elif ...
...
end

will work but it is very clunky since I will need to use these branching if statements and separate functions every time the subclasses behave differently (and will have to keep track of every if statement when I update the code in some way). Any suggestions would be much appreciated.

Thank you!

Upvotes: 3

Views: 177

Answers (2)

BatWannaBe
BatWannaBe

Reputation: 4510

You're talking about multiple dispatch and subtyping, which are very fundamental and peculiar to Julia and would be covered in any introduction tutorial. I'm not going to explain it all because they do a better job, but since you're coming from Python specifically , I can point out basic analogies.

Python can only dispatch (selecting a method) on 1 object so you make it its own type (class) that holds its own version of evaluate, even write it before the dot during a function call, just to point out how special it is.

class Scheme1D:
   pass

class Linear(Scheme1D):
   def evaluate(self, points):
      return "linear"

class Chebyshev(Scheme1D):
   def evaluate(self, points):
      return "chebyshev"

points = None
scheme_1d = Linear()

scheme_1d.evaluate(points)
# Python checks for type(scheme_1d) to find the correct evaluate
# and runs its bytecode (which was compiled upon class definition)

Julia dispatches on both scheme_1d and points (hence multiple dispatch), so evaluate wouldn't belong to any type. Instead, it is 1 function with multiple methods, each method distinguished by its combined input types.

abstract type Scheme1D end

struct Linear <: Scheme1D
end

struct Chebyshev <: Scheme1D
end

# first method definition also creates the function
function evaluate(x::Linear, points)
    return "linear"
end

# adds a method to the function
function evaluate(x::Chebyshev, points)
    return "chebyshev"
end

points = nothing
scheme_1d = Linear()

evaluate(scheme_1d, points)
# Julia checks the types of scheme_1d and points, finds the
# most fitting method, compiles the method for those types
# if it is the first call, and runs the method

This should help you adjust to Julia, but you should really look up tutorials for the finer details. Just a couple more things that'll be helpful to keep in mind when you're learning:

  1. The other big difference here is that in Julia, supertypes are abstract, you can't make an instance out of them. The types you can make instances from are called "concrete".
  2. Julia's compilation for specific concrete input types is called specialization. This is not the same as method dispatch, but happens after it. This means that you can write a method with abstract arguments (in the above example, points was never specified a type so it is implicitly given the universal abstract type Any), and Julia will make multiple specializations for every fitting concrete type.
  3. Specializations seems to be intended as an unseen implementation detail, but I think it's useful for understanding how Julia's compilation works, so I'll tell you that the methodinstances function of the MethodAnalysis package is the current go-to way of seeing these.

Upvotes: 5

Antonello
Antonello

Reputation: 6423

Not sure I understood all, but it wouldn't much different than in Python: you may use an abstract type Scheme_1d and then create the specific linear, chebyshev, etc schemes as subtypes.

Then you would have a single evaluate_scheme(scheme,points) function that dispatch based on the type of the scheme (refer to the documentation about "multiple dispatch").

Upvotes: 0

Related Questions