Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> Tight coupling is the basis of oop.

This is not what OOP people are talking about when they talk about tight or loose coupling though.

They are talking about the relationship between classes.



And that’s the fundamental problem. They fail to see that if they took those same methods and made it independent of state then those things are now called functions.

Functions can be moved to different scopes. Functions don’t rely on state to exist.

You can compose functions with other functions to build new functions.

And here’s the kicker. All of these functions did the same thing as the method.

Functions are more modular. A method, is a restricted function that is tightly bounded with state and all other sibling methods.


There's a trade-off.

Functional programs are easier to read, because the structure makes the state transitions and dependencies obvious - you see your dependencies in the arguments list. But it forces you to basically rewrite big parts of your program after even very simple changes.

You had the structure of the program so fine-tuned to the dependencies of every part of the code - that when any dependencies change you have to completely change that structure. It's rewrite-only programming style.

Imperative (and OO) programming idiomatically let you do a bigger mess with side effects, and you know less about the data dependencies just from looking at the function specifications - but it also allows you to do exploratory programming much faster (no need to pass a new argument down 20 levels of your call stack when some code deep down suddenly requires a new argument). And it allows you to modify the behaviour locally without refactoring the whole thing constantly.

If you have a for loop that filters out even numbers and suddenly you want to sum the numbers and find maximum and minimum too - most of the code stays the same.

If you have functional code doing the same and want to modify it in similar manner - it's a completely different code. Most people would just rewrite it from scratch.

And that's just a very small scale example. With larger programs the rewrite gets harder.

That's why big programs are almost never functional.


>If you have a for loop that filters out even numbers and suddenly you want to sum the numbers and find maximum and minimum too - most of the code stays the same.

   x = [1,2,3,4,5,6,7,8]
   even = [i for i in x if x%2 == 0]
   
Now I want to sum the numbers and add maximum and minimum too.

  s = max(x) + min(x)
  res = reduce(lambda acc, y: acc + y, x, 0) 
I achieved your desired goal without rewriting code? The thing is with functional programming all state is immutable, so you can always access intermediary state without modification of the program at all.

It's an improvement on imperative and OO. Because I only needed to add additional code to achieve the additional goal and those additions are modular and moveable. With imperative I would be changing the code and changing the nature of the original logic and none of it is modular and all of it is tightly integrated.


Sure, if you want to have 4 loops where 1 suffices.


The big oh is the same man. It just feels like it’s less efficient but it’s not. Think about it.

And it’s more modular with more loops. If you’re trying to shove 4 different operations into one loop you’re not programming modularly and you’re trying to take shortcuts.


Reduce FPS of your game from 60 to 15, tell players it's the same cause complexity haven't changed.

But it's not even mainly about performance. The structure of the code changes with every requirement change. In a non-artificial code you're doing stuff other than calculating the result, and all the associated state and dependencies now have to be passed to 4 different loops.

While in the ugly non-modular imperative code you add 3 local variables and you're done, everything outside that innermost loop stays exactly the same.

> you’re trying to take shortcuts

Yes, that's the point. I started by admiting FP code is more elegant. But shortcuts are not inherently worse than elegance. They are just the opposite sides of a trade-off.


tbh FP is so heavily modularized that even the concept of "looping" is modularized away from the logical operation itself. In haskell it looks like this:

   f = a . b . c . d
   a1 = map a 
   b1 = map b
   c1 = map c
   d1 = map d
   f2 = a1 . b1 . c1 . d1
Where f is the composition of operations on a single value and f2 operates on a list of values and returns the mapped list.


Gaming and applications that require extreme performance is the only application where Fp doesn’t work well.

> But it's not even mainly about performance. The structure of the code changes with every requirement change. In a non-artificial code you're doing stuff other than calculating the result, and all the associated state and dependencies now have to be passed to 4 different loops.

No. I’m saying the initial design needs to be one loop for every module. You then compose the modules to form higher level compositions.

If you want new operations then all you do is use the operations on intermediary state.

That’s how the design should be. Your primitive modules remain untouched through out the life cycle of your code and any additional requirements are simply new modules or new compositions of unchanged and solid design modules.


>Gaming and applications that require extreme performance is the only application where Fp doesn’t work well.

Sure, if you are considering pure functional programming. But neither pure functional OOP does work well in a performance context.

If you mix imperative/procedural and functional programming you can have clarity, ease of use, ease of change and some performance, too.


Each has its trade offs you lose clarity, ease of use, ease of change and performance even when you mix all styles.


>Functional programs are easier to read, because the structure makes the state transitions and dependencies obvious - you see your dependencies in the arguments list. But it forces you to basically rewrite big parts of your program after even very simple changes.

Disagree. Readability is opinionated so I won't address that but this is an example of functional:

   gg = (x) => x * x * x

   y = 1
   a = (x) => x + 1
   b = (x) => x * 2
   c = (x) => x * x
   d = (x) => x - 4
   f = a . b . c . d
   
   result = f(y)
OOP example:

   class Domain1
      constructor(n: int)
          this.x = n

      def gg()
         this.x *= this.x * this.x      

      def getX() -> int
         return this.x

   class Domain2:
      constructor(n: int)
          this.x = n
      
      def a:
          this.x += 1

      def b:
          this.x *= 2

      def c:
          this.x *= this.x

      def d:
          this.x -= 4

      def f:
         this.a()
         this.b()
         this.c()
         this.d()

      def getX() -> int
         return this.x

  state = Domain(1)
  state.f()
  result = state.getX()   

         
What if realize d and a fits better in Domain1 and I want to compose d and a with gg in the OOP program? I have to refactor Domain2 and Domain1. Or I create a Domain3 that includes a Domain1 and a Domain2

How do I do it in functional programming?

      domain3 = gg . d . a


      #Note I use the fucntion composition operator which means: a . b = (x) => b(a(x)) or a . b . c = (x) => c(b(a(x)))

Functions by nature with the right types are composable without modification. A can compose with B without A knowing about B or vice versa. The same cannot be said for objects.

Try achieving the same goal with OOP.... It will be a mess of instantiating state and objects within objects and refactoring your classes. OOP is NOT modular.

It's pretty clear. One style is more modular than the other. Objects tie methods to state such that the methods are tied to each other and can't be composed without instantiating state or doing complex Object compositions and rewriting the Objects themselves.

In programming you want legos. You want legos to compose. You don't want legos that don't fit such that you have to break the legos or glue them together.

>Imperative (and OO) programming idiomatically let you do a bigger mess with side effects, and you know less about the data dependencies just from looking at the function specifications - but it also allows you to do exploratory programming much faster (no need to pass a new argument down 20 levels of your call stack when some code deep down suddenly requires a new argument). And it allows you to modify the behaviour locally without refactoring the whole thing constantly.

you shouldn't be programming with state ever if you're doing FP. You need to segregate state away from your program as much as possible. State by nature is hard to modularize. State should be very simple generic mutation operations like getValue and setValue, it should rarely ever contain operational logic.


Your examples are inherently functional, and in practice people would do ABCDService that just returns the result. But ignoring that - how is the code changing when you need to call some external API from the c(x) function and handle the credentials, session, errors etc? Real world has external state and we do need to work with it.

> you shouldn't be programming with state ever if you're doing FP. You need to segregate state away from your program as much as possible. State by nature is hard to modularize. State should be very simple generic mutation operations like getValue and setValue, it should rarely ever contain operational logic.

This approach to state management is exactly what is causing the need to rewrite almost everything when requirements change in a functional program.

I like how clean FP code is when it's done. But I hate writing FP code when I'm not 100% sure what needs to be done and what might change in the future. If I could write imperative code with side effects and once I'm done have it transpiled into efficient, elegant, minimized state functional code - that would be great. Maybe it will happen at some point with AI getting better.


> Your examples are inherently functional, and in practice people would do ABCDService that just returns the result. But ignoring that - how is the code changing when you need to call some external API from the c(x) function and handle the credentials, session, errors etc? Real world has external state and we do need to work with it.

Abcdservice is bad because I want to use a b c and d in different contexts. You’re saying in the real world oop promotes a style where you can’t break down your code into legos. With oop you need to glue a b c and d together.

My code is not inherently functional. I literally picked the smallest possible logical operations and interpreted them as either functional or oop. And then I tried to compose the logical operations.

I mean look at it. A b and c are just one or two mathematical operators. If you’re saying this is inherently functional then your saying computing at its most primitive state is inherently functional.

> This approach to state management is exactly what is causing the need to rewrite almost everything when requirements change in a functional program.

Not true. Fp programs segregate state. Look at my code. All the code for fp is stateless. The only state is y=1.

> But I hate writing FP code when I'm not 100% sure what needs to be done and what might change in the future.

You hate what’s inherently better for the future. Fp is more modular and therefore more adaptable for the future. You hate it because you don’t get it.


> If you’re saying this is inherently functional then your saying computing at its most primitive state is inherently functional.

Sure. But computing isn't what most code does.

> Fp is more modular and therefore more adaptable for the future

That is wrong. It's cleaner to read, but it usually requires more lines of code to be changed when requirements change - so it's less adaptable.

Before changes

FP:

   y = 1
   a = (x) => x + 1
   b = (x) => x \* 2
   c = (x) => x \* x
   d = (x) => x - 4
   f = a . b . c . d

   result = f(y)
ugly imperative code:

    y = 1;
    void doABCD() {
       y += 1;
       y *= 2;
       y *= y;
       y -= 4;
    }
Now you want to count how many times you squared numbers larger than 1000.

FP:

   y = [1, 0]
   a = (x) => [x[0] + 1, x[1]]
   b = (x) => [x[0] \* 2, x[1]]
   c = (x) => [x[0] \* x, x>1000 ? x[1]+1 : x[1]]
   d = (x) => [x[0] - 4, x[1]]
   f = a . b . c . d

   result = f(y)
imperative:

    y = 1;
    count = 0;
    void doABCD() {
       y += 1;
       y *= 2;
       if (y > 1000)
           count ++;
       y *= y;
       y -= 4;
    }
Total lines changed in FP - all except 2. Total lines changed in imperative - 3.

Of course you can refactor the FP version to split the part that requires the new state from the other parts. But in any big program that refactor is going to be PITA.

Do you get my point now? I'm not saying imperative is better. It is ugly. But it's faster to adapt to the new requirements.


No way. Imperative is worse.

You're just not using FP correctly. You're trying to do something monadic which is something I would avoid unless we absolutely need an actual side effect.

   y = 1
   a = (x) => x + 1
   b = (x) => x \* 2
   c = (x) => x \* x
   d = (x) => x - 4
   f = a . b . c . d

   result = f(y)
You're doing the refactor wrong let me show you. You have to compose new pipelines that reveal the intermediate values.

   count = 0
   firstPart = a . b
   secondPart = c . d
   countIfGreaterThan1000 = (x, prevCount) => x > 1000 ? prevCount + 1 : prevCount

   n = firstPart(y)
   newCount = countIfGreaterThan1000(n, count)
   result = secondPart(n)
The key here isn't the amount of lines of code. The key here is to see that under FP the original code is like legos. If you want to reconfigure your fundamental primitives you just recompose it into something different. You don't have to modify your original library of primitives. With OOP you HAVE to modify it. doABCD() can't be reused. What if I want something additional (doABCD2) that does the EXACT same thing as doABCD() but now without counting the amount of times something was squared and greater than 1000 but now instead I want it for the amount of times the total was greater than 3?

You can't reconfigure the code. You have to duplicate the code now.

Basically you have to imagine functional programming as pipelines. If you want to add something in the middle of the pipeline, you cut the composition in half and split the pipe. One pipe goes towards the end result of what d outputs and the other pipe goes towards countGreaterThan1000


So your solution to changing 5 lines out of 7 was to do the refactor I wrote about and change 7 lines :)

I agree it's prettier. But it's objectively a larger change than the 3 lines you'd do in the imperative code. And it's pretty much how adapting to changes usually goes with FP. You constantly have to change the outermost structure of the program even if the change in the requirements is localized to one specific corner case.

> What if I want something that does the EXACT same thing as doABCD() but now without counting the amount of times something was squared and greater than 1000 but now instead I want it for the amount of times the total was greater than 3?

> You can't reconfigure the code. You have to duplicate the code now.

I could, but at this point refactoring is warranted.

    y = 1;
    y2 = 1;
    count = 0;
    _ = 0;
    count2 = 0;
    void doABCD(int &y, int &count, int &count2) {
       y += 1;
       y *= 2;
       if (y > 1000)
           count ++;
       y *= y;
       y -= 4;
       if (y > 3)
           count2 ++;
    }
    doABCD(y, count, _);
    doABCD(y2, _, count2);
8 changes. 11 in total for both modifications.

In FP you had 7 lines of code changed for the first refactor

   y = 1
   a = (x) => x + 1
   b = (x) => x \* 2
   c = (x) => x \* x
   d = (x) => x - 4
   count = 0;
   firstPart = a . b
   secondPart = c . d
   countIfGreaterThan1000= (x, prevCount) => x > 1000 ? prevCount + 1 : prevCount
   n = firstPart(y)
   newCount = countIfGreaterThan1000(n, count)
   result = secondPart(n)
and now you'd have sth like

   y = 1
   y2 = 1
   a = (x) => x + 1
   b = (x) => x \* 2
   c = (x) => x \* x
   d = (x) => x - 4
   count = 0
   count2 = 0
   firstPart = a . b
   secondPart = c . d
   countIfGreaterThan = (x, target, prevCount) => x > target ? prevCount + 1 : prevCount
   n = firstPart(y)
   newCount = countIfGreaterThan(n, 1000, count)
   result = secondPart(n)
   result2 = (firstPart . secondPart) (y2)
   newCount2 = countIfGreaterThan(result2, 3, count2)
That's 7 + 6 = 13 lines for 2 changes if I'm counting correctly.

What FP buys you is not deduplication (you can do that in any paradigm) - it's easier understanding of the code.


Let me emphasize it’s not about prettier. Prettier doesn’t matter.

The key is that the original code is untouched. I don’t have to modify the original code. Anytime you modify your original code it means you initially designed poor primitives. It means you made a mistake in the beginning and you didn’t design your code in a modular way. It’s a design problem. You designed your code wrong in the beginning so when a new change is introduced you have to modify your design. This is literally a form of technical debt.

Do you see what Fp solves? I am not redesigning my code. I made the perfect abstraction from the beginning. The design was already perfect such that I don’t have to change anything about the original primitives. That is the benefit of Fp.

Nirvana in programming is to find the ultimate design scheme such that you never need to do redesigns. Your code becomes so modular that you are simply reconfiguring modules or adding modules as new requirements are introduced. Any time you redesign it means there was technical debt in your design. Your design was not flexible enough to account for changing requirements.

Stop looking at lines. In the real world if you modify your code that usually cascades into thousands of changes on dependent code. In Fp I simply link modules together in a different way. The core primitives remain the same. The original design is solid enough that I don’t change code. I just add new features to the design.

Also for your example you misinterpreted what I said. I don’t want to change the original signature of doABCD because it’s already used everywhere in the application. I want a new doABCD2 that does exactly the same as the original. Remove the side effect from the original and add a new side effect to the new doABCD of counting something else.

Do it without duplicating code or refactoring because duplicate code is technical debt and refactoring old code is admission that old code was not the right design. Be mindful that refactoring the signature means changing all the thousands of other code that depends on doABCD. I don’t want to do that. I want new features to be added to an already perfect design.

FP in my opinion, ironically is actually harder to read.


> You designed your code wrong in the beginning so when a new change is introduced you have to modify your design. This is literally a form of technical debt.

Yes. And just like in real life if you want to do business - you have to accept some degree of debt to get anywhere. Trying to predict the future and make the perfect design upfront is almost always a mistake.

> Stop looking at lines

We can't communicate without establishing some objective measures. Otherways we'll just spew contradictory statements at each other. These toy examples are bad, obviously, but the fact that there's basically no big functional programs speaks for itself.

> refactoring old code is admission that old code was not the right design

And that's perfectly fine.

> I want a new doABCD2 that does exactly the same as the original. Remove the side effect from the original and add a new side effect to the new doABCD of counting something else.

According to your definition of "code changed" if I duplicate everything and leave the old lines there - no code was changed which means the design was perfect :)

I don't think we'll get to a point where we agree about this. One last thing I'd like to know is why do you think nobody writes big projects in functional languages?


>Yes. And just like in real life if you want to do business - you have to accept some degree of debt to get anywhere. Trying to predict the future and make the perfect design upfront is almost always a mistake.

And I'm saying FP offers a way to avoid this type of debt all together. You can accept it if you want. I'm just telling you of a methodology that avoids debt: A perfect initial design that doesn't need refactoring.

>We can't communicate without establishing some objective measures. Otherways we'll just spew contradictory statements at each other. These toy examples are bad, obviously, but the fact that there's basically no big functional programs speaks for itself.

Sure then I'm saying lines of code is not an objective measure. Let's establish another objective measure that's more "good": The amount of lines of structural changes made to the original design. It's universally accepted that lines of code aren't really a good measure but it's one of the few quantitative numbers. So I offer a new metric. How many lines of the original design did you change? In mine: 0.

I don't want to write the psuedocode for it, but let's say doABCD() is called in 1000 different places as well. Then in the imperative code you have 1000 lines of changes thanks to a structural change. Structural design changes leads to exponential changes in the rest of the code hence this is a better metric.

That's an objective measure showing how FP is better. I didn't take any jumps into intuition here and I am sticking with your definition of an "objective measure"

>And that's perfectly fine.

That's just opinion. Surely you see the benefit of a perfect initial design such that code never needs refactoring. It happens so often in business that it's normal to refactor code. But I'm saying here's a way where you perfect your design in the beginning. That's the whole point of modularity right? It's an attempt to anticipate future changes and minimize refactoring and FP offers this in a way Objectively better than imperative. If your always changing the design when a new feature was added what's the point of writing modular and well designed code? Just make it work and forget about everything else because it's "okay" to redesign it.

>According to your definition of "code changed" if I duplicate everything and leave the old lines there - no code was changed which means the design was perfect :)

But then you introduced more technical debt. You duplicated logic. What if I want to change the "a" operation. Now I have to change it for both doABCD and doABCD2. Let's assume I have doABCD3 and 4 and 5 and 6 all the way to 20 who all use operation "a" and now they all have to be changed because they all used duplicate code.

Let's not be pedantic. Refactoring code is a sign of technical debt from the past. But also obviously duplicating code is also known to be technical debt.

>I don't think we'll get to a point where we agree about this.

Sure but under objective measures FP has better metrics. Opinion wise we may never agree, but objectively if we use more detailed and comprehensive rules for the metric, FP is better.

>One last thing I'd like to know is why do you think nobody writes big projects in functional languages?

Part of the reason is because of people with your mentality who don't understand. It's the same reason why the US doesn't use metric. Old cultural habits on top of lack of understanding.


> And here’s the kicker. All of these functions did the same thing as the method.

My only question is if you have function x, y and z, how do you restrict function z so it can only be called from function x, but not from function y? If you have classes you can use access modifiers.


You don’t need to restrict functions.

You can still organize functions into groups via libraries or modules. But ultimately that’s just aesthetics.

    ModulaA.funcA
    ModuleB.funcB

FuncA cannot be called from ModuleB. This restriction is possible in terms of organizing code but you can see it’s just naming and ultimately has no effect outside of aesthetic appeal.


Why do you want that if there is no state involved?


I wasn't referring to a pure functional context. I was thinking of a context where you have functions and procedures but no objects.


You can still have modules, for example. They're a way of grouping functions and types.


Exactly. These functions are stateless and thus calling the functions ultimately doesn’t really do anything.




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

Search: