3 ms·
> 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 wha
by ajuc 2y ago
> 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.
- ninetyninenine 2y agoNo 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
- ajuc 2y agoSo 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.
- ninetyninenine 2y agoLet 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.