rhombus-prototype icon indicating copy to clipboard operation
rhombus-prototype copied to clipboard

Discourage usage of thunk

Open sorawee opened this issue 4 years ago • 6 comments

Related to #54

There are several built-in functions that expect a thunk as an argument. E.g., thread. I argue that these should instead be forms that converts their body into a thunk.

Let's consider thread as an example. Most usage of thread is in the form: (thread (lambda () ...)). The extra lambda layer creates unnecessary rightward drift and verbosity. At least in the Racket repository, (thread (lambda () ...)) is used 8 times as often as the other use of thread. The use of the thunk form also doesn't really help any of these problems besides reducing slight verbosity.

~/racket $ ag "\(thread\s++(\(lambda|\(λ)" --stats
...
pkgs/racket-benchmarks/tests/racket/benchmarks/shootout/typed/chameneos.rktl
30:  (thread
31:   (lambda ()
48:  (thread
49:   (lambda ()

pkgs/racket-benchmarks/tests/racket/benchmarks/control/thread.rkt
22:     (thread (lambda ()
38:     (thread (lambda ()
57:     (thread (lambda ()
73:     (thread (lambda ()
94:     (thread (lambda ()
533 matches
140 files contained matches
4255 files searched
48500018 bytes searched
0.352777 seconds

~/racket $ ag '\(thread\s++(?!\(lam|\(λ)' --stats
pkgs/racket-test-core/tests/racket/file.rktl
...
1059:    (thread go)))

pkgs/racket-test-core/tests/racket/sync.rktl
899:(let ([d (thread-dead-evt (thread void))])
990:(let ([t (thread void)])
1056:		 (cons (make-weak-box (thread (if die? void blocking-thunk)))

pkgs/racket-benchmarks/tests/racket/benchmarks/shootout/echo.rkt
10:  (thread client)

pkgs/racket-benchmarks/tests/racket/benchmarks/shootout/typed/echo.rktl
11:  (thread client)
66 matches
35 files contained matches
4255 files searched
48500018 bytes searched
0.121134 seconds

By turning thread into a macro that implicitly creates a thunk, we can make code less verbose in most cases: (thread (lambda () ...)) would become (thread ...). For the other few cases like (thread client), my proposal would make it slightly more verbose... but by only two characters! E.g., (thread client) would become (thread (client)).

The only disadvantage of the proposal that I can think of is that we can no longer use them as higher-order function. E.g.,

(define (just-apply f) (f))
((if b thread just-apply) (lambda () ...))

would not work anymore. Under this proposal, the code would need to be changed to something like:

((if b 
     (lambda (f) (thread (f)))
     just-apply) 
 (lambda () ...))

which is significantly more verbose. However, given that most uses are in the form (thread (lambda () ...)), I'd say it's worth it. Alternatively, we could provide a star variant like thread* that do expect a thunk as an input, but the star variant should not be the major mode of operation.

sorawee avatar Dec 23 '19 22:12 sorawee

Just to be clear, this issue is unrelated to the thunk form in racket/function correct?

jackfirth avatar Dec 24 '19 02:12 jackfirth

That's correct!

sorawee avatar Dec 24 '19 03:12 sorawee

There should probably be both a version taking a thunk as well as a combined version. A thunk version is useful for working with functional programming utilities while the macro version is obviously more convenient when you are calling it directly. A function version would need to exist anyway to keep generated code from the macro getting too huge.

slaymaker1907 avatar Feb 08 '20 21:02 slaymaker1907

The difference between operations like these is like:

(call/cc (lambda (k) ...))
(let/cc k ...)

The primary distinguishing factor here is the nesting level, which causes one to be indented twice as much as the other.

If we have a Parendown-style weak opening paren or even introduce a simple macro (let's say let-call), that distinction goes away:

(call/cc /lambda (x) ...)
(let-call k call/cc ...)
(let/cc k ...)

With that approach, the remaining selling point of let/cc would be the sheer character-by-character brevity of its name.

(Edited to add the following, because I pressed the button by accident:)

I do think code golf is important sometimes; I think avoiding code golf can be good for finding other abstraction potential, but the verbosity has a cumulative effect. And in this case specifically, I think operations like let/cc really are nicer to have around in the long term than operations like call/cc. What I want to say, though, is that the code golf is ultimately an important factor here, but there are still some other factors involved that can be addressed by reusable abstractions like Parendown and let-call.

rocketnia avatar Feb 09 '20 16:02 rocketnia

What if we had a definition form of let/cc?

(define foo 42)
(capture-continuation k)
(define bar 5)

...where capture-continuation worked as if the rest of the body was wrapped in a let/cc.

jackfirth avatar Feb 12 '20 02:02 jackfirth

The thunk forms are useful because they will often need to be written anyway to minimize the amount of code generated, in that case I think it makes sense to expose it. You really don't need to go in depth with functional programming to make the thunk forms useful. With the thread example, it is probably a good idea to explicitly write out the function for the thread if it is a complicated thread and thunk forms make that easier.

slaymaker1907 avatar Sep 27 '20 10:09 slaymaker1907