Hacker News new | past | comments | ask | show | jobs | submit login
Ask HN: How has functional programming influenced your thinking?
23 points by curious16 on Jan 16, 2023 | hide | past | favorite | 48 comments
Functional programming is a programming paradigm. Has learning this particular paradigm caused any paradigm shift to your thinking about programming or problem solving in general?

If so, how?




Functional Core, Imperative Shell[1] is one of the most useful patterns. I will probably use it in every future project. Two major production projects that use FCIS are:

- ReactJS: https://svitla.com/blog/functional-programming-in-typescript

- CodeMirror/ProseMirror: https://codemirror.net/docs/guide/

Another functional concept I find very useful is immutability. Immutability "clicked" for me after watching this talk[2]. I wish immutability was a first-class concept in JS/TS. (It will be, eventually.[3]) For now, I use immer.js for immutability in JS.

In retrospect, my programming naturally tended to be "functional-esque. Now I do it very deliberately, even though the language isn't purely functional (JS/TS).

[1]: https://hw.leftium.com/#/item/18043058

[2]: https://github.com/matthiasn/talk-transcripts/blob/master/Hi...

[3]: https://fjolt.com/article/javascript-records-and-tuples


Functional programming has instilled in me a belief that I call "You don't have to punch yourself in the face."

The premise is this: you have two arms and two hands. They can be used in a controlled manner to do lots of useful things. They can also be swung around in a wild fashion that results in punching yourself in the face.

The concepts functional programming rallies against, like mutable state, are like arms: useful in certain circumstances, dangerous in others. But that's the thing: You don't have to write code in a dangerous way. You don't have to punch yourself in the face. That is to say, functional programming has reinforced my belief in the imperative style of programming.


I did work in Scala for a few years. We employed Cats[1], and even a bit of Matryoshka[2] though most of the work I do today is in Python.

Nowadays I think about computational requirements in terms of relations among behavioral dependencies. Like, "I want to perform operation O on input A and return a B. To do this, I'll need a way to a -> b and a way to b -> b -> b." I often pass these behavioral dependencies in as arguments and it tends to make the inner core of my programs pretty abstract and built up as layers of specificity.

Zooming out nearly all the way, it makes me feel tethered in a qualitatively unique way to certain deep truths of the universe. In a Platonic sense, invoking certain ideas like a monad make me feel like I'm approaching the divine or at least one instantiation of a timeless universal that operates outside of material existence.

I'd imagine some mathematicians might see the universe in a similar way - one where immortal relations between ontological forms exist beyond time and space and at the same time can be threaded through the material world by intellectual observation and when those two meet a beautiful collision occurs.

1. https://typelevel.org/cats/

2. https://github.com/precog/matryoshka


My biggest takeaway was something I never before thought was even possible - to create a program without any variables. It turns out you can do a whole lot without any variables.

Through that I understood that the state of a program what causes most of the confusion and without variables reasoning about the code becomes much simpler. Without state everything becomes nice.

Through that I understood that state is the enemy, and global state is the greatest enemy of understanding. Later I realized that this same principle strongly aligns with testable code as more state is what makes code less testable.

It also forced me to use a lot of recursion which is nice as I understood it much better than in that single "Fibonacci number" example.

I also learned to write code that can safely run in parallel as functions without side effects and state are the most ideal candidates for that.


>It also forced me to use a lot of recursion which is nice as I understood it much better than in that single "Fibonacci number" example.

Back in college I took a programming class that used Haskell and not having any other do way to perform iterations other than with recursion, you're forced to understand recursion. With imperative languages, you can always fall back to ol' for and while loops so you get the impression that recursion is simply a waste of time.


Minor correction: functional languages have state; they're the captured arguments to partially applied functions. After all, the lambda calculus can simulate a Turing machine.

What functional languages don't have is the ability of a function to modify state in a way that persists beyond its lifetime, which makes the temporal axis of a program more or less the same as the call tree.

State is the enemy only insofar as you need more than the call tree to figure out its value.


I agree, thank you for the correction.

In this context what I mean by stateless is a function call that only depends on the input - given the same starting position it will always produce the same result.

In real world what messes up this behaviour are fields in a class, properties in a file, persistent data of any form and globally initialized variables unknown to the caller.

You don't and will never have an easy way to reproduce, find out or modify these values so ideally you should not put data in database, not use property files and not use fields in classes :)

Yeah... I never said it was easy or doable in real world. But it is a great compass to drive towards a good architecture.


Web apps (well, well-functioning ones, anyway) helped this click for me. While the request is happening, there is state that is passed around, and contexts like the request, the user, etc.. and depending on what you're doing, you may be adding to that state, or modifying it, or whatever.

But that state shouldn't persist beyond the request, otherwise you have problems. You shouldn't have one request modifying the state of other running requests, etc..


It influenced my thinking in the realization that programming is much about a culture as it is about a technology. Functional programming should succeed as something that makes difficult work easier, but a lot of FP advocates want it to be the opposite of that. For a brief period, I too thought, "Hey, I can learn this stuff and then I'll get to work with smarter people!"

Smart doesn't work that way. Shame on me for not learning the same lesson from OOP. These are good technologies, but toxic culture undoes all the good and more.

End result: We can't have nice things, because the arguments are bogus.


I don't know that's true of all functional language use.

I imagine there are normal shops and devs using Elixir, F#, certainly Elm.


I don’t think any technique is inherently bad: imperative, global variables, and even “goto” can be used wisely to solve problems.

Functional programming (similarly type systems) is another great technique to use when you have constraints that you’d like to be automatically enforced by the compiler (referential transparency, equational reasoning, etc.)

When I have to use a language that doesn’t allow expressing problems functionally when that would be the ideal expression of it, I feel I’m using the wrong tool for the job.


>I don’t think any technique is inherently bad: imperative, global variables, and even “goto” can be used wisely to solve problems.

The word "wisely" does the heavy lifting here. Put it this way: would you rather work on a legacy code base that has goto statements all over the place, or on one where strict standards were applied?

I find this is the relevant question when it comes to programming styles.


I’d rather work on a codebase where strict standards were applied.

Banning “goto” is one of such, but also allowing to be utilized under certain conditions to write state machines for example.

I think we will miss innovations if we are dogmatic about features.

“Learn the rules like a pro, so you can break them like an artist.” - Pablo Picasso


The vast majority of challenges, bugs, complexity, and performance issues in software engineering can be reduced to managing state. State is hard. Immutable state solves a lot of these issues in theory, but in practice mutable state is the tradeoff you must make in many cases to get acceptable performance. The cost of this tradeoff is the increased complexity cost.

A good engineer understands the sweet spot of these tradeoffs. Use functional techniques with immutable data structures in the right places where performance is unlikely to become an issue.


I enjoy simple mappings of input to outputs.

For tree based processing functional programming Ocaml and Haskell are meant to be very effective. Especially if you can think of your problem as a functional isomorphism.

When I created my toy database I used a pipeline of tuples that were reduced. Relational data is easy to process with functional ideas.

Clojure pipelines are similar.

My problem with functional programming is its readability. If someone had a different idea of how they would approach the problem functionally, it can be tricky to read the code that performs the functional mapping.


I wrote mostly python and c languages before I knew anything about FP. Part of the appeal is how mathematical it is, so intuition from math and physics modeling can carry over more easily for some people. lazy evaluation and currying as in haskel are powerful ideas that can influence how problems are broken down conceptually. Earlier on i thought of a well-written large program like an octopus with a complicated brain in the middle and many tentacles coming from the body. Now i aim for programs that are more like a green onion with a clear flow from root to bulb to shoot, and elegant in their simplicity. I still mostly write python and c++, but banishing side effects where possible for example has led code that paralellizes more easily and is easier to debug.


In Python or Javascript, I look for opportunities to use pipe() or pipeeither() [1] wherever possible. These crop up a lot -- CLI scripts, data processing lambdas, etc.

A quick and dirty pipeither() with a "poor mans" either monad and you get a lot of the benefits of pure, testable functions, easy maintenance and debugging etc.

It's not Haskell, but its good.

[1]

    const pipelineeither = function (l) {
      const pipelineeither = function (v) {
        let [left, right] = [null, v];
        for (const f of l) {
          if (left !== null) break;
          [left, right] = f(right);
        }
        return [left, right];
      };
      return pipelineeither;
    };


and in Python:

    def pipeeither(l):
        def pipeeither(v):
            return reduce(lambda a, e: e(a[1]) if a[0] is None else a, l, (None, v))
        return pipeeither


For me, functional programming permits better means of reasoning about the flow of data through software systems. You always know how variables came to have the values they do, because they were passed into the function that way. This reasoning extends back up the call stack. It makes debugging much easier and therefore cheaper than with OOP for example. I think a functional approach is just more predictable generally.


The value of values: https://github.com/matthiasn/talk-transcripts/blob/master/Hi...

Having values and having your program phrased as transformations of values makes for straight forward, easily tested code. Provide inputs, assert outputs.


Avoiding side effects is great, it allows you to construct big programs by reasoning about the parts that make them.

FP also taught me a lot about types, which allow me to define even better the parts that construct big programs.


I've been structuring more and more of my code with patterns like:

  bool SomeProperty { get; set; } = false;
  string SomeValue => SomeProperty == true ? SomeValueA : SomeValueB;
  string SomeValueA => "A";
  string SomeValueB => SqliteConnection.ExecuteScalar<string>("SELECT 'B'");
  string SomeDescription => SomeProperty switch 
  {
    false => "Extended description for false",
    true => "Extended description for true"
  }
  string Output => 
  @$"Result: {SomeValue}
     Description: {SomeDescription}";
Combine with language features like LINQ and you can do an incredible amount of damage before you have to take out a proper method body.

I've caught a LOT of bugs converting method bodies to expressions like this.


It's confusing in the context of FP that your example uses SomeX when referring to non-optional types.


It certainly helped me formulate problems as data transformation problems which are solved very neatly with functional programming.

It is much more enjoyable working with immutable data structures which tends to remove long distance dependencies from your code. Instead, decisions are made locally and results passed up vs directly changing state in an object graph.

Not all problems are easily turned into a pipeline of pure functions, but it is worthwhile to learn which category of problems can.

Lean a Lisp. It will up your game.


I don't really understand what it's for or why I'd use it. I don't "get" immutable variables. Anyone I ask for an explanation just says "but it means that if you do something to the variable it doesn't change the value for other parts of the program" which just seems a bit daft to me.

What's an actual concrete use case of this? Where specifically would I use immutable variables to make my code better?


It’s all about handling state in a careful manner.

A helpful consequence of immutability is that it makes concurrent and parallel programming easier. Take Erlang/Elixir. The main reason it’s able to share state between processes so easily is because it completely copies values from one process into another’s mailbox. Compiler guarantees you don’t pass a reference. If it were the same memory, two processes could mutate and unwittingly break one another.

Another helpful aspect are the patterns of code you end up writing. It’s common in Node and C#, even Go, to pass a mutable context or stateful object with a bunch of hands reaching in to read and write. Because you can naturally do this, you naturally also tend to program like this. Cue data access issues again.

But if you guarantee immutability at the runtime level, it changes how you write code. You tend to write functions in a “transformer” / pipeline style. there’re great examples of open source codebases to read from that exhibit this. See a typical F#, Haskell, and presumably Scala web api codebase, and see how they handle state.


Well, in Python for example there is the classic bug where if you change a dictionary in some function, it changes everywhere else. Can't do that with immutable data.

It is easy to avoid, but if you can't do it anyway, the potential for bugs go away.


It is what got me into writing code. I've tried low-level imperative programming in C which wasn't for me, I never seemed to find object oriented programming intuitive.

What was my frame for reference was piping input and output in bash programming and various declarative DSL like SQL or the Hashicorp language, functional programming in Clojure seemed quite productive and grokable, so I dove into that.


I've tried to dabble in functional programming but I guess I'm not mathematical enough to "get it". I can see how it works, I did some toy projects in Haskell and Elixir but it just didn't really pull me in. For the record I code mostly in C for embedded and bare metal or just pure automation for other stuff.


There's the simple good stuff: single assignment in a method/function, filter/map/reduce. Some better stuff: Option/List & flat_map, Either/Result & and_then, elimination of most mutable state in OOP. The initializer/constructor is pretty much used for DI and other fixed context rather than mutable state.

The best part is thinking in terms of data and transformations of that data, whether you compose the functions first or run data through a pipeline.

I've been applying this for some projects in a RoR codebase and the team seems to like it way more than others' more Ruby idiomatic but widely-varying styles. It wouldn't be nearly as useful without Sorbet static typing.

I was doing the Advent of Code in Rust and was disappointed to find that using for loops is substantially more convenient than functional constructions.


a) Insight: Side-effects can often be avoided, and sometimes it’s a good thing to do so, even when not writing programs in a purely functional style.

b) To me, functional programming was tremendeously useful when learning to program. Not sure exactly why, but it clicked with the way my brain works.


>To me, functional programming was tremendeously useful when learning to program.

Did you use any specific book for the purpose?


Yes!

Two classics:

The Little Schemer, and the first few chapters of Structure and Interpretation of Computer Programs.


Paired with types, it has helped me when (and how) to split code and data, and how keeping clear boundaries between them and using pure functions when possible makes everything better: less cognitive overhead, easier tests, better data structures.


ad-hoc abstraction is the secret sauce that destroys code bases in the long run and makes things horrendously complex. Composition based on mathematics is more likely to survive years of serving as atomic elements than anything based on ad-hoc human intuition

Composition is hard


Can you elaborate on this, or link to a more detailed description of what you mean?


I forgot where I read this, but I'll paraphrase: it's easier to work with 10 objects with 1000 applicable functions than with 1000 objects with 10 methods each.

In Haskell or other typed fp languages, you have all these well-recognized and well-used typeclasses and their attached methods, conceptually equivalent to generic interfaces in c#/java. It's easier to adapt new data structures into these interfaces and have all your existing functions immediately work with them.

The "mathematics" part here is that these typeclasses are backed by category theory that prescribes their behaviors.

There are plenty of random readings you can find, so I won't recommend them. Someone's bound to argue with me on what's a better entrypoint.

I do want to note that you don't need to use haskell or an fp language to use these concepts. Example: a complete set of fp data structures in typescript https://gcanti.github.io/fp-ts/modules/. It's just that fp languages make using them much easier because of their type system.


https://mitp-content-server.mit.edu/books/content/sectbyfn/b...

> The list, Lisp's native data structure, is largely responsible for such growth of utility. The simple structure and natural applicability of lists are reflected in functions that are amazingly nonidiosyncratic. In Pascal the plethora of declarable data structures induces a specialization within functions that inhibits and penalizes casual cooperation. It is better to have 100 functions operate on one data structure than to have 10 functions operate on 10 data structures. As a result the pyramid must stand unchanged for a millennium; the organism must evolve or perish.

I love programming with typeclasses based on mathematical abstractions. I guess it's because they actually have a chance of standing unchanged for a millennium.



But inheritance is harder, isn't it?


The concept of inheritance in programming is an ill conceived one. Better not to use it at all.


Inheritance is a concept of OOP not programming itself. And yes I much prefer composition.


I appreciated how immutable state usually makes your life easier.


Yeah. My functions work with data that is received in parameters. Classes are for dependency injection composition and hold state data only when absolutely necessary.


It did not.


Mostly in avoiding mutation and imperative loops, etc.

It’s mostly things at this level, and not larger structural things. I don’t understand (and probably never will) the whole dance around using monads for side effects, aka the entire reason we run software in the first place. Your problem is “I want to update the database” and someone starts talking about stuff like “bind” and “shift”. Now you have two problems.


Mostly negatively. The beautiful code that you produce by functional programming will always make you aware of how bad most programming languages are. I took Ocaml in university and I was completely and utterly blown away.

You just know deep down whenever you touch JS or some other language that there exists a better way of programming.


ramda and other FP js libs go some way to helping with this.




Consider applying for YC's Summer 2025 batch! Applications are open till May 13

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

Search: