Live data from Hacker News

Critique of Lazy Sequences in Clojure

clojure-goes-fast.com

41–50 of 52 posts

Re: Critique of Lazy Sequences in Clojure

#41
post #16

As I posted on Reddit: It might also be good to mention Injest https://github.com/johnmn3/injest Which makes transducers more ergonomic to use if you are like me and use threading macros everywhere Would be curious to hear how others feel about it

Another great library that decidedly reveals transducer value: https://github.com/cgrand/xforms

Re: Critique of Lazy Sequences in Clojure

#42

Earlier quoted context omitted.

It's not really Rust's compiler that 'optimises lazy sequences'. It's LLVM, which notices that the code emitted happens to be able to be optimised down, if you run it for a really, really long time with some very strong optimisations. GHC would be a better example, I think. It performs stream fusion. This means it can turn 'map f (map g xs)' into 'map (f . g) xs', and of course it gets more complex than that, but tha…

> GHC would be a better example, I think. It performs stream fusion. This means it can turn 'map f (map g xs)' into 'map (f . g) xs', and of course it gets more complex than that, but that's the basics. It directly optimises lists (which, this being Haskell, are lazy sequences). Is it for built-in map or it would work in a general way for, say, `myMap f (myMap g xs)` ?

> Is it for built-in map or it would work in a general way for, say, `myMap f (myMap g xs)` ?

Just the stdlib `map`, but it's all ordinary library code. You can easily add your own rewrite rules.

Re: Critique of Lazy Sequences in Clojure

#43
post #31

Earlier quoted context omitted.

You won't start anything. I dare you. Haha. You can't learn Clojure and you know it.

I hope you find something better to do with your day than putting people down.

He's overanalyzing. I think it's best to choose one randomly, even with a coin toss and learn enough about it. Then later he'll know better what to choose next. I'm not trying to put down anyone. I'm trying to challenge the person to actually do something instead of doing analysis-paralysis.

Re: Critique of Lazy Sequences in Clojure

#44
post #6

I've been circling around lisp for a couple of years. I'm starting in a month, I'll spend several hours a day. I still don't know what language I want to learn. I was drawn to Clojure because it looked like a lisp for getting stuff done. But a few things put me off. This article puts me off more. I want to get the semantics down before I have to think about what's going on under the hood.

> I've been circling around lisp for a couple of years. I'm starting in a month, I'll spend several hours a day.

Is this just a personal goal you’re setting? I’m curious because I’m in a similar position, so I’d love to hear your plan!

Re: Critique of Lazy Sequences in Clojure

#46
post #36

> The good parts of laziness: Avoiding unnecessary work Actually be very careful with side effects. Some functions like `map` and `for` take things in chunks, typically in steps of 32 as most underlying structures are in log-32 leaves. ``` (let [printing-range (map (fn [i] (print "debug: " i) i) (range)) first-10 (take 10 printing-range)] first-10) debug: 0 debug: 1 debug: 2 debug: 3 debug: 4 debug: 5 debug: 6 debug:…

It can be legitimate to have side effects in lazy processing and in particular to rely that a lazy sequence is not accessed beyond the visible access that is coded in the program.

Suppose we make a sequence of numbers which grows very rapidly, so that by the time we hit the 17th one, we have a bignum that is gigabytes wide.

You probably don't want this to be chunked in batches of 32.

Another situation might be if we have some side effect: the lazy sequence is connected to some external API somehow or foreign code. You might want it so that the observable behaviors happen only to the extent that the sequence is materialized.

The advice to be careful with side effects is good in general; not sure why you're downvoted.

Re: Critique of Lazy Sequences in Clojure

#47

It’s too bad that transducers were created long after clojure’s inception. Can you always replace a lazy seq with a transducer? Could the language theoretically be redesigned to replace all default usages of lazy seqs with transducers, even if it were a major breaking change? And have lazy operations be very explicit?

Transducers were at least 20 years old when Clojure was first created. I have the book "Common Lisp: the language" from 1989 that describes transducers as found in Clojure.

Clojure is the only language where it is baked in that prominently though.

Re: Critique of Lazy Sequences in Clojure

#48
TXR Lisp also fails this test:

  1> (len
       (with-stream (s (open-file "/usr/share/dict/words"))
         (get-lines s)))
  ** error reading #: file closed
  ** during evaluation of form (len (let ((s (open-file "/usr/share/dict/words")))
                                      (unwind-protect
                                        (get-lines s)
                                        (close-stream s))))
  ** ... an expansion of (len (with-stream
                                (s (open-file "/usr/share/dict/words"))
                                (get-lines s)))
  ** which is located at expr-1:1
The built-in solution is that when you create a lazy list which reads lines from a stream, that lazy list takes care of closing the stream when it is done.

If the lazy list isn't processed to the end, then the stream semantically leaks; it has to be cleaned up by the garbage collector when the lazy list becomes unreachable.

We can see with strace that the stream is closed:

  $ strace txr -p '(flow "/usr/share/dict/words" open-file get-lines len)'
  [...]read(3, "d\nwrapper\nwrapper's\nwrappers\nwra"..., 4096) = 4096
  read(3, "zigzags\nzilch\nzilch's\nzillion\nzi"..., 4096) = 826
  read(3, "", 4096)                       = 0
  close(3)                                = 0
  fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
  write(1, "102305\n", 7102305
  )                 = 7
  exit_group(0)                           = ?
  +++ exited with 0 +++
It is possible to address the error issue with reference counting. Suppose that we define a stream with a reference count, such that it has to be closed that many times before the underlying file descriptor is closed.

I programmed a proof of concept of this today. (I ran into a small issue in the language run-time that I fixed; the close-stream function calls the underlying method and then caches the result, preventing the solution from working.)

  (defstruct refcount-close stream-wrap
    stream
    (count 1)

    (:method close (me throw-on-error-p)
      (put-line `close called on @me`)
      (when (plusp me.count)
        (if (zerop (dec me.count))
          (close-stream me.stream throw-on-error-p)))))

  (flow
    (with-stream (s (make-struct-delegate-stream
                      (new refcount-close
                           count 2
                           stream (open-file "/usr/share/dict/words"))))
      (get-lines s))
    len
    prinl)
With my small fix in stream.c (already merged, going into Version 292), the output is:

  $ ./txr lazy2.tl
  close called on #S(refcount-close stream # count 2)
  close called on #S(refcount-close stream # count 1)
  102305
One close comes from the with-stream macro, the other from the lazy list hitting EOF when its length is being calculated.

Without the fix, I don't get the second call; the code works, but the descriptor isn't closed:

  $ txr lazy2.tl
  close called on #S(refcount-close stream # count 2)
  102305
In the former we see the call to close in strace; in the latter we don't.

Re: Critique of Lazy Sequences in Clojure

#49
post #16

As I posted on Reddit: It might also be good to mention Injest https://github.com/johnmn3/injest Which makes transducers more ergonomic to use if you are like me and use threading macros everywhere Would be curious to hear how others feel about it

these look great to me. would there be a downside to adding them to core?

Macros that rely on parsing and rewriting their bodies are great way to introduce bugs. The regular threading macros work well enough because they are simple. More complex rewrites don't compose with other macros.
Post reply on HN