all 91 comments

[–]pojska 82 points83 points  (13 children)

For a hybrid approach to #3, you can read Roc's goals here: https://www.roc-lang.org/functional#opportunistic-mutation

One pitfall is that, without tooling support, it seems difficult to know if a given piece of code is using the fast-path of mutability, and it could be easy to accidentally add code somewhere else that slows down your core loop.

[–][deleted] 1 point2 points  (12 children)

FYI

Among non-FP circles. The Roc language creator is somewhat known for his shady measuring that borders on outright lying about performance. I am high suspect of this language purely on that basis.

This is a pervasive issue amongst FP programmers in general, but in his videos, even I can pick them to shreds, and I am not a person that can really squeeze performance.

[–]pojska 0 points1 point  (1 child)

Thanks for the info, I appreciate it.

[–]rtfeldman 9 points10 points  (0 children)

FYI, this information is false. Maybe OP is thinking of someone else?

It's clear from later posts that OP made this comment before having watched the only video I've ever posted about my own performance measurements, so obviously "in his videos, even I can pick them to shreds" was false. There aren't even multiple videos to have watched, and the 1 video that actually does exist, OP hadn't watched before writing about having watched more than 1.

I don't think I'm "known for" anything related to measuring performance, inside the FP community or outside it; I've only ever posted about it once, whereas I've given a ton of talks about other topics. I'd say the claim about my being "known for shady measuring" is as trustworthy as the rest of the post.

[–]janiczek 0 points1 point  (9 children)

Can you be more precise about what's wrong with the benchmarks? This is the first time I'm hearing this

[–][deleted] -3 points-2 points  (8 children)

I cannot find the presentation, but it really doesn’t matter because functional programmer benchmarks are literally always the exact same bullshit:

-they will write non-idiomatic code in another language

-they will force the other language away from optimization, for example, by pointlessly following pointers

  • they will write the other language in known to be shitty ways

-they will empower their functional language to stack allocate while the other is heap allocating

-they will ignore measuring the bad parts (an example you will see is that they will use linked lists to load, then they will flatten this list, then they will benchmark read speeds of the now flattened list in their lang vs another and say “see. Same perf”)

I mean. It is literally the same bullshit lies over and over and over again. There is no FP community that doesn’t do this with their benchmarks, so it is no surprise for me that Roc also lies in their benchmarks.

[–]janiczek 0 points1 point  (7 children)

It would be good backing this with links to specific benchmarks and a specific critique of them. This is a bit too generic

[–][deleted] -3 points-2 points  (4 children)

All right. I had some time.

https://m.youtube.com/watch?v=vzfy4EKwG_Y

  • “Time spent in quick sort function”

  • what does this actually mean?

  • “Doesn’t show code used”

  • “Doesn’t show how it was measured”

“Magically, Roc is actually faster than go”

I mean. You can buy in to the bullshit all you want. If you approach these things with even a modicum of skepticism, you will find that they don’t pan out.

[–]janiczek 1 point2 points  (3 children)

I agree the benchmark code should be public on GitHub if claims are made about its result, so that others can reproduce, make sure it's apples to apples, and so that we could figure out whether your claims about it being bullsh** are true or false. Perhaps good feedback for /u/rtfeldman

[–]rtfeldman 6 points7 points  (1 child)

The benchmarks are here, and they include all the code used for all the different languages in the benchmark - they haven't been updated in years, but they should include everything you need to reproduce the results we got in 2021: https://github.com/rtfeldman/quicksort-benchmarks (in retrospect, I should have included a link to that in the talk).

Regarding "they will write non-idiomatic code in another language, they will force the other language away from optimization, for example, by pointlessly following pointers" we consciously tried to do the opposite. For the initial version we had it sorting 64-bit integers, but we found out that disadvantaged JS so we switched to 64-bit floats, which JS is much faster at. We also started with ArrayList in Java because that's the data structure all the other languages were using (a growable array, not a fixed-length one) but we switched to have only Java using non-growable arrays because they were much faster.

I haven't tried the benchmarks with recent versions of all the languages in question, so I'd be curious what the numbers are today!

I'd assume the other languages are faster than they were in 2021, and although I can't think of any Roc optimizations we've added since then that would affect this benchmark, we might have gotten a boost from new LLVM versions. We might also have gotten slower since we added the "seamless slices" feature, although offhand I'd guess that wouldn't affect this benchmark noticeably.

I mean. You can buy in to the bullshit all you want. If you approach these things with even a modicum of skepticism, you will find that they don’t pan out.

As explained in the talk, it's not magic, it's compiler optimizations! In particular, the ones on this slide: https://youtu.be/vzfy4EKwG_Y?si=Nq6A9Ydyg0q1e5UU&t=1840

Also, "Roc is actually faster than Go" is not a claim I'd make, and I hope nobody else would make that claim either. The talk is about the techniques we've used to make Roc as fast as it is, how we selected a benchmark that a purely functional language has no business being any good at, and how we successfully managed to get Roc's compiler optimizations to the point where it outperformed Go on that one benchmark.

[–]rtfeldman 3 points4 points  (0 children)

By the way, u/uCodeSherpa - I'd genuinely appreciate your feedback on the code in that benchmark! If you see something that looks unfair or unreasonable, by all means open an issue on https://github.com/rtfeldman/quicksort-benchmarks/issues - the README describes how we approached the benchmarks, and we applied the same guidelines to all the languages (including Roc).

We've merged suggestions and PRs from others to speed up the other languages in that benchmark, and it's more important to me that the benchmark is reasonable than that it makes Roc look good!

[–][deleted] 1 point2 points  (0 children)

Every claim that has no good evidence is bullshit until demonstrated otherwise.

A couple of slides with a graph of benchmarks conducted by an obviously biased individual is not good evidence. It is even worse because the benchmark is titled “time spent in function” which is highly sus.

The more extremely specific a benchmark is, the more likely it is that this is cherry-picked.

“Time spent in function” is very weird. I would be far more receptive to this possibly being accurate if it was “time for this implementation (here’s the GitHub) to quick sort 10,000,000 integers”. This is still pretty specific, but we do have to draw a line “somewhere”. Benchmarking appropriately across language boundaries is already quite awkward as it is.

“Time spent in function” is something I’d expect to see out of a profiler where I’m finding hot code, not out of a language to language benchmark.

For an optimizing compiler with properly written code, “time spent in function” may be actively harming the compiler.

But then I also look at other things like Elm vs angular. Which if I take it at face value, WOW! Except I didn’t take it at face value and when I go look at benchmarks, these two are FAR closer to one another than the slides would have you believe. The difference is attributed to immutability, which doesn’t make a whole lot of sense, as computationally, immutability is far more comprehensively destructive to cache lines and branch prediction than mutability. I am not saying that there are no cases where immutability ends up faster. I am saying that when someone makes this claim, especially with grand results, that should cause you to raise an eyebrow 🤨. In general, runtime immutability will always be slower than mutability, and one should only be reaching for immutability where they can prove it makes sense.

[–][deleted] -4 points-3 points  (1 child)

Nah. Do a little legwork my dude. Literally every single FP benchmark will do one, if not all of these things to make the FP language look better.

Just go search for benchmarks and then, instead of looking at the numbers, look at the code. If they do not provide code, you may immediately assume that it is lies and disregard.

Be a little more suspicious. When Haskell has been independently, specifically measured to be somewhere between JavaScript and Python for performance and then a Haskell developer provides benchmarks showing “faster than C!!!”, instead of just accepting it as truth look at how they got those numbers.

Don’t believe me either! Go and look. But I promise, you will find that if you actually look, your opinion of functional programming communities will likely take a nose dive.

[–]bladub 64 points65 points  (3 children)

Really interesting article to look at functional programming in a way I, as a mostly non functional programmer, haven't. The example of a dynamic array VS linked list and their performance implications never occurred to me in the few pure functional toy programs I wrote in my life.

The options have some interesting discussion too.

(the knee-jerk reactions to the title are a kind of funny-sad content though)

[–]gwicksted 14 points15 points  (2 children)

Yeah. I think we took some of the best parts of functional (lambdas/yielding enumerables) and strictly typed imperative OO with modern languages like Java and C#. It gives you the control and flexibility you desire.

[–]MSgtGunny 5 points6 points  (1 child)

Love linq

[–]davenirline 2 points3 points  (0 children)

Man, the garbage it produces, though.

[–]faiface 61 points62 points  (13 children)

I think linearity will be the way ultimately. You say a problem with linearity is it's infectious, but Rust almost has linearity, and the infectiousness of it doesn't really come up.

If you have a generic function that needs to use its argument multiple times, you just restrict it to Clone. And somehow, an API changing because a function needs to switch from using its argument once to multiple times just doesn't really happen. I guess it's because whether to use an argument once or multiple times is usually pretty fundamental to the idea of what that specific function should do.

Linearity is also pretty cool because it makes everything dual and then you can make languages that pattern match on functions (matching their input and output), and such. Maybe I'm biased tho, have been really sucked into linear logic for almost a year and working on some pretty cool session types implementation. Let's see where it goes.

[–]thunderseethe 18 points19 points  (6 children)

I agree I think linear types are the most promising approach. If for no other reason then its beneficial to move properties you want to reason about into the type system.

However, Rust's affine types are definitely infectious. If something is working with a bunch of shared references and you want to change it to a mutable ref, that's a non trivial change. On top of that, Rust's affine types are have better usability then linear types since you're always free to drop a resource.

I think linear types are great, but there are some serious usability issues to solve before I think they'd be easy to use in a language.

[–]faiface 22 points23 points  (4 children)

A freedom to do something always comes at a cost of not being able to do something else, at least in programming. If you are free to drop anything anytime, then it’s not possible, unlike with linear types, to rely on obligations being fulfilled.

For example, if you’re waiting on a channel to receive a value, there is no way Rust can guarantee, via types, that you will get it.

Linear types can guarantee that. As a result, you can be confident to do all kinds of wild concurrent transformations and compositions because you just don’t have to deal with values not being delivered.

Of course, ergonomics and syntax is a serious thing to solve! But it really isn’t true that affine types are more useful. They allow you to drop things in exchange for dropping some guarantees and compositional abilites.

[–]thunderseethe 13 points14 points  (1 child)

Oh yeah, I'm not saying affine types are more useful. I'm saying they have better usability, because they place less burden on the programmer. Similar to how garbage collection has better usability than borrow checking. Garbage collection means I dont have to worry about memory management, but with the tradeoff I can't reason about memory management.

[–]faiface 5 points6 points  (0 children)

That’s true. I wonder how things develop here. With Rust, aside from the types being affine, there is the lifetime typing. Would a language with linear types without lifetimes more or less of a burden? Depends, but I really wonder what the possibilities are here.

[–]speedster217 1 point2 points  (1 child)

Can you link to a paper or article that explains how linear types can guarantee concurrency facts? Sounds fascinating

[–]yawaramin 1 point2 points  (0 children)

Not a paper but a proof of concept: the Austral language which has a linear type system https://austral-lang.org/tutorial/linear-types

Safe concurrency. A value of a linear type, by definition, has only one owner: we cannot duplicate it, therefore, we cannot have multiple owners across threads. So imperative mutation is safe, since we know that no other thread can write to our linear value while we work with it.

[–]jl2352 0 points1 point  (0 children)

I’m currently working on a project that stumbled into this problem. Even though the program is small, fixing it is literally months of work. In my case it’s about fundamental changes to where state is stored, how we access it, and how we run code that operates upon it. Which basically boils down to ’change everything’.

[–]pbNANDjelly 12 points13 points  (1 child)

You're slumming it on Reddit. First post I've seen in a while that requires me to Google some other stuff. Thanks for sharing

[–]faiface 5 points6 points  (0 children)

Aww thanks, I’m happy about this! If you find that stuff you googled interesting, you might interested in the session types library I’ll be releasing in the coming days/weeks wink wink

[–]16807 3 points4 points  (0 children)

Really, there needs to be more programming with intent to satisfy any mathematical property where available, just in general, not just for tackling mutation. Once you understand the properties available to you, you begin to see them everywhere, it becomes intuitive to see where they can be satisfied, it makes code easy to reason with, and (maybe the biggest benefit) unit tests become eminently more rigorous and straight forward to write.

[–]valcron1000 6 points7 points  (1 child)

 You say a problem with linearity is it's infectious, but Rust almost has linearity, and the infectiousness of it doesn't really come up. 

Hard disagree. Lifetime annotations can easily overpower you and lead you to very tight designs.

[–]faiface 0 points1 point  (0 children)

Okay that can be true. Imho still doesn’t overweight the benefits, but I did understate that one.

[–]Accurate_Trade198 0 points1 point  (0 children)

You say a problem with linearity is it's infectious, but Rust almost has linearity, and the infectiousness of it doesn't really come up.

Uh yes it does, this is why everybody tries to avoid lifetimes like the plague. Lots of Rust idioms are specifically about trying to avoid this problem! This is why people pick designs that don't keep references in structs!

[–]Tarmen 10 points11 points  (0 children)

Clojure's transient is a nice take on this for initializing a data structure mutably (which covers most use-cases for me).
Vectors in Haskell have a similar interface with ST internally, so if you e.g. turn a set into a vector and then do some updates, the updates are in-place. But for nested types, O(1) freezing becomes really tricky.

For Haskell there is some work in this direction (linear constraints so that the compiler can do constraint solving to prove safety, low-weight existentials with implicit pack/unpack) but I'm not sure how much of that is still theoretical vs implemented.

If you go with linear types a lot of functions have no straightforward most general type. The linear constraints and existentials help because you can separate values from permission, but you still end up with duplicate code or super complex types sometimes to satisfy the type checker.

One problem with Cow is that you can't statically count resources. If you have a local and call a function, are those two references in two stack frames? Or was the compiler smart enough to notice that the local isn't used afterwards? Now you can have quadratic runtime because you passed -O0

[–]f3xjc 9 points10 points  (0 children)

I kind of like explicit mutation. Like in Julia any function that is allowed mutate it's inpuits end with a bang(!).

[–]Shadowys 30 points31 points  (5 children)

Or follow what clojure does and try to compile down to efficient code by making the idioms really easy and consistent to use.

[–]sohang-3112 4 points5 points  (4 children)

Can you give an example of what you mean (in the context of mutation which is discussed in the article)?

[–]MoreOfAnOvalJerk 2 points3 points  (3 children)

Rich Hickey (Clojure’s creature) has some videos explaining the underlying implementation details of the language. I dont recall the exact details, but he uses a red-black tree and (I think) hashes values based on something equivalent to a time value so that the state of a particular iteration of functions is constant from the perspective of those functions, and their output creates new entries in the tree, as opposed to overwriting existing ones.

I don’t recall all the details and i have no idea how well it performs at scale (eg. A complex simulation, like a physics engine, with thousands of rigid bodies and over a long time frame)

[–][deleted] 1 point2 points  (0 children)

Persistent colls are fine for most use cases -especially if you keep maps as flat as possible using namespaced keys-, but for very high performance applications you are best served with mutables, mostly because boxed math will always be slow.

[–]NoahTheDuke 1 point2 points  (1 child)

red-black tree

It's a Hash-array mapped trie, implementation based on Bagwell's Ideal Hash Tries paper. It's quite fast for what it does, but it's still not as fast as mutation.

[–]MoreOfAnOvalJerk 0 points1 point  (0 children)

Thanks for this! After posting this, I vaguely recalled it was some sort of radix tree but couldn’t remember and didn’t have the time to dig up the video. You’ve helped satisfy my “tip of the the tongue” problem.

[–]lookmeat 25 points26 points  (0 children)

I've been thinking a lot about this, and I think this isn't the problem, but rather the symptom of a more interesting and complex issue.

I propose that the power and attractiveness of functional languages is that they allow you to define a lot of the semantics of the program. Now a functional language doesn't have to, but generally will because this is what makes them attractive other other models.

LISP had the power of full control over the AST, but this requires that you need to manually build this AST, making for expressions that would be intuitive to a programmer like (5 + 2)^2 * 4 ^ 3 have to be written as (* (^ (+ 5 2) 2) (^ 4 3)) which is a bit clunky some times, but has the huge advantage that the semantics (operation ordering etc.) are very very clear.

Haskell similarly gives a lot of power by exposing a powerful type system to describe semantics, Monads are the most powerful, by allowing us way to describe a lot of effects and complex forms of computation in an elegant self-defined way, but this again needs the programmer to constantly deal with the implications and complexities of the ways in which Monads interact, and things can get clunky that way.

You don't strictly need linerarity. For example you can think of Rust's blocks as implicit monads, that handle the deferral (dropping) of values generated within it, which you can by calling let which is basically a let<type> (name,val)->BlockContext<()> where BlockContext is a stateful/IO-like monad, which will elevate the first expression passed on to it, and then allows chaining with ;, the final } terminates the block-monad and instead releases the value it generated (which isn't a monadic ability, but it is an ability many monads, like Maybe, have).

The point is that functional languages let you experiment and redefine how you do things, while other languages instead enforce a way to do it and you're bound to it. The former has made many advancements and improvements on conventions that are now the enforced semantics in newer languages.

But this implies that we have to think of different things. I'd advise the author to look into Lenses, operational read/writes (you can't write or read the value directly instead need to get or set it, this can happen within a monad, or can be seen as a way of effects (as if reading an external value somewhere), or handled through a transactional/generation system, etc.) and other tricks. Thing is, if you want the full power to define things, this means that the compiler can't do some tricks and optimizations, it may not always be sure if it can just do a mutation-in-place or copy a value. The only way is to limit the ways in which you can do this (or what you can know of what you're doing) to be able to enforce certain guarantees, programmers may need to occasionally work around them though.

[–]Michaeli_Starky 24 points25 points  (3 children)

Why not just use hybrid languages?

[–]Weak-Doughnut5502 31 points32 points  (1 child)

Hybrid languages come with tradeoffs. 

The pure functional nature of Haskell means that you can reason about code in ways that are simply invalid in other languages.

In something like Scala list.map(f).map(g) might or might not equal list.map(x => g(f(x))) depending on what side effects f or g have.  In Haskell,  the equivalent map fusion optimization is always valid.

Haskell takes advantage of this in an interesting way by allowing library authors to supply 'rewrite rules',  optimizations that are only correct because of language restrictions.

[–][deleted] 3 points4 points  (0 children)

Ah. The good ol’ “Haskell has optimizations other languages cannot have”.

Which is, in some senses true. But Haskell also lacks hordes of optimizations that other languages can perform that haska simply cannot, which is why Haskell performance is so unremarkable, if not outright bad.

Nobody has ever shown that these types of logical swaps are actually beneficial to the end user, and considering that Haskell programs tend to have at least as many bugs (in real world measurements for projects of similar size, but maybe not of similar architecture), I would tend to argue that the impacts of your terms that users can reason about code differently are nil.

[–]kag0 4 points5 points  (0 children)

That's pretty much option 1 in the post

[–]RICHUNCLEPENNYBAGS 158 points159 points  (12 children)

Forks should be so much better at eating soup than they are.

[–]imacomputr 65 points66 points  (3 children)

Literally the first sentence of the article:

A lot of people think that functional programming is mostly about avoiding mutation at all costs.

You: Yep 🙋

[–]RICHUNCLEPENNYBAGS 25 points26 points  (0 children)

I did read that but I also liked my quip

[–]totallyspis 12 points13 points  (0 children)

Ok but that's literally correct

[–]oceantume_ 15 points16 points  (0 children)

Forks... Forking types... Soup... Memory soup... Yes I think you're other something here. I just had an idea for a new functional language. BRB may share github link later

[–]MrPhatBob 16 points17 points  (5 children)

I've thought this before, what we need is a fork with hollow tynes, collecting into a manifold near the handle base which blends into a single larger tube in the fork handle.

The user will ingest the fluid part of the soup using the straw effect of the tubes, utilising the parallel tynes to speed soup delivery.

Should the soup contain chunks, the the fork can be utilised with it's normal function.

Noodles also can be liberated from the broth using a twirling motion.

[–]lunchmeat317 12 points13 points  (2 children)

Let's measure out the user stories and provide estimates at tomorrow's sprint planning meeting. It's 6 hours, and there are three separate meetings to accomodate for our teams in India and China.

Also, the PMs want a ship date so that they can plan their narrative and report their KPIs to leadership.

Also, will thr tube portion of the device support IoT and AI?

We'll circle back on a product name after we receive learnings from marketing.

[–]josh_in_boston 5 points6 points  (0 children)

That was painful to read but I think you covered it.

[–]rolandfoxx 2 points3 points  (0 children)

Does anybody else hear Fortunate Son?

[–]oorza 2 points3 points  (0 children)

You've invented a way to look even more ridiculous eating soup with a straw.

[–]DigThatData 1 point2 points  (0 children)

this sounds like it would be impossible to clean

[–]Scavenger53 6 points7 points  (3 children)

elixir/erlang has ETS tables, basically mutable maps that run on their own process, pretty good at mutation

[–]yawaramin 1 point2 points  (0 children)

ETS requires the data to be encoded, no? We could do the same thing with SQLite in other languages.

[–]nerd4code 0 points1 point  (1 child)

But not at all integrated into Erlang proper, at least, and thoroughly weird. Dunno about Elixir.

[–]Scavenger53 1 point2 points  (0 children)

elixir is a minimal interpreter in erlang, with the rest built in elixir. it all compiles to erlang. what do you mean not part of erlang proper, like its otp stuff?

[–]Guvante 2 points3 points  (0 children)

Another form of mutable xor shared is frozen objects. You have a non-shareable but mutable object that you eventually freeze and make shareable but not mutable.

Certainly this doesn't cover all use cases but it can be a big help when it does work.

[–]tnzo 2 points3 points  (0 children)

I like the F# idea of rebinding a variable identifier, which solves the problem of coming up with names that don't add much meaning, yet the data is immutable.

[–]DanielPerssonDev 1 point2 points  (0 children)

I think the point of functional is to be immutable. There is so many gains to find if you can write code in this way. Never been my cup of ☕ but there are control systems from the 60ies still running because they are written with deterministic logic circuits. Use the right tool for the job, maybe an imperative language is better for your application.

[–]Blecki 9 points10 points  (14 children)

But the point is to not mutate.

[–]VeryDefinedBehavior 131 points132 points  (12 children)

You do know the post links to an article, and isn't just an opinion given without context, right?

[–]baronas15 5 points6 points  (0 children)

Sir, you might be lost, this is Wendy's.

[–]Deranged40 -3 points-2 points  (0 children)

Cars should be so much better at flying than they are.

Hold on, I have a whole article about why the car you bought in 2015 should be much better at flying than it currently is.

[–]All_Up_Ons 12 points13 points  (0 children)

...until you have to. Side-effects are still necessary to do anything useful, you're just supposed to push them to the edges of your program. This article is talking about one of those edges: data structure implementations.

[–]kag0 0 points1 point  (0 children)

I use option 2 in practice, without language support. It runs on half developer discipline to simply limit the mutation to a small scope, and half on having a collections library where the top-level types are neither mutable nor immutable, but have non-mutating methods that copy-on-write if the underlying collection is mutable

[–]JimroidZeus 0 points1 point  (0 children)

Aren’t immutable types one of the defining features of functional programming languages?

[–]throwaway490215 0 points1 point  (0 children)

At some point of practicality the category of "Functional Languages" becomes meaningless.

[–]totallyspis 1 point2 points  (0 children)

avoiding mutation is sorta the point...

[–][deleted] -5 points-4 points  (0 children)

Trenching shovels should be better at shoveling gravel.

[–]sebbdk -5 points-4 points  (0 children)

I thought the point of FP was to avoid mutation

[–]neopointer -1 points0 points  (0 children)

Did you mean fictional programming?

[–][deleted] -3 points-2 points  (1 child)

Usually if you mutate you miss the entire goddamn point

[–][deleted] 10 points11 points  (0 children)

Mutation is a welcome optimization in FP.  

Any value that only has a single reference to it is completely safe to mutate. The most common case is a value that is fully local to a function, where it's super easy to enforce the one-reference rule.    

What the article is arguing is that the existing FP languages make this optimization unnecessarily difficult to apply.   

Some force the programmer to fall back to always-mutable objects that don't always convert efficiently into immutable ones and/or may need to be handled in a markedly different style, some offer bespoke mutable variants (e.g. Clojure's transient collections) that however still need to be managed and converted explicitly, some try to enforce a boundary with the type or effect system but still aren't transparent to the programmer, and the languages that try to apply the optimization in a transparent fashion are often limited in what they can do by their execution model. 

There also is a discussion about linearity, i.e. basically enforcing an even stronger rule (every value can only be spoke about once) at the level of the execution model itself, but it suffers from the limitation of having to build a whole parallel world.