[HN Gopher] Avoiding space leaks at all costs
___________________________________________________________________
Avoiding space leaks at all costs
Author : ghuntley
Score : 53 points
Date : 2022-09-01 09:44 UTC (13 hours ago)
(HTM) web link (kodimensional.dev)
(TXT) w3m dump (kodimensional.dev)
| pie_flavor wrote:
| > 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.
| eptcyka wrote:
| > 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 various language extensions that are needed for it to
| be usable in production without friction, it seems it will
| forever be a research language - and there is nothing wrong with
| that.
| nequo wrote:
| 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.
| kleingeld wrote:
| I believe one could avoid BangPatterns by using seq.
| eptcyka wrote:
| Sure, this language feature is mainstream now. But most real-
| world haskell codbases vary greatly in which language
| extensions they've chosen to use.
| kzrdude wrote:
| Is this a C++ benefit, that the competing implementations
| GCC and LLVM give each other's extensions to the language
| more legitimacy by having a second implementation? It
| doesn't seem like language extensions get so much attention
| in C++ codebases (but maybe I'm mistaken)?
| tialaramex wrote:
| 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 software for C++ you
| are committed to supporting all three compilers and so
| that's effectively just the standard. Indeed if all three
| do or don't do something de facto that is the standard.
| The ISO document used to say C++ has optional garbage
| collection, it doesn't because none of the three
| compilers have that. The ISO standard still doesn't say
| pointer provenance is a thing, but all three compilers
| require it, so in fact it's a thing.
| derefr wrote:
| > The ISO document used to say C++ has optional garbage
| collection
|
| I'm intrigued; what was this feature supposed to look
| like?
| tialaramex wrote:
| 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.
| jesse__ wrote:
| 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.
| mrkeen wrote:
| If our arguments can be so pedantic, then I can say: sure,
| Haskell is forever a research language. But that's irrelevant
| to me, because I use no-longer-really-Haskell.
| Brian_K_White wrote:
| I agree with the original critique more than this defense,
| but I had to upvote the perfection of the expression of the
| point I don't agree with.
| benreesman wrote:
| 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.
| UncleMeat wrote:
| 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
| standardized but in a slightly different and incompatible way
| and now you've got a big problem on your hands.
|
| It is good for people to have ways of playing with a language
| so it evolves. It is (IMO) bad to suggest the adoption of
| these things as a means of making a language production-
| ready.
| hither_shores wrote:
| > 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.
| benreesman wrote:
| 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 or Kotlin or something have
| less pressure for mechanisms like this because you have to
| get to a certain point in the language lifecycle before the
| tradeoff tilts from "let's do a breaking change at a point
| release" to "let's flag in the new behavior", but if (as I
| expect) e.g. Rust lasts as long as C++ or Haskell, it will
| be be bedazzled with all kinds of flags and extensions and
| stuff too, how else do you grow a language for 40 years?
| pie_flavor wrote:
| What's insufficient about the way Rust's already growing?
| derefr wrote:
| > 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.
| skywhopper wrote:
| I have tried and failed to comprehend Haskell for my own uses.
| Certainly most of the examples in this article are meaningless
| gibberish to my smooth, practical brain. So kudos to those who
| grok the underlying math and find it a useful language.
|
| That said, I am a fan of lazy evaluation in the right
| circumstances, and often find myself implementing lazy data
| structures (particularly in my Ruby programs).
|
| Ultimately, though, I'm confused by this article which seems to
| be all about removing laziness from Haskell in ways that seem
| entirely counter to what I take to be its purpose. If you have to
| bend over backwards and rewrite your code in awkward ways to get
| the language to behave the way you want, maybe the project itself
| isn't right for the language? It's one thing to hack around a
| couple of isolated space issues in a larger context, but if you
| are enabling language extensions across the board by default to
| prefer eager evaluation, why are you using Haskell at all?
| benreesman wrote:
| Some would argue that in retrospect strict should have been the
| default, I sympathize with that view personally somewhat.
|
| But every language that beats Darwin for long enough does so by
| iterating the stuff that can be "fixed" and developing
| conventions and tools for the gotchas that can't (practically)
| be.
| koala_man wrote:
| I would love a strict Haskell, but I don't think functional
| programming would have been the same today. Laziness was the
| big motivator for keeping the language uncompromisingly pure,
| and we may not have developed monads and similar without it.
| daniel-cussen wrote:
| I've never heard of or met a circumstance that actually
| warranted Haskell. Not even in finance, those guys are totally
| wasting their time.
|
| It's an attractive (I'm going to say "it seems" though you
| ought to take that for granted) intellectual rathole. Like some
| forms of philosophy, like many things. Thank GOD (despite being
| unbelieving at the time) I went with Lisp instead. And I got
| real speedups.
| benreesman wrote:
| Algorithmic finance might be the most ruthlessly Darwinian
| area of industrial software, it's _way_ up there certainly.
|
| It's also notoriously opaque, which makes it especially hard
| to know that so-and-so is wasting their time.
|
| How about "I don't like Haskell and/or OCaml because X, I
| recommend Lisp instead because Y" as an alternative
| formulation?
|
| That one I'd be interested to read!
| MauranKilom wrote:
| 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.
| Joker_vD wrote:
| 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 own, and you inevitably end with
| the programmer having to slap strict patterns and seqs and
| deepseqs wherever they can reach.
| Quekid5 wrote:
| 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.)
| Joker_vD wrote:
| 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.
| Arnt wrote:
| Lazily evaluating 2+2 requires storing two numbers and an
| operator for a while, eagerly evaluating requires storing just
| one number (4, the result).
|
| This can be a real problem, for example if you read potentially
| large network input eagerly and use it to compute a small
| result lazily. IMO it's always a mistake and IMNSHO the proper
| solution is always something more nuanced than just "make it
| eager" or "make it properly lazy".
| mrkeen wrote:
| 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.
| Joker_vD wrote:
| > you simply need to put a "!" before the term to evaluate in
| order to get eagerness
|
| That's wrong. With the bang pattern, evaluation only goes up
| to WHNF, not the whole way -- it's just a syntax sugar around
| "seq" which has been around since always. If you need to
| fully evaluate the term, look at the deepseq package: it's
| implementation is non-trivial.
| cvoss wrote:
| 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.
| chas wrote:
| 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.
| shirogane86x wrote:
| Sadly even though what you say is true, I do not know of
| any languages that get that right. Even those that do care
| enough to give you a way to make lazy values, they require
| you to explicitly wrap them and force them all over the
| place, making lazy programming effectively so noisy it's
| unusable. Which is the main reason why I still find myself
| coming back to haskell (stuff like parser combinators is
| just so unwieldy without laziness and do notation...)
| MauranKilom wrote:
| Counterpoint: C++ expression templates have existed for a
| long time, and require no explicit work on the user side.
| They are incredibly unwieldy to write and debug though.
| shirogane86x wrote:
| I am gonna be completely honest, I have never quite been
| able to understand expression templates properly. is it
| possible to use them to get hassle-free laziness? is
| there any example of a library that does this (just to
| look at it and see how it's done)?
| MauranKilom wrote:
| Define hassle-free. In readability of user code? Yes. In
| getting clean compiler errors? No.
|
| One example would be Eigen:
| https://eigen.tuxfamily.org/index.php?title=Main_Page
|
| See also their page on lazy evaluation:
| https://eigen.tuxfamily.org/dox/TopicLazyEvaluation.html
|
| Of course, C++ is powerful enough that using specific
| tools can make the expression template abstraction fall
| apart (e.g. auto), but fundamentally you can write matrix
| code very cleanly and get great lazy-evaluated
| performance.
| Quekid5 wrote:
| 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/commen
| ts/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.
| userbinator wrote:
| The phrase "space leak" seemed unusual and a bit of a "tortured
| phrase", since I'm more familiar with this concept as "memory
| bloat" or just inefficiency in general. Upon a little more
| searching it appears to be mainly Haskell jargon.
| assbuttbuttass wrote:
| It's the equivalent of "memory leak" for a garbage-collected
| language
| Traubenfuchs wrote:
| > all mainstream languages have eager evaluation models
|
| That is incorrect, e.g. Java Streams (Javas built in library for
| functional processing of data) are lazy, which can surprise
| beginners. You say it's just an std lib and not a lazy
| programming language? Ok, the much older (but just as lazy) C#
| linq is directly integrated in the C# language, making the C#
| language both eager and lazy by default, depending on which parts
| you use.
| ufo wrote:
| Arguably Java streams aren't really lazy evaluation at the
| language level, because the programmer must use explicit
| lambdas around the lazy bits. Java itself is still eager.
___________________________________________________________________
(page generated 2022-09-01 23:02 UTC)