Live data from Hacker News

Avoiding space leaks at all costs

kodimensional.dev

31–40 of 47 posts

Re: Avoiding space leaks at all costs

#31

Earlier quoted context omitted.

GHC language extensions are like C++ Technical Reports. They are a way to evolve the language without excessive red-tape on an opt-in basis, and periodically the ones that worked out make it into a new “standard”. It’s not really all the different from Python PEPs or Rust’s various proposals / nightly stuff. Just about every language has some form of this.

That's true. And when somebody says "oh you can just use GCC's special feature to make C++ tolerable" they rightly get pushback because only a portion of these features ever make it into a standard. This means that over time you end up with a codebase that is forever tied to these nonstandard features and might even end up being incompatible with major language changes. Or maybe the feature you adopted does get stand…

> Or maybe the feature you adopted does get standardized but in a slightly different and incompatible way and now you've got a big problem on your hands.

Haskell is de facto a single-implementation language: an extension is not going to be made incompatible with some future Haskell standard without having been long since deprecated in GHC.

Re: Avoiding space leaks at all costs

#32
post #14
post #7

Earlier quoted context omitted.

In all languages, sometimes you want eager evaluation and sometimes you want lazy evaluation. If things are lazy by default, you simply need to put a "!" before the term to evaluate in order to get eagerness. If things are eager by default, you need to rewrite the algorithm as a streaming algorithm.

There's no inherent reason why doing lazy evaluation in an eager-by-default language should be any harder than the reverse. I can imagine a language that is eager by default but has a '#' operator that holds expressions in unevaluated form until terms are requested.

While you're technically right, it's only in the same sense that any TC language can do anything any other TC language can -- you can theoretically always delay any computation behind a lambda, but the ergonomics have wide-ranging consequences. You also don't get any memoization that way -- without further runtime support.

There was a good Reddit thread recently where a lot of experts weighed in: https://www.reddit.com/r/haskell/comments/wpbs4z/what_things...

I recommend reading the thread -- lot's of great stuff in there, but...

TL;DR: It's a lot more complicated than it seems at first and it's far from obvious that strict-by-default is worth the cost.

Re: Avoiding space leaks at all costs

#33

Earlier quoted context omitted.

That's true. And when somebody says "oh you can just use GCC's special feature to make C++ tolerable" they rightly get pushback because only a portion of these features ever make it into a standard. This means that over time you end up with a codebase that is forever tied to these nonstandard features and might even end up being incompatible with major language changes. Or maybe the feature you adopted does get stand…

Just about every successful C/C++ project uses flags or intrinsics or whatever to some degree. It’s not unusual or particularly problematic. Linux famously uses a bunch of GCC stuff (though I gather that’s in decline) and non-standard defaults like ‘-fno-strict-aliasing’. And the extensions in the article are for the most part in the GHC2021 standard. So it’s more like, this is in C++20. Younger languages like Rust o…

> how else do you grow a language for 40 years?

You could introduce things in one version of the standard, and then remove them in the next release if people didn't like it, such that to get the feature you have to specifically use --std=2022a, no earlier, no later; and if you want your project to track the evolution of the language rather than getting stuck on a particular version, you'd have to rip use of such features back out.

You could sort of think of it as if you had a dependencies lockfile which includes a version-constraint on the major version of the compiler; where every time there's a change in what code is accepted, that major version of the compiler goes up. Any change other than changing what code is accepted as valid goes into the lower semver fields; and gets replicated across all major versions of the compiler, where relevant. (Except that you don't have to do this by actually maintaining separate major-version branches of the compiler for every standards release; you can do it the way we currently do feature flags, only the only flag you get is which standard-version your project is written for.)

I don't think I've ever seen anyone actually do this for a language/compiler; but it's a common approach used for protocol and API-client libraries, and seems to work well there.

Re: Avoiding space leaks at all costs

#34

Wait, the first suggested fix for "lazy evaluation consumes too much memory" is "make it eager"? I'm sure I'm missing some nuance here, but that seems backwards.

Yes. Which is my main criticism of Haskell: sure, sometimes laziness is very handy, sometimes it's even indispensable... but most of the time, eager is what you want, with judicious sprinkles of explicitly lazy structures (btw, OCaml has "lazy" keyword exactly for that). Sure, GHC goes to heroic lengths and turns as much lazy computations as it can into eager ones, but there is always a limit to what it can do on its…

In OCaml lazy data is fundamentally incompatible with strict data -- you have to explicitly evaluate it, e.g. when passing to a function which takes strict data. This means you end up with two disjoint 'worlds' where you need to write algorithms twice, etc. (In fact you'd have to do that for every possible variant of where exactly the laziness lies.)

The ergonomics become incredibly bad, unfortunately.

(Also see my link to a Reddit thread elsewhere in this thread further ideas on why strict-by-default is not a simple win.)

Re: Avoiding space leaks at all costs

#35
> For now, this has nothing to do with lazy evaluation. Such implementation will be slow in every language. It happens because add doesn’t use tail-call recursion.

This is why I'll never really trust Haskell to be usable in a serious context. Haskellers will talk your ear off about how such restricted abstractions enable the compiler to optimize it to hell and back, and perfectly idiomatic Haskell can be compiled to almost-perfect machine code, but in point of fact GHC basically never does, and you still have to care about things like tail calls and design APIs to do things like continuation passing.

Re: Avoiding space leaks at all costs

#36

Earlier quoted context omitted.

That's true. And when somebody says "oh you can just use GCC's special feature to make C++ tolerable" they rightly get pushback because only a portion of these features ever make it into a standard. This means that over time you end up with a codebase that is forever tied to these nonstandard features and might even end up being incompatible with major language changes. Or maybe the feature you adopted does get stand…

Just about every successful C/C++ project uses flags or intrinsics or whatever to some degree. It’s not unusual or particularly problematic. Linux famously uses a bunch of GCC stuff (though I gather that’s in decline) and non-standard defaults like ‘-fno-strict-aliasing’. And the extensions in the article are for the most part in the GHC2021 standard. So it’s more like, this is in C++20. Younger languages like Rust o…

What's insufficient about the way Rust's already growing?

Re: Avoiding space leaks at all costs

#37
post #14
post #7

Earlier quoted context omitted.

In all languages, sometimes you want eager evaluation and sometimes you want lazy evaluation. If things are lazy by default, you simply need to put a "!" before the term to evaluate in order to get eagerness. If things are eager by default, you need to rewrite the algorithm as a streaming algorithm.

There's no inherent reason why doing lazy evaluation in an eager-by-default language should be any harder than the reverse. I can imagine a language that is eager by default but has a '#' operator that holds expressions in unevaluated form until terms are requested.

The combination of lazy evaluation and state mutation/side effects can be pretty difficult to reason about. For example, if you have a function that changes a global variable as a part of a lazy computation, once that function could have been called you have no way of knowing if or when that global variable will change in the future. If you have other functions that depend on the value of that variable, their future behavior is now much more challenging to reason about than in a strict language. You can also imagine something akin to a race condition in which there are multiple lazy computations which could eventually set that variable to different values and the actual sequence of state transitions depends entirely on the dependency order of a possibly unrelated piece of code. In practice, this means that in languages that are strict by default, lazy computations are often forced to run in order to reason about the code, rather than because the actual results of the computation are required.

Since pure functions compute the same results under lazy or strict evaluation and require that any data dependencies they have are explicitly provided as inputs, they interact with lazy computations in a much more tractable way. This means that adding a strictness operator to a lazy language is much easier than adding a laziness operator a a strict language.

An alternate approach is what python did with generators where there is a data type for lazy computation, but it lives apart from the rest of the language, so it is mostly used for e.g. stream processing where a default-lazy approach is conceptually straightforward and is less likely to lead to extremely non-trivial control flow. This approach does, however, basically give up on having a laziness operator that will turn a strict computation into a lazy one.

Re: Avoiding space leaks at all costs

#38
post #5
post #4

> Fortunately, this is easily possible with Haskell. You need to enable the BangPatterns feature and use exclamations ! in front of patterns for variables where you want the evaluation to be performed eagerly. Of course, this is simple - you just enable a language extension so that the language you are referring to Haskell is no longer really Haskell. I think Haskell is a fine language, but given the amount of variou…

It is Haskell, just not Haskell2010. It is included in GHC2021. Would you say that a company that uses a subset of C++ as a policy is not using C++? Their definition of the subset is as arbitrary as another’s that uses BangPatterns.

I think his point is that a naive implementation of an extremely simple function uses 35x more memory than the 'optimal' implementation, which is not at all obvious without knowing internal details of the compiler.

Re: Avoiding space leaks at all costs

#39
post #29

Earlier quoted context omitted.

C and C++ programmers who write only or mostly for a single platform often have some extensions they insist on, and may even regard the standard language (lacking these features) as defective. Linux won't compile in MSVC. Obviously it needn't, it's a kernel and not a Windows program, but it also can't, because it needs GNU extensions, which are now also implemented for Clang. If you write broadly cross platform softw…

> The ISO document used to say C++ has optional garbage collection I'm intrigued; what was this feature supposed to look like?

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n26...

The standard basically said, if your compiler supports this new feature, here's how you use it in your programs. And no compilers have ever supported the feature. So C++ 20 rips it back out.

Re: Avoiding space leaks at all costs

#40
post #34

Earlier quoted context omitted.

Yes. Which is my main criticism of Haskell: sure, sometimes laziness is very handy, sometimes it's even indispensable... but most of the time, eager is what you want, with judicious sprinkles of explicitly lazy structures (btw, OCaml has "lazy" keyword exactly for that). Sure, GHC goes to heroic lengths and turns as much lazy computations as it can into eager ones, but there is always a limit to what it can do on its…

In OCaml lazy data is fundamentally incompatible with strict data -- you have to explicitly evaluate it, e.g. when passing to a function which takes strict data. This means you end up with two disjoint 'worlds' where you need to write algorithms twice, etc. (In fact you'd have to do that for every possible variant of where exactly the laziness lies.) The ergonomics become incredibly bad, unfortunately. (Also see my l…

The same split exists in Haskell as well: Data.Text.Text vs Data.Text.Lazy.Text, etc. And in LISP IIRC lazy data are type-compatible with strict data.
Post reply on HN