Programming error or language understanding error?

I either have a bug in my code, or a misunderstanding of the Julia language.

I have two struct types: an Ellipse and a Circle. In order to simplify my code, I have a single drawing function that draws an ellipse. Since the function expects an ellipse, the circle has its own ellipse field, with the x-radius and the y-radius of the circle’s ellipse set to the circle’s radius. When I call the circle’s draw function, it calls the draw function for the circle’s ellipse. To reiterate, an ellipse is one of the circle’s fields.

I have a function called “setPos” that updates a shape’s position. This is where things go wrong…but not in the debugger.

function setPos(solidShape::Union{Rect, Ellipse, Circle, Polygon}, coords::PsychoCoords)
    solidShape._pos = SDLcoords(solidShape.win, coords)
    solidShape.pos = coords
    if typeof(solidShape) == "Circle"
        setPos(solidShape.circlesEllipse, coords)
        #solidShape.circlesEllipse._pos = solidShape._pos           # update the ellipse owned by the circle
        #solidShape.circlesEllipse.pos = solidShape.pos         # update the ellipse owned by the circle
    end
end

The first part of the function update’s the circle’s (or any of the shapes’) positions (pos and _pos). The second part checks to see if it is a Circle, and if so, it update’s its ellipse’s positions. You can see that I’ve tried to do it two ways: by directly accessing the ellipse’s fields, and by calling the setPos function.

In the debugger, I can see that the circle’s positions update, as do its ellipse’s positions. However, the animations do not reflect that. Furthermore, if I run it in the REPL and insert some println() statements to print out the positions of the circle and its ellipse, its clear that the circle’s positions updates, but not the position’s of its ellipse. It’s as if the ellipse has its initial values from when it was initially made. I’m left scratching my head. Why do they update in the debugger but nowhere else?

I can get the circle’s ellipse’s position to update if I replace the ellipse struct with a new instance. It’s inelegant and hacky, but it works, and I don’t like it. You can see an example of this below, and the original code of the draw(::Circle) function is commented-out.

function draw(Circ::Circle)

    #draw(Circ.circlesEllipse)
    circlesEllipse = Ellipse(Circ.win, Circ.pos, Circ.rad, Circ.rad, lineWidth=Circ.lineWidth,lineColor=Circ.lineColor,fillColor=Circ.fillColor, fill=Circ.fill)
    draw(circlesEllipse)
end

What am I missing? Why does the circle’s ellipse struct field not update? Both are mutable structs, and nearly identical.

mutable struct Circle
    win::Window
    pos::PsychoCoords   
    rad::Union{Int64, Float64}                          # radius in pixels of the aa-ellipse
    lineWidth::Int64
    lineColor::PsychoColor          # these will need to change to floats to handle Psychopy colors
    fillColor::PsychoColor          # these will need to change to floats to handle Psychopy colors
    fill::Bool                          # these will need to change to floats to handle Psychopy colors
    circlesEllipse::Ellipse
    _lineColor::Vector{Int64}
    _fillColor::Vector{Int64}
    _pos::Vector{Int64}
    _rad::Int64

    function Circle(win::Window,
                    pos::PsychoCoords = [10,10],        # position
                    rad::Union{Int64, Float64} = 20;                        # Horizontal radius in pixels of the aa-ellipse
                    lineWidth::Int64 = 1,
                    lineColor::PsychoColor = fill(128, (4)),                
                    fillColor::PsychoColor = fill(128, (4)),                
                    fill::Bool = false
            )

        _lineColor = colorToSDL(win, lineColor)
        _fillColor = colorToSDL(win, fillColor)

        circlesEllipse = Ellipse(win, pos, rad, rad, lineWidth=lineWidth,lineColor=lineColor,fillColor=fillColor, fill=fill)

        new(win, 
            pos,
            rad,
            lineWidth,
            lineColor,              # these will need to change to floats to handle Psychopy colors
            fillColor,              # these will be Psychopy colors
            fill,
            circlesEllipse,
            _lineColor,
            _fillColor,
            _pos,
            _rad
            )
    end
end
mutable struct Ellipse
	win::Window
	pos::PsychoCoords
....

typeof returns a type, not a string. Maybe you mean if solidShape isa Circle.

3 Likes

This will never evalute to true. The output type of typeof is a DataType.

The statement should be as follows.

if typeof(solidShape) == Circle

Actually the idiomatic way might be to do following.

function setPos(solidShape::Circle, coords::PsychoCoords)
    solidShape._pos = SDLcoords(solidShape.win, coords)
    solidShape.pos = coords
    setPos(solidShape.circlesEllipse, coords)
    #solidShape.circlesEllipse._pos = solidShape._pos           # update the ellipse owned by the circle
    #solidShape.circlesEllipse.pos = solidShape.pos         # update the ellipse owned by the circle
end
4 Likes

Others pointed out the issue in your code, I’ll comment on the design: Does Circle really need to duplicate every field from Ellipse if it just contains an Ellipse? Could you simplify this to

struct Circle
    circlesEllipse::Ellipse
end

And just forward all appropriate methods to the contained Ellipse while providing some new Circle specific ones?

1 Like

This will never evalute to true. The output type of typeof is a DataType .

But I would never know that, as it does evaluate to be true in the debugger…sometimes!

Huh? What do I mean by that?

In the screenshot below, I added a println (line 881) inside of the if typeof(solidShape) == "Circle" statement.

If I run it in the REPL: the print statement never prints. (if evaluates to false).
If I run in the debugger with a breakpoint only on line 882, it never stops at the breakpoint, indicating that the if evaluated to false.

However, if I put a breakpoint at 880, then hit continue after it stops at 880, it goes to the next breakpoint (line 882), implying that the if statement evaluated as true. But, line 881 never prints in that situation, indicating that the if statement must have evaluated to false, yet, as you can see, the debugger has gone to line 882! So all of this time I’m thinking that the if statement had evaluated as true!

Very frustrating when the debugger doesn’t match reality. I’ll try to log an issue on github. Thanks for all of your help.

Oh, and it works if I replace "Circle" with Circle!

It’s a work in progress. They started out identical, then I thought rather than duplicating functionality, I’ll just have the Circle possess an Ellipse, but hadn’t had time to go back and trim out the fat.

I investigated the debugger issue. It looks like stepping advanced from 880 to 882 directly. If you had an additional statement at the end of the function such as an explicit return nothing, then the debugger would have advanced there. I highly recommend explicit return statements.

I filed an issue here on the debugger:

4 Likes

For that use case, you may want to consider redesigning your types, rather than trying to reimplement parts of the type system. I would do something like this:

abstract type AbstractEllipse end
struct Ellipse{T<:Number}  <: AbstractEllipse
  position::T
  radius::T
  eccentricity::T
  color::PsychoColor
 ...
end

struct Circle{T<:Number}  <: AbstractEllipse
  position::T
  radius::T
  color::PsychoColor
 ...
end
Base.propertynames(circ::Circle) = append!(propertynames(Circle), :eccentricity)
function Base.getproperty(circ::Circle{T}, name::Symbol) where T<:Number
  if  name === :eccentricity
        return one(T)
    else
        getfield(circ, name)
    end
end

function draw(ell::AbstractEllipse)
   ...
end

Properties common to all geometric shapes like color, line types and so on, may be put into their own type and included as a field in Circle and Ellipse.

3 Likes