News for hackers. Stuff that matters.
Re:Ask HN: What concepts have you never truly understood? by exdsq on Sunday March 06, 2022 @08:57PM (#30583358)
When you have some spare time write a function that returns a Maybe<Value, Error> and see what a pain it is to compose multiple of them.

Then, try store your programs state by passing it as a function argument to every function so it stays available for the lifetime of the program (no mutable state? This is basically what happens)

After realizing what a pain this is and wishing for boilerplate, look at the State monad. This is what did it for me :)

Re:Ask HN: What concepts have you never truly understood? by eternityforest on Monday March 07, 2022 @12:07AM (#30584420)
I don't see why I would ever want to do the first one in real life, when exceptions exist.

I'm honestly not even sure what function composition is, or why you would compose two functions instead of just explicitly making a new function that calls the two original ones.

The second one makes more sense because of the rumored bug-freeness of side effect free apps, but it seems like for that too, you'd need to understand a lot more than just monads.

Re:Ask HN: What concepts have you never truly understood? by alexmingoia on Monday March 07, 2022 @01:13AM (#30584683)
Function composition is connecting the output of one function to the input of another. In math it is (f ∘ g)(x) = f(g(x))

Or in javascript:

function f(x) { return x + 1 }

function g(x) { return x + 2 }

f(g(1)) === 4

In functional languages like Haskell function composition is made easy, because functions can be partially applied. Partial application means that given a function that takes two arguments, I can call it with one argument and it will return a function that takes one argument which returns the result. Whereas in languages like javascript it is difficult because functions can have different numbers of arguments and there's no way to partially apply them or flip their arguments, so it is difficult to chain functions together. And because javascript functions aren't pure, they may not have any result which can be composed with other functions. In a functional language, functions are more like the functions in math. In most non-functional languages, functions are more like a procedure of commands. For example, the javascript function logMe(x) { console.log(x) } doesn't return anything. So it can't be composed functionally, and is not like a "real" function.

Re:Ask HN: What concepts have you never truly understood? by eternityforest on Monday March 07, 2022 @01:46AM (#30584870)
JavaScript has closures. When you want to partially apply something(Which is technically never, because we would probably describe it in different words and think of it differently) we just make a new function.

When we want to "compose" two functions, there's f(x){return a(b(x))}.

It doesn't matter if the signatures are different, you don't need any nice logical idea of composition, you just explicitly build the new function that does what you want.

I would say that is a lot easier than real composition, because real composition requires you first learn to see and think in terms of patterns you would want to compose in the first place.

Also, function chaining doesn't use intermediate variables. Named variables are like free documentation. With composition you have to actually remember what you're doing. Making everything explicit makes it easier to comment what's happening.

Regular programming is to math as cooking is to chemistry. Functional brings the math back to programming. Math is useful but also hard.

In OOP, more often you're doing the equivalent of:

if crinkle(a(b(x,68), modes.FOO).bar.baz(9)): x = foo() else: x = wipe(7).bar()

And I think it's a lot nicer to just spell it out with meaningful variable names, instead of trying to compose it like an equation.

Re:Ask HN: What concepts have you never truly understood? by Zababa on Monday March 07, 2022 @10:56AM (#30589290)
Not every step has a meaningful name. And usually you already have the name of the function. For example:

    let batter = mix(water, flour)
    let saltedBatter = add(salt, batter)
    let risenSaltedBatter = letToRise(saltedBatter)
    let bread = bake(batter)
on the other hand:

    let bread = bake(letToRise(add(salt, mix(water, flour)))
The first example leads to awkward names and useless variables. The second example feels cleaner to me. Interesting that you take cooking as an example: recipes usually don't name most of the intermediate steps, they describe the actions you take and the ingredients you use. Most recipes also use composition a lot. You start with your actions/functions (cut, cook, bake, dice, etc) and you do a recipe out of them. You don't create new "functions" (actions) specifically for each recipe.
Re:Ask HN: What concepts have you never truly understood? by eternityforest on Monday March 07, 2022 @05:27PM (#30594201)
Recipes are a lot closer to configuration DSLs than code though.

You have standard functions like bake and boil, and they don't tell you to use clean utensils or how to turn the stove on, because an cooking is basically just a few dozen standard functions, end condition rules, etc.

If you think of them as programs, the composition they use isn't the same as functional languages, because the individual steps are unstructured and there's no generalized abstract pattern defining what a RecipeStep is or what composing two of them means.

You wouldn't say add(flour(add(salt,empty))), you would say "add flour, salt, water, and yeast".

"add" can take either an ingredient or a list, and it's pretty encapsulated, since it dynamically selects a way to measure the ingredient based on what it is, what utensils you have, etc.

It could trigger dozens of unrelated code paths based on state, and probably includes error recovery.

Functional might be nice for implementing the internals of that add() function, but in OOP the idea seems to be that it's a pretty minor detail, since add() is only written once and rarely changed, but used millions and millions of times all over the world for a decade.

Usually when people talk about "the beauty of functional" I get the impression that they're not really doing a whole lot of "One really smart function that knows exactly what to do" level abstraction.

It seems like they'd literally be composing together their own variations of what "add" is, as needed from the raw findMeasuringStrategy and checkPantry functions instead.

Re:Ask HN: What concepts have you never truly understood? by lelanthran on Tuesday March 08, 2022 @02:43AM (#30597464)
I feel, in your example, that the first code block is infinitely more useful to the maintainer than the second. It literally tells the reader what each step is *supposed* to do.

That second example tells the reader that it is doing, which may or may not be what it was supposed to be doing.

As a maintainer, I prefer knowing that:

    (add(salt, mix(water, flour))
is supposed to result in a salted batter, which the first code block tells me but the second one does not.
Re:Ask HN: What concepts have you never truly understood? by Zababa on Tuesday March 08, 2022 @05:33AM (#30598449)
My whole point is that sometimes intermediate steps don't have good names. I often have to apply multiple transformations to a collection, but only the first and last step have a name because the intermediate steps are just that. "salted batter" is not a thing, it's just a placeholder name because you needed a name. You will never use "salted batter", you'll only use ingredients and bread.
Re:Ask HN: What concepts have you never truly understood? by lelanthran on Tuesday March 08, 2022 @08:31AM (#30599646)
> You will never use "salted batter", you'll only use ingredients and bread.

For write-only code, sure, that's great.

It's irrelevant when I am tracking down a bug - the label 'saltedBatter' tells me what that intermediate result is supposed to be.

If, when debugging, I find that `add(salt, batter)` results in something else[1], using your convention it is literally impossible to tell that the value is supposed to be `saltedBatter`.

There is literally no way to tell if the intermediate values are correct. Maybe each caller in the composition chain can throw an Exception?

The point is, the variable names are helpful.

> but only the first and last step have a name because the intermediate steps are just that.

That describes the entire program :-)

In all seriousness though, it's because you intend to describe steps in the process that throwing away all labels for the intermediate steps makes sense.

You're describing verbs. We don't need to know the in-between verbs for a verb. When you call `driveTo()`, it is not necessary to know all the verbs involved in that action, it is enough to know that it will attempt a certain action.

Verbs answers the "what we are doing" questions.

Variable names answer the "Why we are doing it" questions - `saltedBatter = foo (bar(), baz);` tells the reader why foo is being called.

We don't even need to know the steps involved or even what the other variables (used as parameters) should be. Assigning the result of a function to a variable ensures that the maintainer knows if that variable ever holds the wrong value.

I think where a lot of this value (variable-names) is lost is in enterprise development - the names are never helpful, they're just a differently-cased version of the type. Like these gems I see daily:

    SomeClass someClass = foo();
    List<OtherClass> otherClassList = bar();
    ....
    return baz(someClass, otherClassList);
So, yeah, I see your point if all you ever see is enterprise-grade naming conventions. OTOH, if those above two lines were written as:

    SomeClass needle = foo();
    List<OtherClass> haystack = bar();
    ...
    return baz(needle, haystack);
you, as the maintainer, know exactly what that last line is supposed to do, and if it isn't doing that, then you know it's a bug.

Compare with this:

    return (foo(), bar());
Or, if you wanted descriptive function names:

    return (calculateResult(), userSubmittedList());

Brevity is not a virtue in programming languages and/or programs. It's probably why Haskell is still not even a rounding error as far as popularity goes.

[1] Maybe `doubleSaltedBatter`, in case an upstack caller used `saltedWater` in place of `water`, or perhaps `pepperedBatter` in case a caller switched out the salt for pepper.

Re:Ask HN: What concepts have you never truly understood? by ParetoOptimal on Tuesday March 08, 2022 @05:31PM (#30608179)
> Brevity is not a virtue in programming languages and/or programs.

Real world Haskell code isn't the codegolf you are implying.

In fact, I think real world Haskell devs overcorrect and make code too verbose.

Also, "brevity is not a virtue in programming languages" is something I disagree with.

Walls of verbose code are just so damn mentally taxing. Especially hen you finish reading and saybto yourself "all that code just to do X?".

Re:Ask HN: What concepts have you never truly understood? by ParetoOptimal on Tuesday March 08, 2022 @05:37PM (#30608259)
> Functional brings the math back to programming. Math is useful but also hard.

So is working with an edifice of unprincipled ad-hoc abstractions, but many seem to prefer pushing that debt on tomorrow them or others.

Re:Ask HN: What concepts have you never truly understood? by 8note on Monday March 07, 2022 @01:23AM (#30584731)
JavaScript has bind() on functions which works well enough. You can partially apply arguments, but I don't think you can project them to a different order.
Re:Ask HN: What concepts have you never truly understood? by exdsq on Monday March 07, 2022 @01:21AM (#30584727)
Maybe this article will explain my point about errors a little, it also covers function composition, and I think is approachable. I guess the only advice I have for you (or anyone) with monads is to look at concrete examples first and not care about the theoretical bits. Intuition comes from use. Is it useful to know in non-functional languages? Probably not; you'll use them without realising and probably make them yourself without realising!

https://medium.com/swlh/monads-in-python-e3c9592285d6

Re:Ask HN: What concepts have you never truly understood? by 8note on Monday March 07, 2022 @01:20AM (#30584717)
A function that can return a value or throw an exception is pretty similar to Maybe<value,error>

I'm not clear why the next function in the pipeline would need to know about the error rather than the caller, but that happens with promise chains too

Re:Ask HN: What concepts have you never truly understood? by Zababa on Monday March 07, 2022 @10:34AM (#30588954)
> I don't see why I would ever want to do the first one in real life, when exceptions exist.

Exceptions usually aren't typechecked.

Re:Ask HN: What concepts have you never truly understood? by eternityforest on Monday March 07, 2022 @05:28PM (#30594216)
Lots of languages only let you throw stuff if explicitly declared.
Re:Ask HN: What concepts have you never truly understood? by Zababa on Tuesday March 08, 2022 @02:16AM (#30597273)
Which ones? I'm not aware of any that has only checked exceptions, at least in the mainstream ones.