ID:1690117
 
I've been moving along with a different method of programming lately. Straying from the object-oriented ways and moving towards a procedural (imperative) approach. This paradigm was actually the same that I had started off with in BASIC variants. Their main driving force was sets of subroutines, with objects merely being backend structures that held data.

In an object-oriented universe, everything revolves around the use of objects holding everything. Methods, variables, etc.. To modify data or call a method, you would do so from that object or interface. OOP works off of the "three pillars" which are:
  • Inheritence
  • Encapsulation
  • Polymorphism


Inheritance brings the "is-a" relationship between objects where an object can be a derived type of a parent object.

Encapsulation ensures that entities are contained in a "bubble" or scope where there's a certain restriction on how or where you can access fields in an object.

Polymorphism allows the methods of an object to be overridden (rewritten) and overloaded (allowing the same method with multiple parameter data types), while also allowing the parent method to be called, optionally.

These three things created a structure of programming that allowed large groups to work in collaboration with the ease of the object notation simulating real-world scenarios. However, this same structure enforces restrictions and loss of flexibility to the programmer. Object-oriented languages are very strict in the regard that objects must exist for their fields to be invoked. An exception to this is the "static" attribute which creates a single "instance" of an object type which may be invoked just about anywhere, and referred to its type instead of an instance name (Class.Method, not ClassInstance.Method).

What I want to talk about is not approaching programming situations with objects in mind. Instead, let's say that objects are more abstract to the user?

Check this little example of DM code below:
item
var/name


An item at this point merely has a name, which is uninitialized. How would we create a new item? Would we use a method? Well, we could create a function which creates a new item:

proc/CreateItem(N="No Name")
var/item/i = new()
i.name = N
return i


What this proc does is give us a freshly baked item instance. CreateItem() is a global function that is accessible anywhere. What if we wanted to make a function to change its name?

proc/SetItemName(item/I, N="No Name")
I.name = N


It might seem redundant to make a function that only sets a single variable, but for the sake of consistency, we want to try and keep the object-oriented functionality as vague as we can get it. So far, making and setting the name of an item in front-end code would look like this:

var/d = CreateItem("Some Item")
SetItemName(d, "Another Item Name")


the /item type is nowhere to be seen in this example. At this point, an Item is an abstract class to the user. They know by its function names that it indeed has a name value. To grab that name value we can add:

proc/GetItemName(item/I)
return I.name


Our current function set (compared to OO):

// Imperative Notation
CreateItem(text)
SetItemName(item, text)
GetItemName(item)

// Object-Oriented Notation (exclusing methods SetName() and GetName())
item
var/name
New(T)
..()
name = T
return src

// Example of creating and setting the name in Imperative Notation
var/d = CreateItem()
SetItemName(d,"Flower")

// Example of creating and setting the name in Object-Oriented Notation
var/item/d = new()
d.name = "Flower"


It's up to you how you like your code. I personally dislike many OO practices and I find it really confusing when using stuff like inheritance (with large object trees) ("is-a"). I would generally prefer to use composition ("has-a") instead.
The problem is, how are you going to make a game only using datums? I also like having the ability to categorize and classify my objects. Inheritance and polymorphism are too useful to ignore.

I think that with the direction you are going in, what you will need is a function that can seamlessly convert any given object instance into an instance of another type, without losing any information. For every object instance, there would need to be a second datum instance that serves to represent the interface for that particular object. This means that whatever happens to the datum should be reflected by the object and vice versa.

By converting each object into a kind of binary instance system, the different types that exist will become abstract, and serve only to produce a certain manifestation of the object when needed.

Similarly, object variables might also need to have a binary system as well, since you can't copy over values for vars that don't exist in another type. In this system, each object would have 2 vars lists. There would be the built-in vars, and then there would be the far more flexible, virtual vars, which allows new vars to be created whenever they are needed. When an object is converted from one type to another, if the new type doesn't have a compile time var that corresponds to the original, then the value is added to the virtual vars list instead.

This is all just a theory, so I really don't know how well it would work in DM. It may be easy to describe, but actually implementing it would be challenging, since it's such a large, sweeping change that engulfs the whole structure of the language. This might also help set the stage for a powerful, interpreted scripting language, but that's another story.
Your "creating and setting in OO" doesn't use the constructor argument to set the name.

Did you know, returning anything in New only applies to when you call New directly? It has no effect in a "new" instruction.
Oh. I didn't know that about New. That's handy to know.
Q&A with Multiverse7! lol:
The problem is, how are you going to make a game only using datums? I also like having the ability to categorize and classify my objects. Inheritance and polymorphism are too useful to ignore.

It is possible to make a game with only datums, but in order to render objects onto the screen, some sort of atom/image is required. Otherwise, datums (or user-defined types in this sense) are useful in themselves. A datum can contain references to other datums via composition:
datum
var/component/varname // establishing a relationship between datum and component using the "has-a" relationship.

component
var/text

proc/Datum_DoSomething(datum/D)
Component_DoAnotherThing(D.component)

proc/Component_DoAnotherThing(component/C)
world << "[C.text]"


I think that with the direction you are going in, what you will need is a function that can seamlessly convert any given object instance into an instance of another type, without losing any information. For every object instance, there would need to be a second datum instance that serves to represent the interface for that particular object. This means that whatever happens to the datum should be reflected by the object and vice versa.

True. If using a non-inherited hierarchy, some sort of identifier will need to be used to specify what sort of child type is being contained (except that in this case all datums have the "type" identifier, but a type can easily be placed in another variable if no typecasting is used.

// This is just pseudocode. Don't take object types and variable names literally.

supertype
var/child_type
var/child

proc/SuperType_SetChild(supertype/S, datum/D)
S.child_type = D.type
S.child = D

proc/SuperType_InteractWithChild(supertype/S)
switch(S.child_type)
if (/someobject)
SomeObject_DoSomething(S.child)
// expects a /someobject type - implicitly passed.


This is all just a theory, so I really don't know how well it would work in DM. It may be easy to describe, but actually implementing it would be challenging, since it's such a large, sweeping change that engulfs the whole structure of the language. This might also help set the stage for a powerful, interpreted scripting language, but that's another story.

Thinking outside of the box is a nice way to build your experience as a programmer. If a language that is built on one fundamental system (such as BYOND with OO) and you decide to use a different system, it can prove to be challenging, but is still possible. At this point, it's up to the programmer what he or she wants to do, as long as it gets the job done effectively.
In response to Mr_Goober
Mr_Goober wrote:
It is possible to make a game with only datums, but in order to render objects onto the screen, some sort of atom/image is required.

I hope you don't mean handling most of the rendering yourself, because that would just be working against BYOND instead of with it. Just because you want to use another paradigm doesn't mean you need to reinvent locations, animations, and movement at the sprite level. That would be a huge amount of work and the end result would be much less efficient than it would be otherwise. If that really is your goal, then you would be better off using another engine entirely, or even making your own. It just isn't a good idea to try and force a high level language to do lower level things than what it was built for.

True. If using a non-inherited hierarchy, some sort of identifier will need to be used to specify what sort of child type is being contained (except that in this case all datums have the "type" identifier, but a type can easily be placed in another variable if no typecasting is used.

I think I'm starting to see things your way now. Composition is clearly far superior to inheritance. Not only is composition more flexible, but it could actually simulate the whole inheritance system at runtime, if needed. Inheritance and even multiple inheritance are just some of the many ways that composition can manifest itself. Hopefully composition will become more popular in the future. Now I can laugh at silly arguments like this one (scroll down and read the whole thing). I wouldn't want to get involved though, because these concepts are blasphemous to the followers of polymorphism. Besides, getting into an argument with them would be like getting stuck between a rock and a hard place.

Thinking outside of the box is a nice way to build your experience as a programmer. If a language that is built on one fundamental system (such as BYOND with OO) and you decide to use a different system, it can prove to be challenging, but is still possible. At this point, it's up to the programmer what he or she wants to do, as long as it gets the job done effectively.

Yes, I always find it quite amusing to make systems do something other than what they were intended to do. It lets me be a programmer and a hacker at the same time.
@Multiverse7 Actually, in regards to rendering, making something to handle where sprites are placed isn't all that difficult. I'm working on a system where the entire program uses /image objects and are sent to clients selectively.

I've found that this can be used in place of Z levels and /obj datums. Only 1 Z level would ever need to be used. In this sense, the map would serve only as a container for sprites. A canvas of sorts.

Setting this up is easy:
sprite
var/list/clients
var/image/image

proc/Sprite_Create()
var/sprite/s = new()
s.image = new()
s.image.loc = locate(1,1,1)
s.clients = new()
return s

proc/Sprite_SetPosition(sprite/S, X=0, Y=0)
S.image.pixel_x = X
S.image.pixel_y = Y

proc/Sprite_AddClient(sprite/S, client/C)
S.clients += C
C.images += S.image

proc/Sprite_SetIcon(sprite/S, icon/I)
S.icon = I


You could even stick this code directly into any project and it should work just fine. The code doesn't use objects as its driving force. Only a container of data. This being said, the code is very portable.
In response to Mr_Goober
In this sense, the map would serve only as a container for sprites. A canvas of sorts.
Thanks, I never considered it that way before. /s

How will you handle mouse interaction?

I think image.override is underappreciated. If you create an image with override = TRUE on an atom, the atom will only be visible through that image.

Your code could be written a slightly different way that I consider cleaner:
sprite
var clients[0]
var image/image

New(I, X = 0, Y = 0)
image = image(loc = locate(1, 1, 1))
if(I) SetIcon(I)
if(X || Y) SetPosition(X, Y)

proc/SetPosition(X = 0, Y = 0)
image.pixel_x = X
image.pixel_y = Y

proc/AddClient(client/C)
if(C in clients) return
clients += C
C << image

// RemoveClient()?

proc/SetIcon(icon/I)
image.icon = I

You could even stick this code directly into any project and it should work just fine. The code doesn't use objects as its driving force. Only a container of data. This being said, the code is very portable.
This code is more portable, since it doesn't fill the global scope with specific/irrelevant junk. It also uses exactly the same number of objects your code does.

I don't think this is very good evidence to support your "Imperative > OO" standing.

var sprite/s = new
s.SetIcon(icon)
s.SetPosition(x, y)
// the above code could've been written "new /sprite (icon, x, y)"
// but you could've just as easily done that too

s.AddClient(client)

// vs

var sprite/s = new
Sprite_SetIcon(s, icon)
Sprite_SetPosition(s, x, y)
Sprite_AddClient(s, client)
How will you handle mouse interaction?
Mouse interaction works just as effectively with image objects than other atoms, I believe. I thought about how interaction with the mouse can be achieved, and I drew up an example which pretty much scales up an blank icon to the screen's dimensions.

Then, MouseMove() is called whenever the mouse moves across it. The icon-x and icon-y parameters will give the respective pixel in which the mouse has moved to, and if relative to a scene, a global pixel location can be calculated.

As far as the way my code style goes, I mostly base it off of BASIC-styled variants which at the time didn't have OO practices. BlitzBasic, for example, had support for user-defined types, but inheritance and polymorphism was not a thing. It would look something like this:

type t_thing
field pos_x#
field pos_y#
end type

function Thing_Create(X#, Y#)
local r.t_thing = new t_thing
Thing_SetPosition(r, X, Y)
return r
end function

function Thing_SetPosition(T.t_thing, X#, Y#)
T\x = X
T\y = Y
end function


EDIT: BlitzMax (a successor to BlitzBasic and BlitzPlus) includes object-oriented support, but I find it simpler to keep the old style.

EDIT2: The importance of a function set is that functions that belong to a given module/object can be called even if no instance exists for it.
In response to Mr_Goober
Mr_Goober wrote:
How will you handle mouse interaction?
Mouse interaction works just as effectively with image objects than other atoms, I believe. I thought about how interaction with the mouse can be achieved, and I drew up an example which pretty much scales up an blank icon to the screen's dimensions.

Then, MouseMove() is called whenever the mouse moves across it. The icon-x and icon-y parameters will give the respective pixel in which the mouse has moved to, and if relative to a scene, a global pixel location can be calculated.
I meant, how will you detect mouse events (MouseEntered(), Exited(), Down(), Up(), Drag(), Drop()) for specific objects, other than the turf at (1, 1, 1), where all your images are set.

As far as the way my code style goes, I mostly base it off of BASIC-styled variants which at the time didn't have OO practices. BlitzBasic, for example, had support for user-defined types, but inheritance and polymorphism was not a thing. It would look something like this:
[...]
That's unfortunate. Good thing you're using DM, so you don't have to deal with that mess.
Matter of opinion, really. I enjoy using Blitz products.
That's a good point. An image might have a lot in common with atoms, but it just isn't a real location, so the mouse can't directly interact with it. Since the server doesn't actually know anything about the opacity of the icons, you might be forced to use objs instead. The only workaround I can think of would be to use icon operations to scan the opacity of all the icons before the game starts, but that just seems crazy. Being forced to use objs would call into question the feasibility of such a system in BYOND.
Sorry if I'm sounding ignorant, I might have missed something--I'm a novice programmer after all, but how is this:
m.Tell("Hello")

// more user friendly than this:

Tell(m,"Hello")

They are equal. OO notation even matches with english word order.

And for the other part, well, wouldn't this imperative notation possibly cause many errors?
var/a = CreateSprite()
a.SetItemName()

// and they are not so easy to spot

To the extent that I understand it, imperative notation makes you remember what methods work with which types, while OOP only lets you use methods that exist in its context.
Therefore each type could have SetName() and GetName() defined separately or inherit from the same object instead of many Type1_SetName(), Type2_SetName(), Type3_SetName()... methods.

Also note that OO paterns emerge from the way you are naming your functions. You only change
var/a = Sprite.Create()
// to
var/a = Sprite_Create()
There's a couple of things about the imperative design that vary from the OO design. One of which is that you don't even need to use objects for imperative programming. You can use virtually anything you'd like without an object mediator, whether it be a file, an image, a savefile, or a list. Anything that has the capability to hold data is yours for using. The main difference is that the front end user won't even know it. This is what I meant by object abstraction.

proc/Thing()
var/d = new/list()
d["name"] = "Hello."
return d

proc/Thing_Name(T, N)
if (N) T["name"] = N
return T["name"]

client/New()
..()
var/e = Thing()
src << Thing_Name(e)


In the example above, using an accessor like . won't work, because it's not an object. It's a list. However, it's being treated like an object. You'd need to use Thing_Name() or Thing["name"] to retrieve the data.

proc/BitFlag()
var/d = 0
if (args.len)
var/n = min(args.len,16)
for (var/i = 1 to n)
if (args[i])
d += 2**(n-i)
return d

proc/BitFlag_Set(N,B)
B = 2**(Clamp(number(B),1,16)-1)
return N | B

proc/BitFlag_Unset(N,B)
B = 2**(Clamp(number(B),1,16)-1)
return N & (~B)


In the example above, a simple number serves as a holder of data.
It seems I'm a bit late to the discussion, and I haven't read through most of it thoroughly, but...

I'm not sure if OO vs "Imperative," as Mr_Goober put it, is really such a big deal. From what I can see in the OP, you are still using objects, and IMO the difference between using a method to set a datum's field vs using a global function to do so is kind of minor.

I think a more useful thing to discuss might be Polymorphism vs Composition. Using polymorphism in the form of simple, shallow inheritance trees can be really helpful. However, I've learned the hard way that an over-reliance on polymorphism can lead to horrible to read and maintain spaghetti code.

So instead of overriding methods in a subtype, you would instead offload the behaviour into a separate datum that would be stored as a var. This datum would then be responsible for handling the special case.

A crappy example.

Instead of:
mob/critter
proc/handle_move()

mob/critter/flying
handle_move()
//special flying stuff


You would do:
mob/critter
var/datum/move_type/movement = new()


In which case flying critters would have a different move_type datum. While in this contrived example the benefits of the second way are not really easy to see, with more complicated problems I think it really can be an improvement. Especially where you need to combine two different "traits" but can't due to otherwise needing multiple inheritance.

what is the advantage of doing this
In response to Miauw62
Miauw62 wrote:
what is the advantage of doing this

You bought nice shoes yestarday but today you saw even more cooler shoes and now you want those. Both are shoes!
i suspect these shoes were made with satanic child labor.
what is the advantage of doing this

Cleaner separation between units of code, so it's easier to modify one thing without breaking another. Basic SRP:

If a class has more then one responsibility, then the responsibilities become coupled.
Changes to one responsibility may impair or inhibit the class' ability to meet the others.
This kind of coupling leads to fragile designs that break in unexpected ways when
changed

Ideally, every single task that might need to be performed should be isolated into it's own atom/datum.

Think about it this way, if you have a proc that does three different things, these three things could interfere with each other at any line of code. If you change one of them, you have to scan everything to make sure that none of the other two things are affected.

If this proc was split up into three separate procs, now they can only interact with each other through a proc call. Now if you change one of them, you only need to look at where that proc is being called in order to see what is affected.

The mental workload on the programmer needed to make a change is reduced, and therefore the likelihood of bugs is lessened. The same concept can be applied to datums (data?).

Also avoids copypasta. I've seen plenty of cases where procs were copied from datum_a to datum_b because datum_b was already a child of datum_c and therefore inheritance could not be used. Instead, the functionality that datum_a and datum_c/datum_b wanted to share is taken over by a new type, datum_d, which both datum_a and datum_b reference.
That makes good sense and seems good practice (although you won't have to resort to those kind of measure in most languages since they provide multiple inheritance or interfaces), but I still don't see how you need the weird stuff in the OP for this.
You would merely give all atoms or atom/movables a var/datum/datum_d/d and initialize it when it's needed. That's fine, I've seen that done and it's neat. But with this system, you would do this for every single proc, which seems very redundant and silly, because most procs aren't needed everywhere.