Hacker News new | past | comments | ask | show | jobs | submit login

While a lot of people make objects for the sake of making objects, I think it is instructive to think why you want to make objects in OO. As another poster has said, what if I had structs and functions with multiple dispatch? Why would that not be better than OO?

People tend to think of OO as a way of encapsulating data and then providing access to it. I rather think of it as a way of encoding program state. Often you have multiple operations that you want to perform on the same program state. It is nice to group that code together. Indeed, we have a computer sciency word for that: cohesion.

In FP, too, we do exactly the same thing. Take a look at any modern FP program and you will see that it is usually broken down into modules that represent program state. The modules contain code that operates on that state. We do it for the same reason we do it in OO: it creates good cohesion.

Getting back to my original question: why not structs with multiple dispatch? Actually, I wouldn't mind that at all (what's not to like about multiple dispatch?). However, the problem, I actually have is the struct. It's the same problem I have with the way many people approach OO: they are thinking about the code from the perspective of how to encapsulate data, not about what functionality they want. They are thinking about raw data instead of program state. A struct is a fine place to store data, but it's not the first place you want to go when you are doing design.

Instead you want to build your functionality and identify where you have commonalities in state. Then you should start thinking, "Hey these are using similar state. Are they related? Would it be more cohesive if I grouped them together?" Of course, it would be kind of strange to do that all the time. Most experienced programmers have a pretty good idea what program state they need to work on before they start. They can design data structures that are likely to be needed ahead of time. However, the priority should be to adjust the data structures to match the needs of the program state and to create more cohesive programs -- not to provide rigid boundaries separating your code.

Of course, nothing I've said is much different than programming in FP, and I wish that were not a surprise. OO and FP are not really so different. It's pretty easy to make the exact same mistakes in FP as you can make in OO. I sometimes feel like FP is just not popular enough to have widespread bad practices (like writing entire applications using only free monads).




> People tend to think of OO as a way of encapsulating data and then providing access to it. I rather think of it as a way of encoding program state. Often you have multiple operations that you want to perform on the same program state. It is nice to group that code together.

That's very consistent with the original definition of OO, which hardly anyone uses much anymore. And erlang and elixir (and Ruby if you follow Gary Bernhardt) both fps do that quite well, via the actor model. Where OO starts to fail is when you gave to make a decision between two objects, e.g. "knight attacks mage" is the "attack" function a member of knight or a member of mage?

Grouping functions by what state they touch is basically just namespacing, common between OO and FP.


This is only true if your object system is based on the idea of "methods belong to classes" as opposed to "methods belong to generic functions", as seen in Common Lisp for example.

If you define a generic function ATTACK with arguments ATTACKER and ATTACKED, you are free to create a method with specialization ((attacker knight) (attacked mage)) and write code that will be executed only when a knight attacks a mage.


> e.g. "knight attacks mage" is the "attack" function a member of knight or a member of mage?

For me, it's a member of the Fighter protocol/interface to which they both conform, and either might override the default implementation or leave it be.


Can't we just create a separate class, like a `Battle`? And then do `Battle.between(knight, mage)` where Battle would handle the negotiations between the two classes? Maybe producing a result without mutating the two entities we pass as arguments. I feel like it depends on what the business is. Is it tracking wizard adventures, fighter exp calculator or distributed battle log.


> Can't we just create a separate class, like a `Battle`? And then do `Battle.between(knight, mage)`

Exactly. And at the end of the day all you're really doing is defining a function. Why do you need a class at all? Is it just because the language you're using doesn't allow standalone functions? If you're defining a static function that has no state on a class, you don't need the class at all. It's just a function that you want.

battle(knight, mage)


The missing observation here is the `Battle` or `Collide` operate at a level of coordination. It is at this level where FP shines.

Simply adding an `addDamage` method to the above system exemplifies where OOP shines. The above method works at lower, domain level, where the semantics of adding damage are specific to the object to which damage is being added.

The point here is that _both_ paradigms should be employed.


In the spirit of separation I would envision knight and mage (nouns) to be objects containing data properties defining their characteristics against an interface. The action (verb), attack, is a function which makes decisions and has portability. Expressed as separated semantic components knight attacks mage would be a subject, action, predicate relationship and each piece is individually portable and mutable.


I think you missed the real question: should the verb be `attack` or `defend` (i.e. "defend from", or "attacked by")?

In class- or prototype-based OOP, we have to choose between `knight.attack(mage)`, with the functionality living inside `knight`; or `mage.defend(knight)` with the functionality living inside `mage`.

> The action (verb), attack, is a function which makes decisions and has portability.

OOP discourages standalone functions, so this function would either have to be in the knight, in the mage or (I would argue even worse) some sort of `GameState` object.


I guess it would depend upon the rules at play and various other unspoken variables, but generically speaking I don't see the difference between attack and defend in this limited scenario. One agent is interacting with another. The same instruction set would apply if it were knight attacking mage or mage attacking knight.

> OOP discourages standalone functions

Oh how I detest OOP. Portability of atomic components is amazing. So is scope nesting. Its like driving to work whenever you want in any of your cars (or your neighbors) instead of checking out a car from a car model inherited by a sedan class inherited from Toyota car class when such a thing becomes available.


I agree that nether way around seems inherently better than the other, so it would depend on the specifics of that project.

> The same instruction set would apply if it were knight attacking mage or mage attacking knight.

Not necessarily! For example, a mage might cast long-range magic, whilst a knight would only have melee attacks. Where we make this distinction can affect our understanding, maintaining and extending of the code.

Standard OOP practice would use an inheritance hierarchy to handle this difference, e.g. a `Character` class could have `RangedCharacter` and `MeleeCharacter` subclasses whose `attack` method handles the details of those sorts of attack; then `Knight` and `Mage` subclasses those, to specialise the parameters. The major problem with this is that we can't model multiple distinctions in this way; for example knights and mages might both be "noble", whilst trolls and imps are "evil". If we want `Knight` to inherit from both `MeleeCharacter` and `NobleCharacter` we'd need multiple inheritance, which is where things get tricky.

More recent trends favour composition rather than inheritance, e.g. passing in an `Attack` argument during construction, with `RangedAttack` and `MeleeAttack` subclasses. Separate objects can be passed in for the `Noble`/`Evil` distinction. Of course, these are just standalone functions with more boilerplate ;)

Mixins are somewhere inbetween, where we avoid some of the boilerplate chaperones ( https://steve-yegge.blogspot.com/2006/03/execution-in-kingdo... )

The CLOS approach, mentioned by others in these comments, lets us define verbs like `attack` outside of any particular class or mixin. We can then define it case-wise based on its arguments, like `attack(RangedCharacter, Character)` and `attack(MeleeCharacter, Character)`.


> Not necessarily! For example, a mage might cast long-range magic

For me that is just a difference in data properties. The event is the actual attack. A ranged attack would be the distance between the two parties. A melee attack would have a distance of 1. A self-inflicted attack would have a distance of 0. If you attempt a melee attack from a different room the attack action still happens, but the other party receives no damage and probably no awareness of the attack. The handling of the action and consideration of the data is still the same regardless of who commits the action and who is the attacked party.


Aren't "data" and "program state" mostly one and the same? Encapsulation can easily be seen to matter whenever one has several plausible representations of data/state that all are to some extent isomorphic to each other, and one wishes to operate on these representations in a way that can be easily verified to respect the isomorphism, and not to rely on irrelevant details.

These considerations are actually becoming more relevant to FP rather than less, especially as we're slowly gaining a broader understanding of the potential of newly-developed features such as homotopy types, in enabling one to write programs that can work directly with such isomorphisms, ensure that desirable encapsulation-like properties hold, and even convert code to work "across" any arbitrary change in internal representation.


> Instead you want to build your functionality and identify where you have commonalities in state. Then you should start thinking, "Hey these are using similar state. Are they related? Would it be more cohesive if I grouped them together?"

Unfortunately in most OOP langauges, this isn't an option. Free functions aren't a thing, for the most part.

An object is the smallest unit, so if you need a single function you end up creating an object to hold it. Which results in an explosion of FooHelper, FooUtilities etc

This objects as the smallest unit also can end up in an explosion of regular classes, all only slightly different.

It's madness.


Static methods are a thing - all they need is a "module"-like unit to hold them, but no that's not the "smallest unit" in any real sense.


In a language like C# you still need to put those static methods in a class.

Even extension methods need to go in a class (albeit a static class).

It would be nice if you could define functions that lived in a namespace.


Just treat the static class with static only member as if it were a namespace.

C# now even has support for omitting the need to type the class name because you can import it as if it were a namespace (`using static`). Seems like a trivial complaint.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: