5 ms·
I'm all for ADTs but the reasons the article gives are all wrong, and in fact I think it makes the code a lot worse as a result. Object programming doesn't mea
by trashburger 3y ago
I'm all for ADTs but the reasons the article gives are all wrong, and in fact I think it makes the code a lot worse as a result.
Object programming doesn't mean "inheritance trees", that's just the (IMHO overly simplified) flavor of object programming that we call "OOP". (I'm omitting a full rant from here, which I ought to put on an article at some point.) Object programming is all about state and behavior, just like functional programming; it only differs in how it lays out the behavior namespace (object programming uses inheritance of behavior through lookup chains, which puts the object in focus; functional programming inverts that and uses the structure of the object to dispatch, putting the function in focus).
Grouping behavior together in a single function like the article suggests is, IMO, an anti-pattern and one that I consider to be one of the great disadvantages of a functional programming style. Bundling behavior together means the behavior is out of the control of the object; the object's identity now becomes a critical part of the operation being performed. IMO, putting the focus on behavior is the wrong approach here.
Don't get me wrong, I'm all for some features in functional programming; immutability is great for being able to make assumptions about program state and re-entrancy, for example. But I think the tendency to "lock" behavior into functions and not letting objects define their own behavior is a major shortcoming.
- grumblingdev 3y ago> Bundling behavior together means the behavior is out of the control of the object; the object's identity now becomes a critical part of the operation being performed. IMO, putting the focus on behavior is the wrong approach here. Why? Do you have an example to better explain? If find people rarely talk about the evaluation criteria for which approach to programming is better. That is, what are you optimizing for?
- trashburger 3y agoI'll respond in reverse order ;) > That is, what are you optimizing for? I'm optimizing for other programmers to easily be able to see how objects fit together and behave, and for them to be able to easily extend the system without touching the behavior of other components (which you cannot do if you bundle behavior together). I've slowly shifted towards this approach throughout the last few years of my programming journey. > Do you have an example to better explain? Let me make an attempt to do so. Let's say that you have an HTML form builder in code (this is something I'm currently working on, if you can't tell ;). Your form builder lets you design the form such that you can split the form into sections, rows and columns. You might design a registration form like this, for example (I'm using Python-like pseudocode here): layout = FormLayout( Section( title=_("Account information"), contents=Column( Field("username"), Row( Column(Field("password")), Column(Field("password_repeat")), ), ), ), # etc. you get the idea ) Now, how would I go about rendering this into a tree? If I were to take the approach that the article suggests, I would have to do something like this: def render_element(element: Element) -> SafeString: # Let's use Python 3.10's match as an analogue for the Dart switch match element: case FormLayout(children): return "".join(render_element(child) for child in children) case Column(css_class, children): return format_html( '<div class="{css_class}">{children}</div>', css_class=css_class or "col-md", children=mark_safe( "".join(render_element(child) for child in children), ), ) # case Row, Field, ... As you can probably tell, this is bound to get super unwieldy over time. And this is just for one property of this HTML form builder; now imagine if we had to do some operation to the tree (like checking all fields in a form are actually rendered)! You might, at this point, suggest that we could just split the behavior into different functions... and that would just make us re-implement the dynamic dispatch system that's found in object programming already ;) You can see how we tend to move the behavior to the object in this case, because it lets each object only worry about its own behavior. I hope this helped with giving a bit of an idea of why I'm for a more object programming-based approach over the pattern matching described in the article.
- grumblingdev 3y agoIt's interesting. I've gone the other direction. Everything as functions. I've realized that things always need to change in unexpected ways. Objects are sticky, people just cram stuff into them instead of re-thinking the hierarchy properly. There probably exists a really nice and cleanly separated object-hierarchy for a codebase at a given point in time, but requirements always evolve too rapidly to enjoy this for too long.
- trashburger 3y agoI believe this to be an artifact of our current paradigm of software development, which is based purely around static "batch processing" where the software can't be molded easily. (I didn't want it to come off as a plug, so I didn't add it to my original message, but I'm working on something to solve that.) To get a better idea of what I think is the better approach, check out these talks: Tudor Gîrba - Moldable development - https://www.youtube.com/watch?v=Pot9GnHFOVU https://www.youtube.com/watch?v=Pot9GnHFOVU Jack Rusher - Stop Writing Dead Programs - https://www.youtube.com/watch?v=8Ab3ArE8W3s https://www.youtube.com/watch?v=8Ab3ArE8W3s
- grumblingdev 3y agoOh yes, I'm super interested in this space too. Really waiting for one of these ideas to breakthrough to mainstream.
- toastal 3y agoI think a lot about how these things might look in a ML-family layout : Element → SafeString = FormLayout [ Section { title : _ "Account information", ; contents : Column [ Field "username" ; Row [ Column [ Field "password" ] ] ; Column [ Field "password_repeat" ] ] ] ] } (* etc. you get the idea *) ]
- zogrodea 3y agoIf you want to override behaviour, why not just define a sum type wrapping other objects, a master function that matches on different cases, and then smaller functions that decide on what to do for each case? That seems fine to me. Like this: type obj1 = { ... } type sum = | One of obj1 | Two of obj2 ... match param with | One obj -> f(obj) | Two obj -> f2(obj) ---- There is also the handle pattern which I've used with some success. You basically place some functions in a record/tuple and then call those functions when you need to. https://jaspervdj.be/posts/2018-03-08-handle-pattern.html https://jaspervdj.be/posts/2018-03-08-handle-pattern.html Edit: I mean to say that I've used something similar to the handle pattern in an immutable context (having a central function that takes objects that can be arbitrarily nested which carry their own function and data, and the parent object can call its function on a child object, and the child can call its function on its child, and so on...). Hopefully I am making sense.
- layer8 3y agoThat means having to code the dispatch manually that OO languages do automatically. It’s kind of the point of OO languages to not have to do that manually, and to have the subtype-specific functions be explicitly associated with each other, and not just implicitly by how they are used.
- zogrodea 3y agoThat's true. I didn't think of it that way.
- trashburger 3y agoWell, as my sibling comment says, you have just re-implemented dynamic dispatch in object programming ;). As a response to your edit, note that dynamic dispatch and immutability are not mutually exclusive; an object can have immutable state and can still have its behavior extended by creating an adapter object, for example.
- _old_dude_ 3y agoIt depends if the hierarchy is open or closed. If the hierarchy is open, the behavior is defined in each subtypes and it's easy to add a new subtypes but you can not add a new operation (a new method). That's the OOP way. If the hierarchy is closed, the behavior is defined in each function, it's easy to add a new operation (new function) but you can not add a new subtype. That's the ADT way. This is known as the https://en.wikipedia.org/wiki/Expression_problem https://en.wikipedia.org/wiki/Expression_problem
- trashburger 3y agoWell, I am a very strong proponent of keeping the hierarchy open in all possible cases; the object should be the one that defines the behavior in all cases, and an object's behavior shouldn't be restricted just because it comes from outside of the object or within. Therefore I agree with your point here.
- 9diov 3y agoThere are cases when the OOP way clearly has the disadvantage. That's why you need visitor pattern (https://en.wikipedia.org/wiki/Visitor_pattern?useskin=vector https://en.wikipedia.org/wiki/Visitor_pattern?useskin=vector). The ADT approach is superior in those cases, as stated in the link above.
- trashburger 3y agoAre you talking about the specific case in the expression problem article? It seems to mainly be a problem when an object's behavior can't be modified, either through the language faculties or through other means like a module system. I don't disagree with its conclusions but I believe it to be a problem of language capabilities.
- munificent 3y agoThere are cases when the FP way clearly has the disadvantage. That's why you need the "record-of-function-pointers" pattern. The interface-and-implementations approach is superior in those cases.
- mrkeen 3y agoI think you have it backward: > But I think the tendency to "lock" behavior into functions and not letting objects define their own behavior is a major shortcoming. "Locking behaviour" happens when you bundle objects & functions together. For instance, an OOP programmer might create a binary tree, mark the fields as private, and expose a public visit() method. If you take away the "objects defining their own behaviour", then you're left with just the binary tree. Then different callers can write their own visit() functions.
- trashburger 3y agoVisibility as a concept is one I also disagree with :) I lump it together with the "OOP" classification in my original comment. I agree with you in this regard. The messages an object is able to respond to should not be restricted because of where it comes from.
- magicalhippo 3y agoI feel visibility should be there, but it should also be possible to bypass. Essentially visibility is a contract, and sometimes you want or need to go beyond. The language should allow this IMO but make it clear that you are bypassing the contract of the original author. Making things unreachable is just a PITA for no good reason.
- isaiahg 3y agoI feel like this is mostly a straw man argument or maybe it comes from a lack of experience in functional programming. Functional programming to me is more of a process to problem solving and less organizational. (Though it can be a part of it) Indeed you can combine OOP and functional styles very easily. Scala is a great language that did just that to great success.
- cageface 3y agoIn non trivial code you often need to make coherent atomic updates across a whole section of your state graph. This is very difficult when the code to update state is fragmented across many classes. For me this is where OO really falls down and more functional approaches shine.
- sirwhinesalot 3y agoMaking everything an object is a cute idea but the fact of the matter is, most of the time data is just data, there's no associated behavior. Lets say you have a Player object who has an Inventory (can also be an object) and an inventory contains items like Swords and Bows etc. When a player wielding a sword takes a swing at an enemy, a particular animation has to play, and sword swing and sword hit sound effect has to play. Which object contains the code that performs the rendering of the animation? Is there an "Animation" object that contains it? Does it call some setPosition/setRotation object in the sword and the player? Does the Animation object know how to call the appropriate Metal/Direct3D/Vulkan APIs or is that another object? Or is the right solution to have an animation subsystem that doesn't care at all about "players" or "swords" and is only concerned with mesh data and how to transform it? Answer: the latter, which is what every game engine that wants to be even remotely efficient does. That's not to say the Smalltalk idea of building larger systems out of smaller systems communicating via message passing is bad or anything, the problem is the OO idea of tying data to behavior. There isn't a single OO codebase out there without POD objects.
- trashburger 3y agoThere is in fact associated behavior; it is just moved off the object in an effort to avoid the after-effects of many languages which implement "OOP". Behavior doesn't have to be part of the object directly, in fact I support your idea that objects can be inert, but behavior inheritance is still an extremely useful concept even within the scenario you propose. As for your specific example, you don't have to add the behavior of actually playing the sound or animating to your `sword` or `player`; I never suggested that the objects themselves having to perform effects being an inherent part of object programming. Maybe you can send `player wieldedWeapon attackSprite` (or `attackMesh`, I'm fond of 2D platformers better :) and then the scene can ask the animation to render itself, same with `player target hurtSound` and `player wieldedWeapon attackSound` and then the sound object can be asked to play itself with a given `audioContext`. Here, we're being as declarative as can be and still giving the objects the freedom to behave however they want within the bounds of the contract they adhere to. So an object programming style does not, in fact, preclude you from making "POD objects"; it only says that the behavior of an object should be exposed through the object itself, rather than reflectively deciding on what the object should do from the outside.
- deleted 3y ago[deleted]