As a longtime Matlab user, someone who enjoys Python syntax, and loves crafting C codes for maximum efficiency, I often struggle with thinking about code interfaces in Julia.

Why? Because the interface possibilities in Julia are extremely powerful. This power can be difficult to deploy, however. Simply put, other languages have conventions or requirements that are essentially constraints on what interfaces ought to look like. These constraints restrict the design space, once they are understood, and simplify design. In contrast, Julia offers more possibilities.

Here are a few relevant thoughts.

  1. Matlab interfaces tended to be holistic functional interfaces that ‘do everything’. Although one might think these are hard to do, packing all the relevant information into a single function call is a useful design constraint that forces or suggest certain choices.

  2. Why isn’t C harder than Julia? The reason that C isn’t harder than Julia is because C interfaces essentially need to fix a small set of types to be reasonable.

    Aside C++ templated interfaces are essentially equivalent to many of the design challenges in Julia. Designing these right and Julia interfaces present similar challenges.

  3. Python promotes the class as a general structure to organize interfaces.

Consider what linear programming might look like in these different languages.

Matlab

optval, optarg = linear_program(c,A,b) # solve min c'*x s.t. Ax = b, x >= 0
optval, optarg, dualvars = linear_program(c,A,b) # and get dual variables too
optval, optarg, dualvars = linear_program(c,A,b,'method','ip','tol',1e-3)

C99 for single line comments

p = alloc_problem(c,A,b)    
s = alloc_solution(p) 
solve_lp(p, &s) # s has fields, optval, optarg, dualvars    
o = interior_point_options()
o.tol = 1e-3
solve_lp_interior_point(p, &s, o) // s has fields, optval, optarg, dualvars 
free(p); free(s); // very important in C

Python

p = opt.LinearProgram(c,A,b)
M = opt.algs.LinearProgramDefault()
soln = M.solve(p) # soln has optval, optarg
# get optval, optarg, dualvars from soln 
M2 = opt.algs.LinearProgramInteriorPoint({'tol':1e-3)}
soln = M2.solve(p)  

Notice that the Matlab interface is probably the easiest and simplest, if far more limited.

There are a number of common challenges that these interfaces have to deal with.

  • How to specify the problem
  • How to specify a solver
  • How to parametrize the solver
  • How to get solution pieces.

Matlab’s do everything solver makes this interface easy to write. It also enforces a design funnel that would channel alternatives into this uber-function. (Matlab loves the uber-function, it’s a great and timeless design!) The other two interfaces are largely similar, but for Python, where should the solve function be inside the class for the problem (P.solve(M) or M.solve(P)). (Probably M.solve(P) as then the solver can extract the relevant information it needs from the problem, whereas otherwise the problem has to fix an interface to a method.)

In C, it’s just a function-fest everywhere, and don’t forget to free.

Julia with Convex.jl

x = Variable()
problem = minimize(c'*x, [A*x == b, x >= 0])
solve!(problem, SCS.Optimizer)
problem.optval, x

Julia with JuMP

model = Model(SCS.Optimizer)
@variable(model, 0 <= x)    
@objective(model, Min, c'*x)
@constraint(model, A*x == b)
optimize!(model) 

Both of these interfaces exploit Julia’s powerful syntax transformations for all sorts of clever features that are largely impossible in C or Matlab. But back to Matlab’s advantages, I don’t want a super-general optimization tool, I just want to solve a simple LP.

Maybe I’m writing a package where I expect people to play around with various LP solvers, or I want to give them that option. How would I do this? This is where Julia design paralysis sets in. It’s definitely possible, but there are many possible ways to treat these issues and few conventions that would simplify or suggest the design.

So what’s my recommendation or advice? The simplest thing is to treat Julia like C and fix a small set of types that will work with the code. Then try and grow this into more general interfaces over time. This is essentially a restatement of Knuth’s “premature optimization dictum” https://en.wikiquote.org/wiki/Donald_Knuth

The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.

  • Don’t be afraid to hardcode Float64, Int, etc. and don’t feel obliged to be fully type general. (Type general functions are very hard to get right and are best done after a few tries with simpler hardcoded type functions.) This will have the side effect of making fewer calls to the Julia compiler when you call functions.

  • Avoid trying to use clever Julia tricks in initial designs. Keep it simple and straightforward like C.

  • Don’t provide flexibility where it isn’t (yet) useful.

  • Don’t be afraid to make horrible breaking changes in future code revisions. The Julia devs drove some of us nuts with constant breaking syntax changes through 0.2 - 0.7 as they evolved and revisited decisions. Designing without reality is impossible.

Back to Knuth though, with a variant with yet more relevant context.

Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.

The goal of these initial design ideas is to find that 3%. That can only be done once an initial framework is established.

And too close, let’s go back to Knuth.

Science is knowledge which we understand so well that we can teach it to a computer; and if we don’t fully understand something, it is an art to deal with it.

This exemplifies good Julia design, and why it is so hard. Good Julia design flows from a deep understanding of the topic and an appreciation of where similarities and differences lie that is often a contribution in itself.