Maintain backwards compatibility when using `const` on a mutable struct field

I searched, and it doesn’t look like anyone has asked this before, but I would be surprised if I am the first, so I apologize if this question is a dupe and I missed it.

In the newly released Julia v1.8.0, you can annotate a field of a mutable struct with const like so:

julia> mutable struct Smile
           const curvature::Float64
       end

In 1.6.7, this is a syntax error:

julia> mutable struct Smile
           const curvature::Float64
       end
ERROR: syntax: expected assignment after "const"
Stacktrace:
 [1] top-level scope
   @ none:1

Is there a way to define Smile so that if the version is < 1.8.0, it is just a struct, and if the version is ≥ 1.8.0, it is a mutable struct with const curvature?

Would it be wise to do so?


Note that the following fails in 1.6.7:

julia> if VERSION >= v"1.8.0"
           mutable struct Smile
               const curvature::Float64
           end
       else
           struct Smile
               curvature::Float64
           end
       end
ERROR: syntax: expected assignment after "const"
Stacktrace:
 [1] top-level scope
   @ none:1

To give a little more context, users of my package[1] are currently used to Smile being an (immutable) struct, and now I want to give them the option of mutating some (but not all) of the fields of Smile. But I also want to maintain compatibility with the LTS branch, with the understanding that the mutation functionality will only be available if you use v1.8.0 or higher.

So, I’m not trying to turn a mutable struct into a mutable struct with const fields, which would be a breaking change (because users of the package may have written code that mutates a field that is now going to become immutable). Rather, I’m trying to change a struct into a mutable struct.


  1. Me. ↩︎

Maybe try a @static if?

1.6.7:

julia> @static if VERSION >= v"1.8.0"
           mutable struct Smile
               const curvature::Float64
           end
       else
           struct Smile
               curvature::Float64
           end
       end
ERROR: syntax: expected assignment after "const"
Stacktrace:
 [1] top-level scope
   @ none:1

How about:

macro _const(expr)
 if VERSION >= v"1.8.0-"
   Expr(:const, esc(expr))
 else
   esc(expr)
 end
end

In v1.7:

julia> mutable struct Foo
         @_const x
       end

In v1.8:

julia> mutable struct Foo
         @_const x
       end

julia> isconst(Foo, :x)
true

Edit: sorry, I missed that you want a struct in v1.7 and below, rather than a mutable struct. So the answer here doesn’t quite work, but at least it shows how you can actually generate code with const in it only on v1.8.

2 Likes

Hmmm.

You could put the 1.6-compatible struct definition in one source code file, and put the 1.8-compatible struct definition in another file. And then your package would have code of the form:

if VERSION >= v"1.8"
    include("structs-1.8.jl")
else
    include("structs-1.6.jl")
end
3 Likes

@rdeits No worries, it’s clear how you could use the same approach on the mutable keyword.

@dilumaluthge This is an elegant solution, with the small downside that you have to repeat all the docstrings, inner constructor methods etc. in each file.

I wonder if there’s a way to have everything in the same file, though? I was surprised that the following doesn’t work, since the documentation says that you can use a begin ... end block with @eval:

if VERSION >= v"1.8.0"
    @eval begin
        mutable struct Smile
           const curvature::Float64
        end
    end
else
    @eval begin
        struct Smile
            curvature::Float64
        end
    end
end

This generates the same syntax error in 1.6.7.

Presumably, each file would only contain the functionality that is different between the versions, and the rest of the code would be in a common, third file.

The struct definition itself has to appear in each file, and therefore so does its docstring and inner constructor methods.

So you really did mean “small” downside :wink:

1 Like

The docstring doesn’t need to appear in each file, as you may add it to the struct name instead of the definition. As an example:

julia> begin
       """
           A
       Struct `A`, wraps an `Int`
       """
       A

       struct A
           x :: Int
       end
       end

help?> A
search: A Any any all abs ARGS ans axes atan asin asec any! all! acsc acot acos abs2 Array atanh atand asinh asind asech asecd ascii angle acsch acscd acoth

  A

  Struct A, wraps an Int

julia> A(2)
A(2)

Regarding the inner constructors, if these are identical across versions, perhaps you’ll need a third file that you include within each struct definition :stuck_out_tongue:

2 Likes

Haha, I’m a bit of a DRY maximalist (refer to first name)

Unfortunately, even this doesn’t seem to work completely, e.g in this PR, where tests pass on v1.6, but the coverage run fails with the same error. Is there a way to instruct the coverage action to ignore certain files on specific versions?