Type alias or newtype wrapper?

Hi! I am writing a small from-scratch ray tracer to get myself acquainted with julia.

I’ve defined a color type as

struct Color
    r::UInt8
    g::UInt8
    b::UInt8
end

What would be the most idiomatic definition for the image type?

const Image = Array{Color, 2}

or

struct Image
  data::Array{Color, 2}
end

?

3 Likes

Either one could make sense, although if you go with the second, I’d go with

struct Image <: AbstractMatrix{Color}
  data::Matrix{Color}
end

And then implement the rest of the relevant AbstractArray interface (see the manual section on it) to get the most out of the type (iteration, broadcast, promotion, etc.).

Images.jl does not define an Image type (just operations on arrays of Colors), but that’s a perfectly valid approach! You can take a look at that ecosystem of packages for inspiration.

3 Likes

One thing to think about is whether you want to use Image for dispatch. If you really just want to treat image as a regular array, the first option is probably the way to go, but if you wanted to overload inv to invert the colors of the image for example, you should definitely go with the second option, because otherwise, you are commiting type piracy.

5 Likes

I wouldn’t say so. Since @matklad defined Color himself, extending Base functions to Matrix{Color} isn’t piracy.

I would still call it type piracy, since it still has a lot of the same problems:

  1. Functions written for Matrix assume a very specific meaning of inv in the linear algebra sense and may rely closely on the mathematical and programmatical properties of inv(::Matrix). So at first sight unrelated functions may now silently return nonsensical results.
  2. This is a bit technical, but if a function calls inv(::Matrix), type inferences tries its best to analyze inv and optimize as much as it can. Since the element type may sometimes only be partially inferred, the compiler may still look at all methods that could possibly get called (at least if there aren’t too many) and can try to specialize on that. If you are now adding another method to inv for a subtype of Matrix, it can break assumptions the compiler previously made about those methods that could get called, therefore previous specializations may not be valid anymore, so the function that called inv(::Matrix) needs to be recompiled once it is called again. These are called method invalidations and can cause large compiler latencies. (I probably missed some nuances here, you should really read @timholy’s great blog post: https://julialang.org/blog/2020/08/invalidations/)

While the point on invalidations is definitely an important and topical one (although not at all where I would start if I were familiarizing myself with the language), the one on extending inv in a nonsensical way, I think, is not as clear cut. For example, Images does exactly what you are advocating against, e.g. here. Obviously, if the purpose of the newly extended method doesn’t make sense with the function it’s extending, that’s bad design, but otherwise, I’d still say this is valid.

Another thing to think about is that Images.jl, together with other packages, already uses this kind of definition. It might be a good starting point to piggyback on that.

1 Like

That’s definitely type piracy to me as well, but I am not advocating against all uses of type piracy, I am only saying that you should be careful and consider the dangers. Even Base commits quite a bit of type piracy: https://github.com/JuliaLang/julia/pull/37404#discussion_r483803996.