Hacker News new | ask | show | jobs
by erikpukinskis 3499 days ago
I think the readability is a bit of a wash.

But the second one is more debuggable than the first, which I think is even more important than readability.

In the first case, you need to rewrite the control structure to even be able to inspect anything:

    let allPersons = names.map(Person.init)
    {log allPersons[0].name}
    {breakpoint}
    let persons = allPersons.filter { $0.isValid }
There are lots of data structures in this style of programming that don't have any names. Who knows what kind of data structures map and filter create in order to do their work. Are we allocating 2 arrays? 1? None? In the procedural style, everything that exists in memory has a name.

It's also totally unclear what the order of operations is. Are Person.init and $0.isValid alternated? Is the `map` run in full before the `filter` starts? No way to know.

People talk shit about procedural programming, as if it's antiquated. But the core promise of functional programming—that you can stop thinking about the underlying procedures—never seems to fully pan out. So when you inevitably need to start digging under the hood to figure out what your declarative code actually means in practice, you end up having to think procedurally anyway. Now you're thinking procedurally, but your code is declarative, and the runtime is trying as hard as it can to prevent you from knowing what's exactly happening moment to moment.

I think there are specific cases where a declarative interface is the right abstraction. CSS is a good example. But these are narrowly defined domains with relatively clear semantics that get frequent use, so the time it takes to learn the semantics will pay off.

The idea of littering your entire codebase thickly with declarative APIs, each of which has unique control structures that must be understood in order to read code, is not a good approach in my opinion.

This is what Rails is, and it creates a situation where you are captive to your tools: you can do a lot very easily, but you cannot stray far beyond the declarations that your library author overlords have chosen for you, or you quickly find yourself in a space where in order to not shoot yourself in the foot you need to have a huge body of internals in your head.

2 comments

> But the second one is more debuggable than the first, which I think is even more important than readability.

The first is less likely to require debugging in the first place.

> There are lots of data structures in this style of programming that don't have any names.

So you can only reason about things that have names? Now we know where idiomatic Java comes from.

> Who knows what kind of data structures map and filter create in order to do their work.

In most reasonable implementations, the only data structure being created is the final result (a functorial value in map's case, a sequence in filter's case). For example, in SML:

    fun map _ nil = nil
      | map f (x :: xs) = f x :: map f xs

    fun filter _ nil = nil
      | filter p (x :: xs) =
        if p x then x :: filter p xs
        else filter p xs
> But the core promise of functional programming—that you can stop thinking about the underlying procedures—never seems to fully pan out.

Functional programming doesn't promise freedom from procedures. It promises (and delivers) freedom from physical object identities when you only care about logical values.

---

@banachtarski:

Code that's likely to require debugging (say, because it implements tricky algorithms) should be isolated from the rest anyway, regardless of whether your program is written in a functional style or not. Say, in Haskell:

Bad:

    filter (\x -> tricky_logic_1) $
    map    (\x -> tricky_logic_2) $ xs
Good:

    -- Now trickyFunction1 and trickyFunction2 can be
    -- tested in isolation. Or whatever.
    trickyFunction1 x = ...
    trickyFunction2 x = ...
    
    filter trickyFunction1 (map trickyFunction2 xs)
> The first is less likely to require debugging in the first place.

I'm all for functional languages but this scares me a bit. What do you do when you need to debug something and everything ends up being harder to debug but "less likely to need debugging." I've actually run into this situation a number of times and faced with a sea of linked compound expressions, debugging can be a daunting proposition.

It's still a net win in my experience. The minor inconvenience of inserting a few temporary variables to hold intermediate values is much less than the burden of all the additional reading and debugging needed when you spell out every step for everything you want to do.

It's kind of strange to me. We generally acknowledge that not repeating yourself and dividing responsibilities sensibly leads to better code that has fewer bugs and is easier to reason about. And yet when we consider doing the same thing with iteration, we say, "Whoa, hang on. Why can't we just write out the whole thing every time instead of factoring the common bits into a function?"

Comment out all but the first and output the result. If it's what you expected, uncomment next line and output the result. Is it what you expected. Repeat... Debugging is just a specialized form of troubleshooting so the same rules apply. Bring the system to a known good, and increase until you find where it breaks.
You have a ton of these types of statements. Which one do you apply the treatment too? Your approach only allows doing this troubleshooting approach to a single place at a time easily.
How do you debug several places at the same time?

You have a ton of those statements, that's why you organize them in functions, and go debugging high level functions before you go into lower level ones, and then into atomic statements.

Anyway, you'll almost certainly want to do that in an repl. I don't know if swift supports one, if not that would be a real drawback.

They do, it's really slick how well they have it integrated.
> So you can only reason about things that have names?

I think the point was that you can debug things that have names because they are separately watchable.

But apart from that breaking things down and naming them can make for easier comprehension. This is true in written English: Naming actors when explaining something and using an active voice is generally recommended. e.g "The user enters a password and the program encrypts it and stores it in the database" Rather than: "Passwords will be stored encrypted"

But also in mathematics. When working something out it's better to name intermediate values (x,y) and then use them in new equations rather than use the equivalent of a point free style that you sometimes see in functional programs.

> I think the point was that you can debug things that have names because they are separately watchable.

Note I said “reason”, not “debug”. When manipulating algebraic expressions, I don't need to give every subexpression a name - that would be torture!

You're 100% right about debugability. It's the main reason I don't push to use haskell every day. That said, I think we really need to get clever and think of good tools to debug in spite of these difficulties. Most complex bugs are from interacting systems that can't be found by step debugging. For example, the following code is considered pythonic, and for good reason:

    fs = [f(x) for x in xs]
The other commenter's points about being cleaner and less prone to debugging are totally legit. If we can make e.g. list/set comprehensions debuggable, we can probably make other FP idioms debuggable and get the best of both worlds.

Kinda reminds me of microservices, actually. Tough to debug, but good in other ways.