Naming of singleton types and their instance

Based on the discussion in the above thread I am refactoring my code to dispatch on instances instead of types (which also helps me to avoid some illegal stuff I was doing, e.g. overloading Base.show for types). But now I have a (silly) naming problem…

Let’s assume I have a singleton type representing the planet Mars:

struct Mars end

Now I have a function that returns its radius. In the past I would have written it like this

radius(::Type{Mars}) = 3389.5

which makes a nice looking call possible

radius(Mars)

If I want to use the instance instead this becomes

radius(::Mars) = 3389.5
radius(Mars()) # ugly

Not only do I think that the empty parens are ugly but also a potential source of errors for newcomers, e.g.

julia> radius(Mars)
ERROR: MethodError: no method matching radius(::Type{Mars})

Possible solutions:

  1. Do it like Base
struct Mars end
const mars = Mars()

This is not really satisfying because Mars is a proper noun and should start with a capital letter.

  1. Add a *Type suffix
# Ugly name for the type
struct MarsType end
# Nice name for the instance
const Mars = MarsType()
# Hide the ugly type name from the user
Base.show(io::IO, ::MarsType) = print(io, "Mars")
# Ugly code
radius(::MarsType) = 3389.5
# Nice call
radius(Mars)

Also not really satisfying because I have to type MarsType a lot and it feels like an implementation detail leaking out.

Any other ideas?

3 Likes

The Julia convention is have instance-bindings in lower-case, irrespective of proper English spelling, eg einstein = Physicist(name="Einstein") would be correct. I think it would be fine to do the const mars = Mars() and then only export mars.

3 Likes

Also, some recommend uppercase constants, so const MARS = ... would be OK too.

2 Likes

Thanks! It seems the only problems here are my aesthetic sensibilities :joy:

How would you handle the following case?

# 1
struct UTC <: TimeScale end
const utc = UTC() 

# 2
struct Utc <: TimeScale end
const UTC = Utc()

# 3
struct UTCScale <: TimeScale end
const UTC = UTCScale()
Base.show(io::IO, ::UTCScale) = print(io, "UTC")

I would not use 2 for acronyms, they look better in all caps. Apart from that, both 1 and 3 are fine.

Not everyone makes constants all caps though, not even in Base, what I linked is just one of the recommendations.

I generally try not to worry about these things in the first pass too much, if a patter emerges when I refactor then I make things consistent.

Hi,

For the planet case I would do it slightly different:

abstract type AbstractPlanet end

struct Planet <: AbstractPlanet
    radius :: Float64
    mass :: Float64
end

Now Mars is just an instance of Planet:

Mars = Planet(3389.5 , 642e21)

Since every planet has a mass and radius, you can now dispatch on AbstractPlanet:

mass(planet::AbstractPlanet) = planet.mass
radius(planet::AbstractPlanet) = planet.radius

Then you get:

julia> radius(Mars)
3389.5 

This does not answer the question which naming convention is better, but personally this is my preferred way to organize such code.
Like this you can dissociate the instance name from the struct’s name, and avoids creating a lot of singleton types.

For the timescale I would do something similar.
Hope this helps.

1 Like

Thanks for the suggestion, but Mars being a singleton is non-negotiable for my implementation. I need this to be able to dispatch on the type like in this simplified example:

# I have a state vector type that has the central body as a type parameter
struct State{T} end

function State{S}(s::State{T}) where {S<:Planet, T<:Planet}
    # do the transformation
end

mars_state = State{Mars}(...)
# Magically transform the state from Mars-centered to Earth-centered
earth_state = State{Earth}(mars_state)

And I also like the fact that there cannot be competing instances of Mars this way :wink:

1 Like

Until some time ago I’d have agreed with you and this is how planets are defined in AstroLib.jl. However, I see the point of Helgee’s approach: if you want to add a property to a planet you don’t have to change the data structure and planet instances, but just define a new method, perhaps not necessary for all planets.

@helgee: we should probably have an AstroBase.jl package where to move this stuff and avoid code/numbers duplication :slight_smile:

2 Likes

:+1: :100:

Yeah, I totally agree this is a powerful way to extend behaviour and I wouldn’t know how to do this in general with the “struct Planet” approach.

On the other hand, if I want to add behaviour, I find it quite easy to add a field to the Planet struct and then write an outer constructor such that the old code still works. I think this was explained in the discussion linked by Helgee.

GitHub - mauro3/Parameters.jl: Types with default field values, keyword constructors and (un-)pack macros often can help with that by allowing to specify a default for a newly added field.

2 Likes