News for hackers. Stuff that matters.
Re:Solid Relevance by lerptime on Monday October 19, 2020 @02:02PM (#24828973)
Because you cannot predict the future of your program. Coupled logic that seems correct now may need to be uncoupled in the future.

By having every sector of logic be ungrouped it leads to maximum reusability for the future.

Technical debt is at the lowest possible level in projects where every piece of logic is uncoupled. In fact the very phenomenon of Technical debt arises from logic that's hard to uncouple. By having every sector of your program be a lego brick you are preparing your program for an inevitable future where the structure must be reconfigured.

Grouping logic together is like super gluing lego bricks together because it seems like they belong together.

There's a term for this style of coding that's actually negative. It's called ravioli code. But the negativity mostly applies to when this style of coding is used in OOP.

Re:Solid Relevance by AnimalMuppet on Monday October 19, 2020 @02:35PM (#24829404)
Very, very wrong.

> In fact the very phenomenon of Technical debt arises from logic that's hard to uncouple.

No, one kind of technical debt arises that way. Not the only kind. There's another kind that arises when everything is in such small pieces that nobody can tell how the pieces relate to each other. You can easily understand each piece, but you have to go through a chain of a bazillion function calls to see what's actually going on.

> By having every sector of your program be a lego brick you are preparing your program for an inevitable future where the structure must be reconfigured.

This is like economists, who have correctly predicted nine of the last two recessions. Sure, you're prepared for when the change comes (if you can find where to make it - see above). You've also prepared for all the changes that never come. That's rather wasteful.

> Because you cannot predict the future of your program. Coupled logic that seems correct now may need to be uncoupled in the future.

Sure, it may. And when that happens, we'll decouple it. And in the meantime, we'll enjoy the coupling that, at the moment, is the correct thing.

See, this is all in the context of SOLID, the very first piece of which is "Single Responsibility". We don't couple things on a whim.

Re:Solid Relevance by lerptime on Monday October 19, 2020 @03:02PM (#24829687)
No you're very wrong.

>No, one kind of technical debt arises that way. Not the only kind. There's another kind that arises when everything is in such small pieces that nobody can tell how the pieces relate to each other. You can easily understand each piece, but you have to go through a chain of a bazillion function calls to see what's actually going on.

All this logic must exist regardless of whether it's coupled or uncoupled. You do not alleviate the logical burden by coupling logic together or uncoupling the logic. If you call this "technical debt" then you cannot get rid of it. Why even call it debt then as logic is intrinsic to application.

In fact decoupling logic can improve readability as it marks each piece of logic with a contextual function name. Either way you do not alleviate complexity by coupling logic. You alleviate this complexity problem by providing logical layers, using namespaces, and using good function naming.

Coupling logic into Objects does nothing objectively as it does not eliminate complexity it only makes sure that the complexity is LESS modular.

Let's be clear though. I am saying that the lower levels of your application should be ALL uncoupled logic. Then you build logical layers on top of your primitives that consists of compositions of your logic that build higher and higher. So a person on layer 5 only needs to go to layer 4 to understand it, and does not need to go to layer 1 to understand every primitive.

>This is like economists, who have correctly predicted nine of the last two recessions. Sure, you're prepared for when the change comes (if you can find where to make it - see above). You've also prepared for all the changes that never come. That's rather wasteful.

Analogies don't offer proof of an argument. Additionally it's not a good analogy. Economists don't completely understand what causes recessions and how to prevent them. We do, however, completely understand the difference between uncoupled logic and coupled logic.

>Sure, it may. And when that happens, we'll decouple it. And in the meantime, we'll enjoy the coupling that, at the moment, is the correct thing.

This is the main point of tech debt. It is hard to decouple. You can have all logic decoupled at the lowest layer and still have everything be readable. I think the previous sentence is just a type of programming you haven't dealt with. You've never wrote a program with decoupled logic that was readable. Think deeper. There is no reason why coupled logic should be more readable then decoupled logic. The Logic still exists either way, you either compose two modules and give it a name or you don't even use modules shove all your logic into a module with a single name.

>See, this is all in the context of SOLID, the very first piece of which is "Single Responsibility". We don't couple things on a whim.

The problem is OOP does this all the time. It couples state with logic. These two things should be uncoupled.

Re:Solid Relevance by to11mtm on Monday October 19, 2020 @04:13PM (#24830446)
> Let's be clear though. I am saying that the lower levels of your application should be ALL uncoupled logic. Then you build logical layers on top of your primitives that consists of compositions of your logic that build higher and higher. So a person on layer 5 only needs to go to layer 4 to understand it, and does not need to go to layer 1 to understand every primitive.

Yep. If you do this right you only need to understand one or two layers at a time. Great example of this is the Akka Toolkits for JVM and .NET; Every time I've hacked on a given subsection, the amount of domain knowledge I've had to have was 'Basics anyone using Akka has to know' and then just the layer I was working with and maybe one other.

> The problem is OOP does this all the time. It couples state with logic. These two things should be uncoupled.

Agreed. And dare I say, when you start going from coupled state+logic, and go more towards functional... you'll find you can possibly shed a layer or two in your overall design.

Re:Solid Relevance by AnimalMuppet on Monday October 19, 2020 @05:38PM (#24831312)
You seem to think that FP is the one right answer. You are mistaken. There are places where it's the right answer, or at least a very good one - the best definition I've seen is when your program looks like a pipe.

And there are places where FP is the wrong answer - where your program has state, and there are multiple parts to the state, and those parts have to be kept in sync with each other. At that point, bundling the data to the functions is warranted, because it limits the number of functions that can put the data in an inconsistent state.

And if you're going to say "don't structure the data that way", well, there are times when that's the nature of the problem, not just the nature of the program to solve the problem.

Re:Solid Relevance by nendroid on Monday October 19, 2020 @06:35PM (#24831786)
>You seem to think that FP is the one right answer. You are mistaken. There are places where it's the right answer, or at least a very good one - the best definition I've seen is when your program looks like a pipe.

All programs are pipes from IO to IO or from one state to another state. Purity lives in the pipes, impurity lives in the IO nodes between the pipes. The goal is to keep the impurity as small as possible and have most of your logic live in the pipe, as combinators are the most composeable primitive. I also never said FP is the one right answer. Most programs need to modify state. In FP everything is immutable so your program cannot be pure FP in most cases. What I am saying is to segregate state and IO from logic. Example:

    void addOneToState()
can be broken down into

    int addNTo(int n, int m)

    void changeStateTo(int x)
I have split the code above into a method that is devoid of logic and only updates state and logic that lives as a stateless combinator. All programs are made from a series of pipes and tubes. You just need to find the pipe and modularize it and seperate it from the parts that can't be pipes.

>At that point, bundling the data to the functions is warranted.

It's never warranted. There is no benefit. Whether you couple it or uncouple it from data the logic still exists, the only difference is coupling. Adding coupling does not improve your code in any other way other than adding coupling.

>And if you're going to say "don't structure the data that way", well, there are times when that's the nature of the problem, not just the nature of the program to solve the problem.

There is no data structure that has to be coupled with logic. However you structure your data it can always be decoupled from logic. Always.

>where your program has state, and there are multiple parts to the state, and those parts have to be kept in sync with each other.

This doesn't change anything.

   State makeNewState (State oldState) {
        newY = addNTo(oldState.y, 1)
        return New State {
              x = oldState.x
              y = newY
              somethingToBeInSyncTo = newY
        }
   }

   void updateState(State x)
y is still in sync with somethingToBeInSyncTo.
Re:Solid Relevance by AnimalMuppet on Monday October 19, 2020 @07:15PM (#24832120)
"I also never said"? Yeah, I suspected that you and lerptime were the same person.

> It's never warranted. There is no benefit.

When you make such a statement, you sound like someone with fairly limited experience. In particular, from your example, I suspect that you've never worked in a multi-threaded embedded system. somethingToBeInSyncTo, done that way, means that any other thread holding a reference, holds a reference to the old value, while this thread has the new one. And updating running threads with new values on the fly is... problematic.

There's a reason why some programs are not written the way you propose...

Re:Solid Relevance by nendroid on Monday October 19, 2020 @08:20PM (#24832594)
>Yeah, I suspected that you and lerptime were the same person.

to get around the post limit. Keep it on the DL.

>When you make such a statement, you sound like someone with fairly limited experience. In particular, from your example, I suspect that you've never worked in a multi-threaded embedded system. somethingToBeInSyncTo, done that way, means that any other thread holding a reference, holds a reference to the old value, while this thread has the new one. And updating running threads with new values on the fly is... problematic.

I find that you're the one who seems inexperienced. My example is thread safe ... makeNewState is a combinator. It makes no mutations. Like IO, and mutations, the problems with threading happen with mutation. Thus, locks and other procedural commands used to deal with threading live in the void IO function.

We're more referring to coupling functions with external data and mutations via objects. If you're talking about synchronizing two threads, this again needs to be handled with parent IO function outside the logic of your pipeline combinators. You can still segregate logic away from threading and IO.

My combinator in this case is useable for your example case of synchronizing two threads. If you organized your code in ways where my combinator will be unusable than you have failed to decouple your logic away from threading, IO and mutation.

Re:Solid Relevance by AnimalMuppet on Tuesday October 20, 2020 @12:25PM (#24839262)
> I find that you're the one who seems inexperienced.

35 years professional software engineering, 30 years in embedded. I'm going to guess that you have less.

Your way of organizing code works great, on paper. Maybe on a small project. For a real embedded system, with multiple threads, with state data being used everywhere... your approach doesn't make much sense. It makes a much worse design than shared mutable state. (Yes, shared mutable state is in fact as evil as you have think it is. It's still better than trying to make your alternative work in that environment.)

Why is state data used everywhere? Because embedded systems often respond in different ways depending on the state of external inputs, and don't go read the state at the time they make the decision on how to respond. If those decisions are spread through the bulk of the code, there's really not much left to put in your combinator. And there's no point in trying to add that paradigm to a system that's going to have tons of shared mutable state anyway.

Re:Solid Relevance by nendroid on Tuesday October 20, 2020 @12:57PM (#24839706)
>35 years professional software engineering, 30 years in embedded. I'm going to guess that you have less.

Doesn't change the fact that you're wrong and 35 years of programming doesn't ensure that you're a good developer. There are examples of crap code and patterns that have been around perpetually.

>Why is state data used everywhere? Because embedded systems often respond in different ways depending on the state of external inputs

State being everywhere doesn't preclude it to being segregated from logic.

>If those decisions are spread through the bulk of the code, there's really not much left to put in your combinator. And there's no point in trying to add that paradigm to a system that's going to have tons of shared mutable state anyway.

The decision is what goes in the combinator.

   #IO functions.

    Data getIOFromModuleA()
    void sendIOToModuleA(Response r)

    Data getIOFromModuleB()
    void sendIOToModuleB(Response r)
       
    ....


   # combinators, reuseable in both module A and module B because of segregation from IO and State

    Decision makeDecision(Data x)
    Response createResponse(Decision x)
It's the same pattern for State. Just replace the word IO with State. Response can be anything, it can be the entire new state of the program. It can represent a delta of a change, it can be a response specific to a module. Also again, all threading commands go into State or IO functions.

OOP fails to segregate code this way and if you don't organize your code like this then you have 35 years of experience of navigating someone elses mess or your own.

Re:Solid Relevance by AnimalMuppet on Tuesday October 20, 2020 @01:18PM (#24839964)
So I said you were inexperienced. You said I was. I listed my experience. You said it was the wrong experience, but conspicuously declined to state your own.

So I'll just leave you with this. "In theory, there is no difference between theory and practice. In practice, there is." You've got your theory. Nice theory. Use it for a decade of actual real-world work, and let me know how it works out. I'll be especially interested if any of that work is in embedded.

Re:Solid Relevance by nendroid on Tuesday October 20, 2020 @02:31PM (#24840796)
I said it as a minor minor retort. You want the full story? The full story is I don't care for experience. I find experience doesn't correlate with actual skill. Many experienced developers act and seem very inexperienced which is what I said about you: You "seem inexperienced" because you act that way. The very act of bringing up experience is off topic and a sign of lack of maturity.

If you have to know I'd say I have about 15 years of experience in research, gaming, embedded and web. I will say that the pattern I described can be followed in every one of those fields but it's a niche pattern because it is indeed promoted by people who do FP.

I don't know why you focus on embedded, but embedded people tend to follow these patterns the least due mostly to C++. The overall promoted pattern in C++ is to pass references everywhere and it's highly unlikely that someone in that world will follow this pattern because you have to go out of the way to do it and learn about it. It requires a disciplined approach.

The big problem is if you need to slightly modify a bunch of values in an array of 100 objects. It's hard to figure out the right way to do it in a combinator. The common pattern used nowadays is instead of having combinators handle it, have the combinators generate logical instructions to send to the IO function similar to how in web development your stateless servers pass SQL instructions to the SQL server. The IO function processes the instructions and handles the threading, mutation and IO. Here's a library that does this:

   https://github.com/google/cpp-frp <---- real world example.
Nothing I said is "theory" in the sense that you put it. These are rare but actual patterns that are used successfully by people in the know. You are not in the "know" despite your experience. As a result for most of your program experience you've just been dealing with and fixing mess after mess after mess.
Re:Solid Relevance by AnimalMuppet on Tuesday October 20, 2020 @05:17PM (#24842427)
"It is useless to have a conversation with those who will not listen." Neither of us are listening, because we're both sure we're right. So I'm out.

You get the last word, if you want.

Re:Solid Relevance by nendroid on Tuesday October 20, 2020 @05:29PM (#24842529)
Don't be stupid. I'm listening to you. I'm actually responding to you. You say theory is different from the real world so I present you with a real world solution and examples of how it works with embedded specifically. Your job should you choose to continue is to use other real world examples to counter my arguments. That's all, if you convince me it's done, If i convince you it's also done. Until then leaving the discussion before it's resolved is pointless.

Don't back out, I'm not fighting for the last word. I'm having a discussion with you in hopes you can convince me. But you haven't really said anything substantial other than things along the lines of "Trust me I have lots of experience, your stuff is just theory."

If you start introducing the real arguments then this discussion can actually go somewhere.

Re:Solid Relevance by AnimalMuppet on Tuesday October 20, 2020 @06:39PM (#24843033)
Fine.

  void handleEvent1() {
    if (currentState == state1) {
      currentState = state2;
      setHardwareState2();
    } else {
      currentState = state3();
      setHardwareState3();
    }
  }
You can move state to the boundary, and IO to the boundary, and then... what's left? It was all boundary.

A bit more realistically, here's an automobile cruise control that uses the same button for "reduce set speed" and "set the speed". (Don't blame me, I've had more than one car with that exact setup).

  void buttonXEvent() {
    if (cruiseControlEnabled) {
      if (cruiseSpeedSet) {
        cruiseSpeed--;
      } else {
         cruiseSpeed = currentSpeed;
         cruiseSpeedSet = true;
      }
      writeSpeedToEngineComputer(currentCruiseSpeed);
    }  // else do nothing
  }
How would you write this in your approach?
Re:Solid Relevance by nendroid on Tuesday October 20, 2020 @07:47PM (#24843475)

  //combinators
  CruiseSpeed = int

  CruiseSpeed getNextCruiseSpeed(bool cruiseControl, bool cruiseSpeedSet, int cruiseSpeed, int currentSpeed) {
      if cruiseControl && cruiseSpeedSet
         return cruiseSpeed - 1
      elif cruisControl && !cruiseSpeedSet
         return currentSpeed
      else
         return cruiseSpeed
  }

  bool getNextCruiseSpeedSet(bool cruiseControl, bool cruiseSpeedSet){
      return cruiseControl && !cruiseSpeedSet ? true : cruiseSpeedSet
  }

  //IO functions
  void getCurrentSpeed()
  bool isCruiseSpeedSet()
  void setCruisSpeedSet(bool cruiseSpeedSet)
  void writeSpeedToEngineComputer(CruiseSpeed x)
  void handleButtonPress(bool cruiseControl, bool cruiseSpeedSet, int cruiseSpeed, int currentSpeed){
     speed = getNextCruiseSpeed(cruiseControl, cruiseSpeedSet, cruiseSpeed, currentSpeed)
     cruiseSpeedSet = getNextCruiseSpeedSet(cruiseControl, cruiseSpeedSet)
     writeSpeedToEngineComputer(speed)
     setCruiseSpeedSet(cruiseSpeedSet)
  }
Your example uses free variables extensively. It means that all your logic cannot move outside of the context of the free variable which means they cannot be reused. Even IO functions should not use external context.

Combinators can be reused everywhere.

Re:Solid Relevance by AnimalMuppet on Tuesday October 20, 2020 @08:12PM (#24843646)
I see no respect in which your solution is superior.

By the time you implement getCurrentSpeed(), etc., you're going to have a fair amount more code than me. (writeSpeedToEngineComputer() doesn't count, because I need to implement that also.)

Your reason for doing so is, I think, in your last two paragraphs. But this isn't particularly reusable code - I'm not going to use getNextCruiseSpeed() or handleButtonPress() in the windshield wiper controller. It's pretty much tied to the specific use. (There may be a handleButtonPress() in the windshield wiper controller, but I can't reuse this one.)

So you offer me something that is more verbose, based on a benefit that I can't actually benefit from.

(I presume that getCurrentSpeed() returning void was just because of quick typing.)

Re:Solid Relevance by nendroid on Tuesday October 20, 2020 @09:39PM (#24844074)
The point was to prove that I can pull the combinator out of your unsegregated code and moularize IO away from logic.

The other point of this was to improve code modularity in every corner of your code. Why is this good? If all your logic can be shifted around then that means that you can reuse code to anticipate future changes.

The most insidious form of technical debt is unanticipated technical debt. You encounter it when a new requirement makes you realize that you can't reuse segments of your code. You realize that you grouped things together incorrectly. You failed to have the foresight to make your code more modular.

It's a problem people think they can solve in hindsight but they can't in reality because they cannot predict the future so they will inevitably group code incorrectly. And therein lies the problem: grouping. Don't group your code. Make all your logic modular and just organize your code with superficial schemes like namespaces.

The problem with OOP is that the very nature of an object is a grouping. OOP is all about grouping the code around shared mutable variables within a context. You are forced to assume a grouping and you may be right in some cases (like cruisecontrol) but you will inevitably be mistaken in many other cases. This "mistake" is the origin of the most insidious form of technical debt.

The other problem with OOP is that they think that this style of programming is modular. They have no idea how un-modular it is. So they use it extensively with patterns thinking they're improving modularity.

The most modular primitive is the combinator. That is why to prevent technical debt as much as possible, the key is to move as much code as possible into the combinators.

Re:Solid Relevance by jdmichal on Monday October 19, 2020 @03:13PM (#24829786)
I think you're basically describing the idea of having pure computations on pure data objects as much as possible? In which case, I believe that's fairly idiomatic in functional languages. And it's becoming more common in languages like C# and Java that started out OOP but have introduced enough functional niceties to make that style more idiomatic. I've been doing it in Java for at least three years. Here's a comment I wrote about it from then:

https://news.ycombinator.com/item?id=14687947