← Back to context

Comment by munchler

3 months ago

Cute. All kidding aside, though, functional programming is worth the effort to learn, and it doesn't actually take 15 years. The payoff is at the end of the article:

"It’s quite natural to program in Haskell by building a declarative model of your domain data, writing pure functions over that data, and interacting with the real world at the program’s boundaries. That’s my favorite way to work, Haskell or not."

Haskell can be intimidating, though, so I would recommend F# for most beginners. It supports OOP and doesn't require every single function to be pure, so the learning curve is less intense, but you end up absorbing the same lesson as above.

Yes - the value of functional programming isn't that working in OCAML, or F#, or Haskell is 10x as productive as other languages. But that it can teach you worthwhile lessens about designing software that apply equally to imperative languages.

Modelling the business domain, reasoning and managing side effects, avoiding common imperative bugs, these are all valuable skills to develop.

F# is a great language to learn, and very approachable. Worst part about it is interacting with antiquated .NET API's. (I can't believe the state that .NET support for common serialization formats is still in...)

  • > Yes - the value of functional programming isn't that working in OCAML, or F#, or Haskell is 10x as productive as other languages.

    This is not true in my personal experience.

    As has been famously said (paraphrased): Functional programming makes tough problems easy and easy problems tough.

    In other words the value of functional programming depends on your domain.

    • It’s only tough to change your way of thinking. Most people making the switch find it tough because they are trying to find imperative techniques to do something in a functional way and struggling because they can’t find an if else statement or a for loop. But if you were never taught to think in terms of conditional branching or looping indexes you’ll save a lot of time.

      1 reply →

    • > easy problems tough.

      That needs a qualifier: it can make easy problems tough if you're not familiar with how to solve them in a functional context.

      A big part of that is because smart people have already solved the tough problems and made them available as language features or libraries.

      11 replies →

    • > makes tough problems easy and easy problems tough

      And because of mutual recursion, that means that tough is easy (and easy tough). In other words, if we call the class of tough problems T and easy problems NT, we have T==NT, given FP.

  • Hot take of the day: you learn that with imperative programming just as well.

    I familiarized myself with fp to the point of writing scheme and haskell around 15 years ago. Read the classics, understood advanced typing, lambda calculus and so on. The best “fp” I’m using nowadays is closures, currying in the form of func.bind(this[, first]) and map/filter. Which all are absolutely learnable by the means of closures, which are useful but I can live without. Sometimes not having these makes you write effing code instead of fiddling with its forms for hours.

    Still waiting for the returns from arcane fp-like code I produced earlier. Cannot recognize nor understand none of my projects in this style that I bothered to save in vcs. Imperative code reads like prose, I have some of it still in production since 2010.

    These FP talks are disguised elitism imo (not necessarily bad faith). Beta reduction and monadic transformers sound so cool, but that’s it job-wise.

    • > These FP talks are disguised elitism imo (not necessarily bad faith). Beta reduction and monadic transformers sound so cool, but that’s it job-wise.

      They may be disguised mathematics. People are into math because it is neat / elegant / cool. So they study it regardless of whether it has a practical use or not.

      3 replies →

    • I commonly implement things in an imperative style as a quick hack, then if it gets use I translate it into a more functional style. It kind of just happens as I clean it up and refactor during revisits.

      It might be a matter of taste, but I enjoy code built with functional abstractions that allow neat composable data flows and some caches loitering around. I find it also helps when adding UI. Sometimes performance could be better with mutation, but when I'm at that point I've already spent much more time tuning the thing with caches.

    • In theory, you could pick up your one language, say, Java, and through the course of a normal career learn everything necessary to program in that language in the best possible way.

      In practice, it's a pretty well-known phenomenon experienced by many skilled programmers that being forced into different styles by different languages results in learning things that you would only have learned very slowly if you had stuck only to your original language. To be concrete about the "very slowly", I'm talking time frames of your entire career, if not your entire life and beyond. It would be a long time programming in Java before you discover the concept of something like "pure functions" as a distinct sort of function, a desirable sort of function, and one that you might want organize your programming style around.

      Of course, having heard of the concept already, we'd all like to fancy ourselves smart enough to figure it out in less than, say, three decades. But we're just lying to ourselves when we do that. Even the smartest of us is not as smart as all of us. You are not independently capable of rediscovering everything all the various programming communities have discovered over decades. If you want to know what even the smartest of us can do on their own without reference to decades of experience of others, you can look into the state of computer programming in more-or-less the 1980s, 90s if you're feeling generous. I think we've learned a lot since then, and the delta between the modern programmer and a 1980s programmer certainly isn't in their IQ or anything similar, it is in their increased collective community experience.

      By getting out into systems that force us to learn different paradigms, and into communities that have learned how to use them, we draw on the experience of others and cover far more ground than we could ever have covered on our own, or in the context of a single language where we can settle into a local optima comfort zone. Jumping out of your original community is kind of an annealing process for our programming skills.

      "The best “fp” I’m using nowadays is closures, currying in the form of func.bind(this[, first]) and map/filter."

      That is really not the lesson about software design that FP teaches, and blindly carrying those principles into imperative programming is at times a quite negative value, as your experience bears out. FP has more to say about purity of functions, the utility of composition of small parts, the flexibility of composition with small parts, ways to wrap parts of the program that can't be handled that way, and providing an existence proof that despite what an imperative programmer might think it is in fact possible to program this way at a system architecture level. I actually agree 100% that anyone whose takeaway from FP is "we should use map everywhere because they're better than for loops and anyone who uses for loops is a Bad Programmer" missed the forest for the leaves, and I choose that modification of the standard metaphor carefully. I consider my programming style highly influenced by my time in functional programming land and you'd need to do a very careful search to find a "map" in my code. That's not what it's about. I'm not surprised when imperative code is messed up by translating that into it.

      5 replies →

  • I wouldn't even say antiquated, modern .Net APIs can suck to work with too, the entire ecosystem is written for C# ASP.Net core and everything else feels second class.

    I love F#, the working with C# elements of the language drove me away.

    • Which elements were the source of pain?

      Writing web applications and back-ends in F#, as far as I'm aware, is straightforward and a joy because of integration with the rest of ecosystem.

I was a college dropout and self taught bash and python programmer and quite some time ago, I read about Haskell, decided to teach myself to use it, and then realized I had absolutely no idea what programming actually was, and basically spent the next 15 years teaching myself computer science, category theory, abstract algebra and so on, so that I could finally understand Haskell code.

I still don't understand Haskell, but it did help me learn Rust when I decided to learn that. And I think I could explain what a monad is.

edit: It's a data structure that implements flat map. Hope this saves someone else a few years of their life.

  • > I still don't understand Haskell

    It's not you. Haskell has very bad syntax. It's not hard to understand it, it you rewrite the same things in something saner. Haskell was developed by people who enjoy one-liners and don't really need to write practical programs.

    Another aspect of Haskell is that it was written by people who were so misguided as to think that mathematical formulas are somehow superior to typical imperative languages, with meaningful variable names, predictable interpretations of sequences of instructions etc. They, instead, made it all bespoke. Every operation has its own syntax, variables are typically named as they would in math formulas (eg. X and X'). This makes no sense, and is, in fact, very harmful when writing real-world programs, but because, by and large, Haskell never raises to the task of writing real-world programs, it doesn't deter the adepts from using it.

    • Just to give a different pov I find Haskell very intuitive, and particularly I find that code written by other people is very easy to understand (compared to Java or TypeScript at least).

      And by the way x and x' are totally fine names for a value of a very generic type (or even a very specific type depending on the circumstances), as long as the types and the functions are decently named. I mean, how else would you call the arguments of

      splitAt :: Eq a => a -> [a] -> [[a]]

      ?

      There is no need for anything more complex than

      splitAt x xs = ...

      4 replies →

    • That's what (some) other people do. None of that stops you writing Haskell in whatever style you want, with meaningful variable names, curly braces and semicolons!

      1 reply →

    • You knew Paul Hudak, Simon Peyton Jones, Phil Wadler, etc? Were they thinking about the benefits of mathematical formulas over program counters and procedural keywords when designing Haskell?

      I was under the impression from the History of Haskell [0] that they were interested in unifying research into lazy evaluation of functional programming languages.

      > This makes no sense, and is, in fact, very harmful when writing real-world programs,

      Gosh, what am I doing with my life? I must have made up all those programs I wrote on my stream, the ones I use to maintain my website, and all the boring line-of-business code I write at work. /s

      In all seriousness, Haskell has its warts, but being impractical isn't one of them. To some purists the committee has been overly pragmatic with the design of the language. As far as functional programming languages go it's pretty hairy. You have "pure" functions in the base libraries that can throw runtime exceptions when given the wrong values for their arguments (ie: the infamous head function). Bottom, a special kind of null value, is a member of every type. There exist functions to escape the type system entirely that are used with some frequency to make things work. The committee has gone back more than once to reshape the type-class hierarchy much to the chagrin of the community of maintainers who had to manually patch old code or risk having it not longer compile on new versions of the base libraries. These are all hairy, pragmatic trade-offs the language and ecosystem designers and maintainers have had to make... because people write software using this language to solve problems they have and they have to maintain these systems.

      [0] https://www.microsoft.com/en-us/research/wp-content/uploads/...

      10 replies →

  • I think that's a good starting definition for programmers, but still could cause confusion when you run into something like IO in Haskell. IO isn't really a data structure, and it's hard to fit the "flat map" concept to it.

    • If you want you can still keep this point of view, by saying that IO is conceptually a data structure that builds a description of what the program does. In this point of view it follows that there is another, impure program that interprets the IO data structure and actually performs the computations

      (Of course in practice IO isn't implemented like this, because it would be too slow)

      (But in every other language, like Javascript or Python, you can define IO as a data structure. Or even in Haskell itself, you can define for eg. a free monad that gets interpreted later, and it can be made to be just as good as IO itself, though typically people make it less powerful than IO)

      However note that every other "computational" monad (like the list monad or the Cont monad) actually is a data structure, even though they describe effects just like IO does. This is because IO is the only possible exception to the "monads are data structures" thing in Haskell (if you don't subscribe to the above view), because Haskell doesn't let you define types that aren't data structures

      The only issue with this point of view is that you now need to say what flatMap means for things that are not shaped like arrays. Eg. does it make intuitive sense to flatMap a tree? (A retort is that it must make sense, whatever you call this operation; and flattening a tree means to turn a tree of trees into an one-level tree)

I feel the same pay-off - but arrived at that point via Clojure. Immutable-first, aim for purity, ability to drop out of it when necessary.

As stringent as you need it to be (static vs. dynamic types vs. specs), as flexible as you want it to be.

I really wanted to like F#, and I kinda do, but it has a number of quirks, compiler issues and cracks in the design that are getting worse:

First off, the compiler is single-pass. All your definitions have to be in order. This even extends to type hints - it can't use clues to the right to deduce the type of an expression on the left. This is supposedly for perf reasons, but the compiler can become extremely slow because the inference engine has to work so hard - slower than GHC for sure.

Speaking of slowness, Haskell is surprisingly fast. Idiomatic Haskell can be within 50% the perf of C, since its laziness and purity unlock powerful optimizations. F# is eager and the compiler doesn't do anything fancy. Perf often makes you reach for mutable state and imperative structure, which is disappointing.

The OOP paradigm feels bolted on. Should you use classes or modules? Pure functions or members? It depends, what mood are you in? Unfortunately only member functions support overloads, and overloads are useful for some SFINAE-type patterns with `inline` functions, so they get a bit overused.

`ref` struct support, which is vital for zero-copy and efficient immutable data, have very primitive support. even C# is ahead on this.

Very limited support for implicit conversions, no support for type classes and no function overloading leaves F# with nothing like a numeric tower you'd have in Lisp, and makes building something like Numpy clunky.

I use C# at work, and I love Haskell, so I really wanted to love F#. But it just doesn't get the love it needs from MS, and some design decisions aren't aging well - particularly as C# itself evolves in directions that are tricky for F#'s aging compiler to support.

I feel like functional programming is pretty trivial. It's pure programming that is very difficult.

They're often conflated because Haskell is pure and functional and probably the most talked about heavily functional language.

I certainly didn't know that impure functional languages like OCaml existed for ages.

  • Is Haskell pure?

    It has exceptions

    You can divide by zero

    It has unsafe IO primitives

    • You're right: "pure" is not a well-defined concept. The well-defined concept that describes Haskell's benefits in this regard is "referential transparency". That means that this code

          let x = <definition of x>
          in ... x ... x ... 
      

      (i.e. defining a variable x and then using it some number of times) is equivalent to

          ... <definition of x> ... <definition of x> ...
      

      Seen in the opposite direction (transforming the bottom code to the top code) this means that extracting repeated code is always a valid thing to do. It's not valid in most other languages, and certainly no mainstream ones.

      4 replies →

    • It is pure in the same way that Rust is memory safe. That is too say there are a tiny number of exceptions/escape hatches, but they are not meant to be the norm. Every day programming doesn't involve them.

      Exceptions aren't impure anyway.

      5 replies →

    • I feel like exceptions where added as a mix of "look we can do that too" and "maybe if so many functions return optional values then it is going to be too much of a pain to use"

      In hindsight I think few would now regret not having added them in the first place.

      2 replies →

    • >It has unsafe IO primitives

      To be tongue in cheek then it also has the side effect of heating the CPU.

That’s interesting because F#’s OOP, as someone who knows neither C# nor Java, makes it more intimidating to me than OCaml.

Also interesting that when FP is mentioned, Hindley-Milner is implicitly understood to be part of FP too even though it doesn’t have to be. Clojure emphasizes immutability and FP but with dynamic typing and everything that comes with that.

  • Doesn't the "O" in OCaml stand for "Object", though? I think you could pick up either F# or OCaml just as easily.

    The nuances of OOP in F# can be ignored by beginners, so I really wouldn’t let yourself be intimidated coming from Clojure.

    [0] https://ocaml.org/docs/objects

    • OCaml classes and objects are (ironically) rarely used and generally discouraged. There are some cases where they’re practically required, such as GUI and FFI (js_of_ocaml). But otherwise, most code does encapsulation and abstraction using modules and functor modules (which are more like Haskell and Rust typeclasses than traditional OOP classes).

      I don’t know much about F#, but last time I used it most of its standard library was in C# and .NET, so F# code would interact with objects and classes a lot. AFAIK F# also doesn’t have functor modules, so even without the dependence on C# code, you still can’t avoid classes and objects like you can with OCaml (e.g. you can’t write a generic collection module like `List` or `Set` without functors, it would have to be a collection of a specific type or a class).

      6 replies →

  • > Clojure emphasizes immutability

    Is "emphasizes" just another word for second-class support?

    C++ emphasizes the importance of memory safety.

    • > Is "emphasizes" just another word for second-class support?

      I don't know what's your personal definition of "second-class support" but what it means is that it's explicitly supported by the language.

      6 replies →

> doesn't require every single function to be pure

having never done F# or haskell, doesn't that start getting into the territory of languages that encourage functional programming like ruby or javascript (modern javascript)?

I would suggest Scala as FP for beginners. It doesnt forces you to do pure functions. And its really beginners friendly to start with.

In modern Fortran, functions should be pure (although the language does not require this), and procedures that mutate arguments are made subroutines (which do not have return values).

  • Note that Fortran's interpretation of the term "pure" bizarrely allows a "pure" subprogram to depend on mutable state elsewhere (in a host, a module, or a COMMON block). So Fortran's "pure" functions aren't referentially transparent.

    (F'2023 added a stronger form of "pure" and calls it "simple", but it didn't strengthen the places where a "pure" procedure should be required to be "simple", such as DO CONCURRENT, so being "simple" will be its own reward, if any compiler actually implements it. And a "simple" function's result value can still depend on a mutable pointer target.)

But why do we need Haskell for this?

  • Realistically we don't but it's very rare to meet a programmer who understands these distinctions thats not also a great functional programmer.

    This is my experience after spending five years as a Haskell programmer and managing a Haskell team for several years and now moving back to the c++ world to play with AI.

    I know lots of good c++ programmers working on cutting edge stuff, real experts in their field, but they sometimes still don't have a clear way to understand how to model data

    That is my opinion. It's probably highly contentious.

    • I've actually had to fire a technically exceptional Haskell programmer because of the damage they did to our C# codebase (and arguably moreso, the team). Sometimes it's not a matter of talent or skill, but culture fit.

      In my experience FP-aligned people on non-FP projects tend to be more likely to overengineer, more prone to argue in favor of the Great Rewrite For No Reason Except Aesthetics, and more likely to abuse "lesser" programmers when they put up PRs. They suck as team players on teams that are not made of language nerds. I am not just talking about the one person here who I fired, this is a legit pattern I've noticed over at least a half dozen people.

      Conversely, they are exactly the right people to deploy when you have really tough, self-contained problems to solve that you wouldn't trust the normal Java 9-5ers to tackle.

      No matter how they do it, you can always rewrite their working code in a more maintainable language later once it's working, and make it integrate well with the rest of your stack. :D

      4 replies →

  • Because for some reason there are no pure strict-by-default languages around.

    • All of them (especially newer ones) are, except Haskell (und some other, nowadays either obsolete or really obscure languages).

      Idris (2), PureScript, Elm, Unison, Roc, Lean (4), Koka, Flix (and some other I've forgotten about).

    • Elm is one example of such. However, it's also an illustration of why these languages are rare. With a strict semantics there's an almost unbearable temptation to add library functions with side effects. Elm only avoided this fate by giving its BDFL strict control over which packages could access the JS FFI. But that upset a lot of people.

I would recommend neither of those.

Haskell has very bad syntax (with extensive backing from Microsoft, iirc the guy who writes the compiler is a Microsoft's Research employee).

F# is a straight-up Microsoft's language.

It doesn't matter what other benefits it has. Just don't touch anything created by that company, and you will have one fewer regrets in your life.

But, if you still want a language from that category: SML or Erlang would be my pick.

  • SPJ has left MSR and is now at Epic games, working on a new PL. However, even while he was at MSR, MS didn't really have a say in how Haskell was developed.

    • Well, MS didn't have to do anything. It's enough that they have (or had) the opportunity to do something.

      There isn't an Overmind in MS that in a creepy voice tells you to spawn more overlords. Less than that, there doesn't need to be a written document that tells you to give money to MS or your data etc. There's just a general accepted understanding among the people who run that company that ends justify the means. And by "ends" they mean them and their investors getting rich.

      If Haskell compiler could've been turned into a money-making machine, and it only required killing off half of Haskell programmer, MS would be working overtime on the plan to hide the bodies, but they'd never even consider the possibility of killing being bad... (metaphorically speaking, hopefully)

      2 replies →

  • What's wrong with Haskell's syntax? I think it's generally pretty nice though can be excessively terse at times.

    • * Significant white space, and the rules around whitespace are very convoluted.

      * There's no pattern or regularity to how infix / prefix / suffix operators are used which makes splitting program text into self-contained sub-programs virtually impossible if you don't know the exact behavior, including priority of each operator.

      * There's a tradition of exceptionally bad names for variables, inherited from the realm of mathematical formulas. In mathematics, it's desirable to give variables names devoid of everyday meaning to emphasize the generic nature of the idea being expressed. This works in the context of very short formulas, but breaks entirely in the context of programs which are usually many orders of magnitude bigger than even the largest formula you've ever seen. There, having meaningful names is a life west.

      * It's impossible to make a good debugger for Haskell because of the language being "lazy". Debuggers are essential tools that help programmers in understanding the behavior of their programs. Haskell programmers are forced to rely on their imagination when explaining to themselves how their program works.

      * Excessive flexibility. For example, a Haskell programmer may decide to overload string literals (or any literals for that matter). This is orders of magnitude worse than eg. overloading operators in C++, which is criticizes for defying expectations of the reader.

      One of these points would've been enough for me to make the experience of working with a language unpleasant. All of them combined is a lot more than unpleasant.

    • From the point of view of writing a parser, Haskell's whitespace syntax seems like a hack. So, the grammar is defined with braces and semicolons, and to implement significant whitespace, the lexer inserts opening braces and semicolons at the start of each line according to some layout rules. That's not the hacky part; what makes it a hack is that to insert closing braces, the lexer inserts a closing brace when the parser signals an error. You can read about it here [0].

      Also, on an aesthetic level, I think a lot of infix operators are kind of ugly. Examples include (<$>), ($), and (<*>). I think Haskell has too many infix operators. This is probably a result of allowing user-definable operators. I do like how you can turn functions into infix operators using backticks, though (e.g. "f x y" can be written as "x `f` y").

      [0]: https://amelia.how/posts/parsing-layout.html

  • > Just don't touch anything created by that company, and you will have one fewer regrets in your life.

    :-)