API design question: asking the user for methods and types

I am in the process polishing up a package API before registering. The API works with “models” (arbitrary structures, the user defines them), and requires that certain transformations are available for each model (eg “simulate this model with the given parameters”).

The current API requires that

  1. the user defines a type (eg struct) for a new model type,
  2. implements 4 methods that dispatch on this type.

Eg minimal (stylized, non-working) example of how one would this style:

using ThePackage
import ThePackage: simulate

struct MyModel
    ...
end

simulate(m::MyModel, params) = ...

This works fine. But am slightly uneasy about an API that requires that the user defines methods for functions.

Instead, I could ask for the model type and a bunch of closures that define these transformations. But I expect that the user would define the methods (possibly under some arbitrary function) anyway, as they are easier to program that way.

This is a style question, suggestions and existing examples of packages that do something similar would be appreciated. It may turn out that the style is totally idiomatic and I am worrying about nothing, and requiring the users to define types and methods for some API is idiomatic.

I think that’s absolutely fine. This is e.g. also how the iterator and AbstractArray interfaces work: https://docs.julialang.org/en/stable/manual/interfaces/#.

Sure, but those are meant for programmers who implement new types. The question is if it is reasonable to require this of users.

I think this API is fine. Although if it is possible, I would prefer an interface without requiring a type-def.

One thing I would add:

struct MyModel <: ThePackage.AbstractModel
...

The question is if it is reasonable to require this of users.

The answer clearly depends on your target group. Since this seems to be econ/econometrics, I’ll be bold and offer my thoughts: if you aim for tech-savvy PhD students, then it’s fine. However, it’s probably too much for MSc students and professors…
/Paul S

1 Like

I agree that you shouldn’t hesitate to do this.

One of the nicest things about Julia is how well this API pattern works. If it’s a question of organization, you might consider putting the methods that the user needs to overload in a sub-module. I recently did that and am pretty pleased with the result. Note also that for any methods that you don’t want a default definition of, but you intend for the user to define, you can still include them for overloading by doing

function funcname end
1 Like

I don’t see that defining a closure is much easier and also this introduces the possibility that the user refers to non-constant global variables.

I would also add a full example in the documentation which can be copy-pasted as a starting point.

2 Likes

If the result is public, can you please provide a link?

NamedTuples should help with that, but I think that the cleanest thing to do is to define a type for a model; makes code organization so much easier. The functions need something for dispatch.

That’s what I fear, too. Yet this is a very useful API, and as @saschatimme suggested, probably preferable to closures (and I don’t see a third option). So perhaps examples are the way to go; those I meant to do anyway. I hope that in due time, this kind of API becomes commonplace enough.

I’d be happy to. You can see the implementation in this file and a little documentation here. Note that this particular interface is very much intended “for other developers” and not so much “for users”, but I endeavored to make it as simple as possible, so the overall design of the interface itself is something I’d probably be perfectly comfortable exposing “to users”, just not for this particular application.

A nice but much more complicated (on the back-end, but perhaps more user-friendly) example of a similar interface that I’ve made use of can be seen in the POMDPs set of packages.

1 Like

I’d also draw a distinction between “confusing” and “unfamiliar”. This interface may be very unfamiliar to some users, so at first they may be taken aback, but if you design it well enough it probably won’t take long for even those users to get comfortable with it and prefer it to the alternatives. Closures in my view would be more likely to be “confusing”, so users might be perfectly comfortable with it at first, but ultimately get confused or frustrated when they dig into their applications.

I find that most beginners are able to perform pattern matching based on a well designed set of examples, so an API that requires users to define types should be perfectly okay with some simple examples to build from.

1 Like

As more of an end user for this kind of thing I have to agree that this is likely unfamiliar versus truly confusing. I have started using your DynamicHMC package as well, and the examples on how you use a stuct to build your models really led to an eureka moment for me, so I would rather have to learn to do this in a novel / nice way, as I see it having an impact on my own code beyond just using your packages for a particular problem.

4 Likes

Thanks for the links, now everything is clear and I am convinced that this is (or will become) an idiomatic API for packages. (Also, POMDPs is probably some kind of gold standard when it comes to documentation.)

I will also consider putting the functions in a submodule, for use with

import MyPkg.Interface: *

which I guess is still valid even after the removal of importall.

1 Like

Just to add one more example (perhaps the “classic” example): DataStreams.jl. Granted this is a far more complicated example, but it is the way it is for good reason. A while ago (prior to the most recent overhaul), I had said to @quinnj, “This is so confusing, why is this so confusing?”. But then I tried working on a PR to make it “less confusing” and quickly realized that I was being naive, and that @quinnj had very good reasons for writing that package the way he did. That’s an extremely important package in the data ecosystem, by the way.

2 Likes