[HN Gopher] Functional Programming Lessons Conclusion
___________________________________________________________________
Functional Programming Lessons Conclusion
Author : speckx
Score : 40 points
Date : 2025-04-14 15:40 UTC (7 hours ago)
(HTM) web link (jerf.org)
(TXT) w3m dump (jerf.org)
| taylorallred wrote:
| I mostly agree with the sentiments in this article. I was once an
| extremely zealous FP acolyte but eventually I realized that there
| are a few lessons worth taking from FP and applying to more
| conventional programming: 1. Pure functions are great to use when
| you have the opportunity. 2. Recursion works great for certain
| problems and with pure functions. 3. Higher order functions can
| be a useful shorthand (but please only use them with pure
| functions). Otherwise, I think simple, procedural programming is
| generally a better default paradigm.
| Tainnor wrote:
| > Otherwise, I think simple, procedural programming is
| generally a better default paradigm.
|
| I think this is almost the opposite conclusion from the one TFA
| (and in particular the longer form article linked elsewhere
| here), which is more like: most standard imperative programming
| is bad and standard (pure) FP is at least slightly better, but
| people generally don't draw the right conclusions about how to
| apply FP lessons to imperative languages.
| zactato wrote:
| I've always thought that there should be mutability of objects
| within the function that created them, but immutability once the
| object is returned.
|
| Ultimately one of the major goals of immutability is isolation of
| side effects.
| tremon wrote:
| How does this work out for functions in the middle of the call
| stack? Can the objects a function creates be mutated by
| functions they call? Phrased differently, can functions modify
| their input parameters? If a function returns one of their
| input parameters (modified or not), does that mean the calling
| function can no longer mutate it?
|
| Maybe I'm discarding this too readily, but I don't think this
| idea of "local mutability" has much value -- if an object is
| mutable, the compiler and runtime has to support mutation and
| many optimizations are impossible because every object is
| mutable somewhere during their lifetime (and for objects
| created in main, they're mutable for the lifetime of the
| program).
| jerf wrote:
| If we include as a axiom for the purposes of this
| conversation that we must be able to refactor out any part of
| the "constructor" and that the refactored function must have
| the same mutation "privileges" as the original creator, which
| I think is fairly reasonable, this leads you in the direction
| of something like Rust I think, which can construct objects
| and lend bits and pieces of it out in a controlled manner to
| auxiliary functions, but the value being returned can still
| have constraints on it.
| taeric wrote:
| I mean, this isn't that different from any number of things
| you do in real life? You took a car to some destination. It
| is assumed you didn't change the engine. Took it to a
| mechanic, maybe they did?
|
| More, many modifications are flat out expected. You filled
| out a job application, you probably don't expect to change
| the job it is for. But you do expect that you can fill out
| your pieces, such that it is a modifiable document while you
| have it. (Back to the car example, it is expected that you
| used fuel and caused wear on the tires.)
|
| As annoying as they were to deal with, the idea of having
| "frozen" objects actually fits really well with how many
| people want to think of things. You open it to get it ready
| for use. This will involve a fair bit of a setup. When done,
| you expect that you can freeze those and pass off to
| something else.
|
| Transactions can also get into this. Not surprising, as they
| are part of the vocabulary we have built on how to deal with
| modifications. Same for synchronization and plenty of other
| terms. None of them go away if you just choose to use
| immutable objects.
| chowells wrote:
| Local mutability is fantastic and practical... In a language
| like Haskell where the type system tracks exactly what values
| are mutable and all mutation is precisely scoped by functions
| that freeze the value they generate in a way that prevents
| leaking.
|
| In a language that isn't so precise, it's a lot harder to get
| value from the idea.
| Tainnor wrote:
| You can get value out of local mutability in languages like
| Scala or Kotlin. Variables can be declared mutable in the
| scope of a function, and their value can then later be
| assigned to a non-mutable variable, for example.
| Collections also come in mutable and immutable variants
| (although this has some pitfalls in Kotlin).
| williamdclt wrote:
| I have to say I don't understand your point! The parents
| comment is both clear and a reasonable, common approach of
| programming
|
| > Can the objects a function creates be mutated by functions
| they call?
|
| No
|
| > can functions modify their input parameters?
|
| No
|
| > If a function returns one of their input parameters
| (modified or not), does that mean the calling function can no
| longer mutate it?
|
| No. Because the called function isn't allowed to mutate its
| inputs, there's no problem for the caller to mutate it. It's
| irrelevant whether the input was also an output of the called
| function as it cannot mutate it anyway.
|
| I suppose you can get into race conditions between caller and
| callee if your language provides asynchronicity and no
| further immutability guarantees. Still, you eliminated a
| whole lot of potential bugs
| BWStearns wrote:
| > I consider [having a big benefit at 100% vs an 80/20 rule] a
| characteristic of type systems in general; a type system that you
| can rely on is vastly more useful than a type system you can
| almost rely on, and it doesn't take much "almost" to greatly
| diminish the utility of a given type system.
|
| This! This is why I don't particularly care for gradual typing in
| languages like Python. It's a lot of extra overhead but you still
| can't really rely on it for much. Typescript types are just
| barely over the hump in terms of being "always" enough reliable
| to really lean on it.
| dimal wrote:
| I agree with the 100% rule. The problem with Typescript is how
| many teams allow "any". They'll say, "We're using TypeScript!
| The autocomplete is great!" And on the surface, it feels safe.
| You get some compiler errors when you make a breaking change.
| But the any's run through the codebase like holes in Swiss
| cheese, and you never know when you'll hit one, until you've
| caused a bug in production. And then they try to deal with it
| by writing more tests. Having 100% type coverage is far more
| important.
| klabb3 wrote:
| In my rather small code base I've been quite happy with
| "unknown" instead of any. It makes me use it less because of
| the extra checks, and catches the occasional bug, while still
| having an escape hatch in cases of extensive type wrangling.
|
| The other approach, having an absolutist view of types, can
| be very constraining and complex even for relatively simple
| domain problems. Rust for instance is imo in diminishing
| returns territory. Enums? Everyone loves them, uses them
| daily and even write their own out of joy. OTOH, it took
| years of debate to get GATs implemented (is it now? I haven't
| checked), and not because people like and need them, but
| because they are a necessary technicality to do fundamental
| things (especially with async).
| WorldMaker wrote:
| Typescript's --strict is sometimes a very different
| ballgame from default. I appreciate why in a brownfield you
| start with the default, but I don't understand how any
| project starts greenfield work without strict in 2025. (But
| also I've fought to get brownfield projects to --strict as
| fast as possible. Explicit `any` at least is code
| searchable with the most basic grep skills and gives you a
| TODO burndown chart for after the fastest conversion to
| --strict.)
|
| Typescript's --strict still isn't _technically_ Sound, in
| the functional programming sense, but that gets back to the
| pragmatism mentioned in the article of trying to get that
| 80 /20 benefit of enough FP purity to reap as many benefits
| without insisting on the investment to get 100% purity.
| (Arguably why Typescript beat Flow in the marketplace.)
| d0mine wrote:
| It may be the exact opposite. You can't express (at least you
| shouldn't try to avoid Turing tarpit-like issues) all the
| desired constraints for your problem domain using just the type
| system (you need a readable general purpose programming
| language for that).
|
| If you think your type system is both readable and powerful
| then why would you need yet another programming language?
| (Haskell comes to mind as an example of such language--don't
| know how true it is). The opposite (runtime language used at
| compile time) may also be successful eg Zig.
|
| Gradual typing in Python provides the best of both worlds:
| things that are easy to express as types you express as types.
| On the other hand, you don't need to bend over backwards and
| refactor half your code just to satisfy your compiler (Rust
| comes to mind). You can choose the trade off suitable for your
| project and be dynamic where it is beneficial. Different
| projects may require a different boundary. There is no size
| fits all.
|
| P.S. As I understand it, the article itself is about
| "pragmatism beats purity."
| aabhay wrote:
| On the other hand, if you think of a programming language as
| a specialized tool then you choose the tool for the job and
| don't reach for your swiss army knife to chop down a tree.
|
| The problem with gradually typed languages is that there are
| few such trees that should be chopped by their blunt blades.
| At least Rust is the best for a number of things instead of
| mediocre at all of them.
|
| One counterpoint to this is local self exploratory
| programming. For that a swiss army knife is ideal, but in
| those cases who cares about functional programming or
| abstractions?
| sevensor wrote:
| > you still can't really rely on it for much
|
| And yet, type annotations in Python are a tremendous
| improvement and they catch a lot of bugs before they ever
| appear. Even if I could rely on the type system for nothing it
| would still catch the bugs that it catches. In fact, there are
| places where I rely on the type system because I know it does a
| good job: pure functions on immutable data. And this leads to a
| secondary benefit: because the type checker is so good at
| finding errors in pure functions on immutable data, you end up
| pushing more of your code into those functions.
| brandonspark wrote:
| I was hoping this article would be a little more concrete, but it
| seems that it largely is talking about the takeaways about
| functional programming in a philosophical, effort-in vs value-out
| kind of way. This is valuable, but for people unfamiliar with
| functional programming I'm not sure that it gives much context
| for understanding.
|
| I agree with the high-level, though. I find that people (with
| respect to programming languages) focus far too much on the
| specific, nitpicky details (whether I use the `let` keyword,
| whether I have explicit type annotations or type inference). I
| find the global, project-level benefits to be far more tangible.
| jerf wrote:
| This is the conclusion of
| https://jerf.org/iri/blogbooks/functional-programming-lesson...
| . The concreteness precedes it, this is just the wrap up and
| summary.
| Tainnor wrote:
| It's a shame that this is not the link that was submitted
| because that (long) article was a really interesting read
| that gave me some food for thought and also articulated a
| bunch of things rather clearly that I've been thinking in a
| similar form for a while. I'm not sure that I agree with all
| of it, though (I still prefer maps, folds etc. over explicit
| loops in many cases, but do agree that this is less important
| than the overall code architecture).
| brandonspark wrote:
| I see. This is indeed the in-depth breakdown I was looking
| for, thank you.
| ninetyninenine wrote:
| web development today is literally just massive massive mutation
| operations on databases.
|
| Functional programming can't stop it, it just sort of puts a
| fence around it. The fence makes sense if it's just 10% of your
| program that you want to fence off. But the database is literally
| the core of our application then it's like putting a fence around
| 90% of the house and you have 10% of pure functional programming.
|
| Most operations are located at the database level. That's where
| the most bugs occur and that's where most operations occur. You
| can't really make that functional and pure.
|
| This high level stuff is actually wrong. At the highest level web
| apps are NOT functional.
|
| I get where he's coming from but he missed the point. Why do
| functional paradigms fail or not matter so much at the micro
| level? Because web applications are PRIMARILY not functional by
| nature. That's why the stuff doesn't matter. You're writing and
| changing for loops into recursion/maps in the 10% of your pure
| code that's fenced off from the 90% core of the application.
|
| You want to make functional patterns that are applicable at the
| high level? You need to change the nature of reality to do that.
| Make it so we don't need to mutate a database ever and create a
| DSL around that is functional. SQL is not really functional.
| Barring that you can keep a mutating database but create a DSL
| around it that hides the mutation.
| Tainnor wrote:
| There must be something wrong in the way FP is taught if the
| takeaway that people have is that it prevents or is somehow
| opposed to mutation.
|
| On the one hand you have bunch of FP languages that don't care
| in the least bit about "purity" (i.e. being side-effect free)
| or are more pragmatic around it, such as various LISPs, OCaml,
| Scala or even parts of the JS ecosystem. And on the other hand,
| there's a lot of research and discussion in e.g. the Haskell
| community about how to properly deal with side effects. The
| idea here is not that side effects are bad, but that we want to
| know where they happen and combine them safely.
| Joker_vD wrote:
| > The idea here is not that side effects are bad, but that we
| want to know where they happen and combine them safely.
|
| Yeah, the idea is not that people gathering together in
| groups more than two and/or past the 21:00 is bad, but that
| we want to know where it happens and ensure safety for all.
| Now, your papers, please or we'll apply the type checker
| (we'll apply it to y'all anyhow, of course, but we'd like you
| to cooperate with the inference process).
| chowells wrote:
| I don't understand why people get so angry when a compiler
| points out that their code is broken. Is it better if runs
| and does the wrong thing instead?
| ninetyninenine wrote:
| No you missed the point. I completely get the meaning of
| segregating IO/mutation away from pure logic.
|
| And my point is, what is the purpose of all of this is 90% of
| what your app does is mutation and side effects? Functional
| shell, imperative core indeed, but the shell is literally
| just thin layer of skin. The imperative core is a massive
| black hole.
|
| Functional programming can't save you from black hole.
| Tainnor wrote:
| Obviously the answer to "this doesn't solve my problems" is
| "don't use it then". If your problem domain literally is
| nothing but API calls and DB updates, then you may not
| benefit from this.
|
| OTOH, in my experience a lot of people underestimate how
| much pure business logic exists (or can be extracted) in
| many applications. In apps I've worked on I've found a lot
| of value in isolating these parts more cleanly. The
| blogbook series by the author of TFA (linked further
| upthread) goes into some detail about how to do that even
| without going fully down the "pure functional programming"
| rabbithole.
| meltyness wrote:
| Would you consider TLA+ functional? It sounds like the tension
| you're describing might be how most distributed consensus
| protocols are implemented as imperative code, and part of the
| Raft excursion involved writing a TLA+ proof of the protocol.
|
| https://github.com/ongardie/raft.tla
| kikimora wrote:
| I realized I don't understand idea of pure functions anymore. If
| a function fetches a web page it is not pure because it modifies
| some state. But if function modified EAX register it is still
| pure. How creating a socket or changing a buffer is different
| from changing a register value considering that in all cases
| outside observers would never know?
| tines wrote:
| Purity is relative to a given level of abstraction?
| entropicdrifter wrote:
| If a function is pure, it can take in and output 100%
| unmodifiable values. There will never be any side-effects in a
| pure function.
|
| In other words, if you need to modify the contents of a
| variable for a function to run, that's not a pure function.
| Taking something in and outputting something just like it but
| with some modifications is allowed, so long as the input is
| unmodified.
|
| Does that make more sense? You can't modify anything inside a
| function that originated from outside of the function's scope.
| betenoire wrote:
| I think they understand that, and are referring to more
| nuanced side effects. Logging, for an example, is a side
| effect, same with even using a date function. Hitting an API
| endpoint without cache may be functional if the response
| never changes, but do you want that? Usually we want a cache,
| which is skirting idempotency. The closer you look, the more
| side effects you see
| williamdclt wrote:
| Let's put your uncertainty to rest: at the extreme, any
| function execution spends both time and energy, both of which
| are observable side-effects.
|
| So yes, you're right that there no such thing as an absolutely
| pure function. "Pure" always assumes that all dependencies are
| pure themselves. Where it's a reasonable assumption and whether
| it's still useful or not depends on your program: assuming an
| API call to be pure is certainly not reasonable for many use
| cases, but it is reasonable enough to be useful for others.
| jerf wrote:
| It so happens that this was the topic of one of the posts in
| this series:
| https://jerf.org/iri/post/2025/fp_lessons_purity/#purity-is-...
|
| I'm assuming from your post you haven't come from there, and we
| just coincidentally picked a similar example...
| your_fin wrote:
| The author addresses this nicely in an earlier part of the blog
| book: https://jerf.org/iri/post/2025/fp_lessons_purity/
___________________________________________________________________
(page generated 2025-04-14 23:01 UTC)