I have a package which reproduces equations from a professional standard that is revised every two years. I need to be able to access specific editions of the package which are linked to specific editions of the standard document e.g. 2025 Edition. What is the best way to smoothly achieve that?
I used to make a new package with the year in the package name, but that was a lot of work, and realistically, the content of the standard doesn’t change much, if at all, between editions. I’m currently considering two new options (but am open to others).
Keep persistent git branches e.g. 2025.
Hijack the release version number to use year as the “major” digit e.g. 2025.0.0.
Which method do you think would cause the least friction for my internal users?
(I intend to register this package with LocalRegistry.jl using a local file server for others in my company, but I am still setting that up.)
I think the best approach is to create a single package where each version is a module (aka Version.jl). You move all the commonalities into a single top module, then have dedicated sub modules for the version specific details. That also makes it really clear to users what changes from one to the next version. It also avoids clashing definitions and gives you a clear namespace for each specific version.
That has nothing to do with major versions. You can only load one version of a given package in the same process, no matter in what way the version numbers differ.
I don’t think I like this idea because it forces me to change all my downstream function calls in the packages using this package e.g. from Div3.2023.KM6.f(x) to Div3.2025.KM6.f(x). I would rather load a specific version of Div3.jl into an environment and have the function definitions update in place when called by a downstream package there e.g. as Div3.KM6.f(x). I have tried to reproduce the source text as closely as possible, giving the equations the same names in the same order, so I am reluctant to scramble them into different common or edition-specific modules.
Hmm, the downside I see then is ever-expanding duplicate source code. I wonder if I should flip my question. Why might I not want to implement editions via each method?
If you use proper abstraction you should be able to avoid this. Put the common parts into functions shared across modules, but dispatch to version specifics where you need to.
Many reasons why this is a bad idea… for one, it does not avoid your code duplication concern. I think it makes it worse, or even unavoidable. Github code search only indexes the main branch, see About GitHub Code Search - GitHub Docs. For N versions you’ll need N PRs to fix anything etc etc.
julia> module Div3
module Impl
"f, introduced 2021"
f_2021(x) = x^2
"f, revised 2025"
f_2025(x) = x^3
g_2021(x) = x/2
g_2023(x) = x/3
end
module Ed2021
import ..Impl
const f = Impl.f_2021
const g = Impl.g_2021
end
module Ed2023
import ..Impl
const f = Impl.f_2021
const g = Impl.g_2023
end
module Ed2025
import ..Impl
const f = Impl.f_2025
const g = Impl.g_2023
end
end
Main.Div3
then you can do:
julia> using .Div3.
Ed2021
Ed2023
Ed2025
Impl
julia> using .Div3.Ed2023:
eval
f
g
include
julia> using .Div3.Ed2023: f, g
julia> f(2)
4
julia> g(2)
0.6666666666666666
And maybe even cleaner. Something based on singletons like
abstract type Edition end
struct Ed2021 <: Edition end
struct Ed2025 <: Edition end
h(::Ed2021, x) = 1.5x
h(::Ed2025, x) = 1.6x
This one can be combined with Preferences.jl to set a global version. Many possible flavours of this recipe
A potential complication with the module method is that I am using Pluto notebooks as source code to get math rendering for function definitions. I’m not sure if I would be able to still render the functions as math if I pull their definition from other modules/notebooks. Maybe a special type that stores the function with its LaTeXString …
module Div3
module KM6
include("KM6.plutojl")
end # module KM6
module KD2
include("KD2.plutojl")
end # module KD2
module KD4
include("KD4.plutojl")
end # module KD4
end # module Div3
You would never be able to use functions from two different versions within the same environment. So that rules out comparisons of any kind for all your users. Besides it just abusing a system which it was never intended for in the first place. I doubt such a hack would even be accepted in the general registry. Now that I think of it how would even register a package which is spread across multiple releases?
I’d expect that a lot of final deliverables could profit from a low-effort way of including analyses according to multiple (all!) standard versions.
E.g. “this boiler is happy up to 85.2C in 1998-2013, up to 81.5C in 2014-2021, and up to 80.0C in 2022-2025”.
User then has the relevant information to know whether to fight over the relevant date (…some of the work/signatures happened in '21, some got delayed into '22. It’s always complicated, and sometimes up to interpretation).
Poweruser who needs to develop and maintain intuitions over all the editions and differences between them is empowered to do so.
I would recommend to go with versioning rather than separate branches or package names. Using something like 2025.x.y, 2027.x.y, etc makes it obvious which standard a release matches while still letting you publish bug fixes for each edition. Its also easier for users to pin a specific version through the package manager than having to switch branches.