Code design question here. I'm writing a drawing package, and in the package, there are several possible structs referring to geometric objects. For example, circle, triangle, rectangle. My idea is to create a drawobject(o::MyStruct)
function that dispatches in each struct. The question is, should the parameters of the geometric objects go inside the structs, or appear only as arguments in the function?
For example:
struct Circle
radius::Real
label::String
end
c = Circle(1,"C")
drawobject(c)
Or should I go with:
abstract type Circle end
drawobject(c ; label="C", r=1.0)
Is there any heuristic to know when to use one or the other in Julia?
I think they should be in the struct
If I may. Why would you go with that? I'm trying to develop an intuition for this sort of thing.
Well, there's a lot of different considerations, but I think that generally arguments that are a 'bundle of connected things' usually should be bundled together into a struct when possible
For instance, to take a mathematical example, we could say write a function for finding the norm of a complex number like this:
norm(re, im) = re^2 + im^2
but the real and imaginary parts of a complex number are tightly connected things and we usually want to think of it as just one thing. Why juggle around a bunch of different parts? Rather, we prefer to write
norm(z) = real(z)^2 + imag(z)^2
That said, it's possible your label isn't really an 'instrinsic part' of the circle and you might prefer to split that out of the struct
Makes total sense. Would you say that the heuristic here would be to place inside the struct the fields that are specific ("unique") for that struct, and leave as an argument those that appear in all of them?
I mean, this would even make sense with the fact that structs in Julia do not inherit stuff.
Your explanation actually made me realized I had the whole thing reversed. I was placing the commonly shared parameters in the struct, and leaving everything else out.
Davi Sales Barreira said:
Makes total sense. Would you say that the heuristic here would be to place inside the struct the fields that are specific ("unique") for that struct, and leave as an argument those that appear in all of them?
No, I don't think I'd say that. I'd just say that the stuff that makes a circle a circle should be inside the circle struct.
Ok. I see your point. But then I still have a conundrum. My package is for drawing, so I have properties such as linethickness
and linecolor
and fillcolor
... Would you say that all of these drawing attributes should be inside the struct or outside?
At some point, for example, I could end up with a struct TRex
, which would have stuff like head
and headcolor
...
I know this sound silly, but the idea here is that the struct are like the "atomic" drawings. And they are going to be things beyond simple geometric objects... Thus, I'm trying to understand how should one deal with all this complexity.
Yeah, I think this just comes down to taste and ergonomics at that point. I'd try out some different designs and see what you like
Sometimes it makes sense for stuff like the line thickness and whatnot to be in the struct and sometimes it doesn't
I see. I was wondering if there was a sort of guideline for this stuff. So I guess not.
Yet, thanks a lot @Mason Protter !
You've already helped me a lot, not only in this post, but in many other posts here in Zulip. I'm truly grateful!
E.g. in Plots.jl when you do something like
xs = 1:10
plot(xs, xs .^ 2, title="a title", linewidth=2)
this actually makes a struct
that contains all that data (as well as a bunch of other default data) because it's easier to move around and combine with other plots and whatnot.
Yeah. Though that's an implementation detail of Plots. I think Davi's question is more about what the user-facing API should be?
For this I think you'd also be well served by looking at what other drawing packages do. There's so many to take inspiration from, including ones from other languages. Usually you can mimic the API from another language's packages into Julia with very little effort (it's the other direction which is often not so easy).
There's a lot of options! For example, Luxor.jl doesn't have structs at all, it's a very stateful imperative drawing API. That might not be what you want but personally I do find that style very easy for quick drawings. As drawings get larger and you want to compose parts, having more explicit state and functional style is less confusing, I think.
So I guess I'd ask: What are the structs for?
Having something like
drawobject(Circle(center, radius), stroke_width=1.0)
is strictly more typing than
circle(center, radius, stroke_width=1.0)
But it might be worth it if you think the user typically wants to pass Circle
objects around and manipulate them. It depends on your expected use case.
also check out https://jkrumbiegel.com/Layered.jl/dev/ for a different take on 2d graphics api
Last updated: Dec 28 2024 at 04:38 UTC