Strategy help

I’d looking for some input on the best way to tackle this.

Right now I have a struct that looks something like this:

mutable struct Line 
    win::Window
    startPoint::Vector{Int64}
    endPoint::Vector{Int64}
    lineColor::Vector{Int64}            
    #----------
    function Line(  win::Window,
                    startPoint::Vector{Int64} = [0,0],
                    endPoint::Vector{Int64} = [10,10];
                    width::Int64 = 1,
                    lineColor::Vector{Int64} = fill(128, (4)),               
			)
 
        new(win, 
            startPoint,
            endPoint,
            width,
            lineColor
            )

    end
end

There is more to it than that: I pulled out superfluous code to make it easier to read.

Right now lineColor is a vector of RGBA values from 0-255 (I’m using SDL). I’d like to make it more flexible, so that it can take vectors of floating point numbers from 0.0 to 1.0, or even the word “red”, and automatically translate the value passed to it into an RGBA Int vector based on its type (maybe _lineColor as an ‘hidden’ variable used internally for setting the final color).

I’m not sure of the right way, or best way, to tackle this. Should I replace lineColor::Vector{Int64} with lineColor::{Any}? Is there a way to do multiple dispatch or overload the inner constructor, so that I have one for lineColor::Vector{Int64}, one for lineColor::Vector{Float64}, and one for lineColor::String?

How would you tackle this?

Make linecolor a Color from Colors.jl.

https://juliagraphics.github.io/Colors.jl/

I think you can also make the struct immutable, as the Vector properties can be modified, appended to, and manipulated even in a immutable struct.

1 Like

I used Colors.jl for another project a few months ago, and I found it to be not the most user friendly experience. After looking back through their documentation, it’s non-obvious how I would use Colors.jl.

My ideal is that the user (other programers) would not have to worry about color type conversion: they could simply pass a vector of ints, floats, or a string and the code would automagically translate it to SDL’s RGBA255 color format. That part is easy. It’s the big picture structuring of the problem that I’m struggling with. It’s more of a language thing: I know how I would solve the problem in C++, but not in Julia.

Dan: what would immutable get me?

Do you want a line to have a single RGBA color? You can make a field

julia> struct Foo
           color::RGBA{Float64}
       end

julia> Foo(RGBA(.1, .2, .3, .4))
Foo(RGBA{Float64}(0.1,0.2,0.3,0.4))

julia> Foo(colorant"orange")
Foo(RGBA{Float64}(1.0,0.6470588235294118,0.0,1.0))

julia> Foo(s::AbstractString) = Foo(parse(Colorant, s))
Foo

julia> Foo("yellow")
Foo(RGBA{Float64}(1.0,1.0,0.0,1.0))

You can make more constructors if you like. Check out the Construction and Conversion page.

1 Like

immutable values are easier for the compiler to reason about, and therefore optimize for. Specifically, immutable values can often reside in processor registers avoiding the back and forth to memory (which is a huge deal).

1 Like

I think we are off track here: conversion is trivially simple, so that is a non-issue, and I can do that without Colors.jl.

What I’m really asking is how to structure things. Should I replace lineColor::Vector{Int64} with lineColor::{Any} ? Is there a way to do multiple dispatch or overload the inner constructor, so that I have one for lineColor::Vector{Int64} , one for lineColor::Vector{Float64} , and one for lineColor::String ?

No, you shouldn’t store the color as a string. You should store the color as a single canonical color type, and convert any argument to that type in the construction function.

2 Likes

That is what I was planning on doing.

OK, forget color completely.

Let’s say it is coordinate systems. If the struct constructor receives a vector of Ints, then the coordinates are in pixels, else if it is a vector of Floats, then the coordinates are in percentage of screen height (which is then converted into pixels held in another variable).

How would you structure things? Is it possible to have two constructors, one expecting vector of Ints and another expecting a vector of floats? Or should I specify the coordinates as a vector::{Any} and have the [single] constructor determine what to do based on the coordinates type?

If you want maximum flexibility use type parameters for each field.

mutable struct Line{S,E,C}
    win::Window
    startPoint::S
    endPoint::E
    width::Int64
    lineColor::C
end

You can then create outer constructors as needed.

function Line(
    win::Window,
    startPoint::S = [0,0],
    endPoint::E = [10,10],
    width = 1,
    lineColor::C = fill(128, 4)
) where {S,E,C}
    Line{S,E,C}(
        win,
        startPoint,
        endPoint,
        width,
        lineColor,
    )
end

Demonstration:

julia> struct Window end

julia> mutable struct Line{S,E,C}
           win::Window
           startPoint::S
           endPoint::E
           width::Int64
           lineColor::C
       end

julia> function Line(
           win::Window,
           startPoint::S = [0,0],
           endPoint::E = [10,10],
           width = 1,
           lineColor::C = fill(128, 4)
       ) where {S,E,C}
           Line{S,E,C}(
               win,
               startPoint,
               endPoint,
               width,
               lineColor,
           )
       end
Line

julia> Line(Window())
Line{Vector{Int64}, Vector{Int64}, Vector{Int64}}(Window(), [0, 0], [10, 10], 1, [128, 128, 128, 128])

julia> Line(Window(), (0,0), (1.0, 1.0), 0x5, fill(128,4))
Line{Tuple{Int64, Int64}, Tuple{Float64, Float64}, Vector{Int64}}(Window(), (0, 0), (1.0, 1.0), 5, [128, 128, 128, 128])

julia> Line(Window(), (0,0), (1.0, 1.0), 0x5, :yellow)
Line{Tuple{Int64, Int64}, Tuple{Float64, Float64}, Symbol}(Window(), (0, 0), (1.0, 1.0), 5, :yellow)

You can offer a more specific constructor that will convert floats to integer pixel values.

function Line(
    win::Window,
    startPoint::S,
    endPoint::E,
    width::Float64 = 0.5,
    lineColor::C = fill(128, 4)
) where {
    S <: AbstractVector{<: AbstractFloat},
    E <: AbstractVector{<: AbstractFloat},
    C
}
    screenSize = getScreenSize()
    startPoint = map(screenSize, startPoint) do s, percent
        round(Int64, s*percent)
    end
    endPoint = map(screenSize, endPoint) do s, percent
        round(Int64, s*percent)
    end
    width = round(Int, screenSize[1] * width)
    return Line(win, startPoint, endPoint, width, lineColor)
end
julia> getScreenSize() = (1024, 768)
getScreenSize (generic function with 1 method)

julia> function Line(
           win::Window,
           startPoint::S,
           endPoint::E,
           width::Float64 = 0.5,
           lineColor::C = fill(128, 4)
       ) where {
           S <: AbstractVector{<: AbstractFloat},
           E <: AbstractVector{<: AbstractFloat},
           C
       }
           screenSize = getScreenSize()
           startPoint = map(screenSize, startPoint) do s, percent
               round(Int64, s*percent)
           end
           endPoint = map(screenSize, endPoint) do s, percent
               round(Int64, s*percent)
           end
           width = round(Int, screenSize[1] * width)
           return Line(win, startPoint, endPoint, width, lineColor)
       end
Line

julia> Line(Window(), [0.3,0.4], [0.5,0.6])
Line{Vector{Int64}, Vector{Int64}, Vector{Int64}}(Window(), [307, 307], [512, 461], 512, [128, 128, 128, 128])

Thanks Mkiti. I feel like a dummy, but what does the {S,E,C} do in mutable struct Line{S,E,C} ?

Those are type parameters.

https://docs.julialang.org/en/v1/manual/types/#Parametric-Types

Basically we have a Line type that has a startPoint of type S, endPoint of type E and a lineColor of type C.

Thus we could have a Line{Vector{Int64}, Vector{Int64}, Vector{Int64}} where each of S, E, and C are all Vector{Int64}.

julia> default_line = Line(Window())
Line{Vector{Int64}, Vector{Int64}, Vector{Int64}}(Window(), [0, 0], [10, 10], 1, [128, 128, 128, 128])

julia> typeof(default_line)
Line{Vector{Int64}, Vector{Int64}, Vector{Int64}}

julia> fieldnames(typeof(default_line))
(:win, :startPoint, :endPoint, :width, :lineColor)

julia> fieldtypes(typeof(default_line))
(Window, Vector{Int64}, Vector{Int64}, Int64, Vector{Int64})

We could bring in some other packages such as GeometryBasics and Colors. We could then use the types GeometryBasics.Point3 and Colors.RGB with Line.

julia> using GeometryBasics, Colors

julia> other_line = Line(Window(), Point3(1,2,3), Point3(4,5,6), 1, Colors.RGB(1,0,0))
Line{Point3{Int64}, Point3{Int64}, RGB{FixedPointNumbers.N0f8}}(Window(), [1, 2, 3], [4, 5, 6], 1, RGB{N0f8}(1.0,0.0,0.0))

julia> other_line_type = typeof(other_line)
Line{Point3{Int64}, Point3{Int64}, RGB{FixedPointNumbers.N0f8}}

julia> fieldnames(other_line_type)
(:win, :startPoint, :endPoint, :width, :lineColor)

julia> fieldtypes(other_line_type)
(Window, Point3{Int64}, Point3{Int64}, Int64, RGB{FixedPointNumbers.N0f8})

Here S and E are Point3{Int64} and C is RGB{FixedPointNumbers.N0f8}.

You can, of course, use type parameters, as demonstrated above, but both in the case of colors and coordinates it is, arguably, better to decide on a canonical representation (such as RGBA) and then use the constructors to convert different inputs to that representation. For some reason, though, you seem resistant to that approach.

No, avoid this unless absolutely necessary.

Using Vector for any of these seems like a suboptimal approach. I would use a data structure that enforces the correct format, such as Tuples and a Color type. There is nothing in the type of Vector that guarantees length, for example. Vectors are also bad for memory and performance.

3 Likes

I think the simplest thing here is the one you initially tought: get the field colour in the struct as you think is better for how your struct will work (for example a tuple of 3 integers), create a construcor that accepts exactly that “format”, and then create (override) the constructors for accepting string, integers, floats or whatever you think it is convenient for the user, and for each you “convert” to the “format” you defined in the struct in the constructor…

2 Likes

I’m really confused by this thread, and in particular by the last response. Your other posts seem to contradict that you were planning to do this, but if you mean this, then your question is answered, isn’t it?

And the solution has nothing to do with colors, but exactly addresses general strategy: decide on a definitive/canonical representation, then convert other input formats in the constructor(s). If you need input on how to do conversion, we will be happy to help, but you also said

What’s missing for a full solution?

1 Like

Are you looking for something like

mutable struct Line 
    win::Window
    startPoint::Vector{Int64}
    endPoint::Vector{Int64}
    width::Int64
    lineColor::Vector{Int64}            
    #----------
    function Line(  win::Window,
                    startPoint::Vector{Int64} = [0,0],
                    endPoint::Vector{Int64} = [10,10];
                    width::Int64 = 1,
                    lineColor::Vector{Int64} = fill(128, (4)),               
			)
 
        new(win, 
            startPoint,
            endPoint,
            width,
            lineColor
            )
    end

    function Line(  win::Window,
                    startPoint::Vector{Int64} = [0,0],
                    endPoint::Vector{Int64} = [10,10];
                    width::Int64 = 1,
                    lineColor::Vector{Float64},               
			)
        @assert all(0 .<= lineColor .<= 1) 
        new(win, 
            startPoint,
            endPoint,
            width,
            round.(Int64, lineColor .* 255))
            )
    end

end

or

mutable struct Line 
    win::Window
    startPoint::Vector{Int64}
    endPoint::Vector{Int64}
    width::Int64
    lineColor::Vector{Int64}            
    #----------
    function Line(  win::Window,
                    startPoint::Vector{Int64} = [0,0],
                    endPoint::Vector{Int64} = [10,10];
                    width::Int64 = 1,
                    lineColor::Union{AbstractString, AbstractVector{<:Real}} = fill(127, 4),               
			)
        if lineColor isa AbstractString
            error("Not yet implemented")
        elseif !(Integer in supertypes(eltype(lineColor)))
            @assert all(0 .<= lineColor .<= 1) 
            lineColor = round.(Int64, lineColor .* 255))
        end
        new(win, 
            startPoint,
            endPoint,
            width,
            lineColor
            )
    end

end

This is what I ended up doing for the two types of coordinates (pixels vs percentage of screen height). Pixels are Int64 and percentages are Float64.

mutable struct Line2    
    win::Window
    startPoint::Vector{Real}
    endPoint::Vector{Real}
    lineColor::Vector{Int64}           
    _startPoint::Vector{Int64}
    _endPoint::Vector{Int64}
    #----------	Pixel Coordinates
    function Line2( win::Window,
                    startPoint::Vector{Int64} = [0,0],
                    endPoint::Vector{Int64} = [10,10];
                    width::Int64 = 1,
                    lineColor::Vector{Int64} = fill(128, (4)),             
                    _startPoint::Vector{Int64} = [0,0],
                    _endPoint::Vector{Int64} = [10,10]
            )

        if length(endPoint) != 2
            message = "endPoint needs two coordinates, got " * String(length(endPoint)) * " instead."
            error(message)
        end
        if length(startPoint) != 2
            message = "startPoint needs two coordinates, got " * String(length(startPoint)) * " instead."
            error(message)
        end     
        new(win, 
            startPoint,
            endPoint,
            width,
            convert(Vector{Int64},lineColor),
            startPoint,
            endPoint
            )

    end
    #----------	Percentage of screen height coordinates
    function Line2( win::Window,
                    startPoint::Vector{Float64} = [0.1,0.1],
                    endPoint::Vector{Float64} = [0.2,0.2];
                    width::Int64 = 1,
                    lineColor::Vector{Int64} = fill(128, (4)),              
                    _startPoint::Vector{Int64} = [0,0],
                    _endPoint::Vector{Int64} = [10,10]
                )

        if length(endPoint) != 2
            message = "endPoint needs two coordinates, got " * String(length(endPoint)) * " instead."
            error(message)
        end
        if length(startPoint) != 2
            message = "startPoint needs two coordinates, got " * String(length(startPoint)) * " instead."
            error(message)
        end     
        _, displayHeight = getSize(win)

        _startPoint[1] = round(Int64, startPoint[1] * displayHeight)        # convert percentage to pixels
        _startPoint[2] = round(Int64, startPoint[2] * displayHeight)
        _endPoint[1] = round(Int64, endPoint[1] * displayHeight)
        _endPoint[2] = round(Int64, endPoint[2] * displayHeight)

        new(win, 
            startPoint,
            endPoint,
            width,
            convert(Vector{Int64},lineColor),
            _startPoint,
            _endPoint
            )

    end

end

For the coordinates, would it make more sense to replace ::Vector{Type} with ::MVector{2, Type} ?

GHTaarn: yes, something like that.

On the otherhand, for color, what if I typed lineColor as Any, and called an external function to translate it (string or float colors) to an internal variable _lineColor for drawing?

lineColor::Any              # what the usesr passes: string, or vectors of ints or floats
_lineColor::Vector{Int64}   # internal SDL-style RGBA255 color

or does it make more sense to use a Union*

lineColor::Union{String, MVector{4, Int64}, MVector{4, Float64}}               # what the usesr passes: string, or vectors of ints or floats
_lineColor::Vector{Int64}   # internal SDL-style RGBA255 color
  • which I lasted used in C/C++ 20 years ago…