Hello everyone. I'm learning Nim but I'm finding it difficult to implement some OOP software designs I have in mind. I see that Nim is a really flexible language that doesn't enforce the programmer to a particular programming paradigm/style (I may be wrong, but that's what I noticed while playing around with it). However I'm having trouble with the lack of multiple inheritance and/or interfaces...
I would like to make a concrete example hoping for someone to help me understand what would be an alternative way of implementing something like this in Nim. Let's consider a graphics library (maybe like SFML, strongly object oriented).
There are drawable objects (text, rectangles, circles, sprites, etc...) and render targets (a window or a texture - so I can render off-screen to a framebuffer). So the first thing I'll need is some sort of "Drawable" base class/interface and a "RenderTarget" one (I'm strongly referencing SFML here). But then a lot of drawable objects (rectangles, circles, etc...) have in common the fact that they're a shape, and having a "Shape" base class would be pretty useful to recycle a lot of (OpenGL) code used to draw... shapes. But, as far as I can understand, there's not a simple, "native" way of having a Rectangle class inherit from Drawable and Shape. Also, as I mentioned, there are multiple render targets (for example the main window or textures). Even in this case, I would have to define a texture as being both a Drawable and a RenderTarget, but how would be the best way to do that?
Please note that I'm not criticizing Nim by any means, in fact I've been using it various times in the last 2 years for various small projects and it's just an amazing language. I'm just asking for some advice/ideas on how to cleanly implement such software architectures, since that has always been my major "trouble" with the language. Also I did various experiments (even with the example above) and I know there are some ways but I have never been satisfied with them (for various reasons). Without listing all the approaches I tried here, I would simply like for someone that is experienced with the language to directly point me out to the "right" way. Or, better said, to a "good" way, since I'm sure there are multiple ones given the flexibility of Nim.
Thank you very much in advance, I hope this is not a "dumb" question.
I don't know the right answer to your problem. However note that a lot of languages do not support multiple inheritance for a good reason (outside the scope of this question), so how would you use those? :) A working pattern for me is the following: create an interface to your "multi-interface" object:
type
Texture = ref object ...
AbstractRenderTarget = ref object {.inheritable.} ...
Window = ref object ...
proc setCurrentRenderTarget(t: AbstractRenderTarget)
proc renderTargetWithTexture(t: Texture): AbstractRenderTarget = ...
proc renderTargetWithWindow(w: Window): AbstractRenderTarget = ...
...
var myTexture: Texture = ...
setCurrentRenderTarget(renderTargetWithTexture(myTexture))
You could go a bit further and make renderTargetWithTexture and renderTargetWithWindow implicit converters, so that setRenderTarget is just:
setRenderTarget(myTexture) # renderTargetWithTexture is called implicitly
Such pattern usually allows for a nice decoupled design, although may be a bit problematic when you need to work with value types instead of reference types.
First off, thank you for the quick response.
note that a lot of languages do not support multiple inheritance for a good reason (outside the scope of this question), so how would you use those?
In fact I see that my question is not strictly Nim-related but I'm actually asking for help on how to do stuff without multiple inheritance in general. I hope that's not a problem :D
By the way, I read the code you proposed but I have a few doubts. If I'm understanding it correctly, the procs "renderTargetWithTexture" and "renderTargetWithWindow" will convert from a texture/window object to an abstract render target object. But does that mean I'll have to allocate a new AbstractRenderTarget on the heap (and assign it data from the texture/window I got in) each time I have to convert one of those? Wouldn't that slow down things a bit? Also because I was thinking about an API like this:
let mainWindow: Window = newWindow(..)
let myRectangle: Rectangle = newRectangle(...)
let myTexture: Texture = newTexture(..)
myTexture.draw(myRectangle)
window.draw(myTexture)
In my mind I imagine RenderTargets having a draw method that accepts any Drawable object and Drawable object having a drawOn method that accepts a RenderTarget. The drawOn method of the Drawable objects would do the hard work, while the draw method of RenderTargets would simply be something like
proc draw(rt: RenderTarget, obj: Drawable):
obj.drawOn(rt)
just to make (IMHO) the API syntax cleaner for someone using the library (like the first code snipped).
But if I had, for instance, hundreds of rectangles, textures, etc... every time I call the draw method on an object I would have to run the converter and allocate another object on the heap... right...?
But does that mean I'll have to allocate a new AbstractRenderTarget on the heap
Yes. Or you could just return something already cached within the respective Texture/Window objects to make it high load friendly.
As a side note, your suggested API makes the user believe there's no price for switching the rendering context all the time. That could work when you implement deferred rendering (e.g. later, call underlying system functions in a batch per context). But such API will not work for e.g. direct OpenGL rendering, because switching context states is considered to be expensive.
nimx lib kinda solves this like this:
let myImage = imageWithSize(x, y)
myImage.draw: # Context switch here
let ctx = getCurrentContext()
ctx.drawRect(...)
ctx.drawText(...)
... etc
# Context switches back
That was really helpful, I'll look into a similar approach and I'll try to apply these suggestions to other designs I'll have to implement.
I see that the problem is definitely my way of thinking about the project structure/architecture (which is no surprise given I have a C++ background). I guess I'll study a bit more how to avoid "bad OOP" and reason in a "cleaner" way (i.e. like the "prefer composition over inheritance" I read everywhere :D ). If you happen to have any specific blog post/book/article worth mentioning, that would be amazing. But I'll find stuff myself.
I hope I can come back to this thread if I have other questions or I need more advice/help in the future.
Please, feel free to ask anything, I'll try my best to answer. Note though, that's only my opinion ;)
As for a good read, nothing comes to mind atm. But generally, since you're asking about OOP in terms of graphics, I would suggest sacrificing OOP, as well as composition-over-inheritance, in favor of DOD (Data Oriented Design). Additional keywords to lookup: ECS (Entity Component System). OOP is really unfriendly to batch processing. DOD turns OOP completely inside out, so to accept this you'll have to forget about OOP completely =). When you start thinking DOD (that will likely take some time), you'll suddenly notice that DOD is a lot more suitable approach for lots of programming problems. It makes your systems not only blazing fast but also makes it easier to reason about your code and your dataflow.
Ruby is a fully OOP language but doesn't have multiple inheritance either. Every class is open though (you can add/change methods in them), and you can create the equivalence of multiple inheritance by using modules and mixins where you include them in classes to extend them.
You can use this paradigm with Nim too, as it also provides for modules.
There's lots of ways to define and do/implement OO paradigms. Below is tutorial info on Ruby's implementation of its OOP paradigm
https://launchschool.com/books/oo_ruby/read/the_object_model
https://www.tutorialspoint.com/ruby/ruby_object_oriented.htm
Once you figure out the characteristics/features you want to have in your OOP paradigm then you can build it.
Allowing multiple inheritence makes the rules about function overloads and virtual dispatch decidedly more tricky, as well as the language implementation around object layouts. These impact language designers/implementors quite a bit, and raise the already high bar to get a language done, stable and adopted.
It is simple to think this way, if class A inherits from multiple classes, then the class A will have the same grandparent class multiple times, this means the code will be complicated and a series of bugs will go unacknowledged. Personally, I think multiple inheritance has a bad rap, and that a well done system of trait style composition would be really powerful/useful... but there are a lot of ways that it can be implemented badly, and a lot of reasons it's not a good idea in a language like C++.