[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)