tilda - a threading macro full of itself

90 views
Skip to first unread message

zeRusski

unread,
May 7, 2019, 9:39:24 AM5/7/19
to Racket Users
I asked in a separate thread how one debugs set of scopes in Racket macros. I appreciate the question may read a bit hand-wavy and abstract, so I split out the piece of code I had in mind into a separate package so that interested Racketeers could have a look and their time permitting maybe even suggest improvements.

Tilda is nothing but an opinionated threading macro. Even though "threading" macros punctuate all of my Clojure code I've been mostly making do without them in Racket. I was just being lazy tbh. I finally caved but instead of using off the shelf implementation I turned this into a nice exercise in macrology.


README shows off its capabilities with silly but hopefully sufficient examples.

If you have 5-10min on your hands and would like to help me learn, I'd appreciate PRs that improve or critique my code. Obviously, feel free to write here, but Github may provide a nicer interface to discuss code.

Also, see if you can break it:
- break scoping rules,
- hit an error that doesn't point to correct location with errortrace on etc.

Thanks


Matthias Felleisen

unread,
May 7, 2019, 10:16:39 AM5/7/19
to zeRusski, Racket Users
Let me point out that if ~> specified an identifier (as suggested in my first response) you could co-mingle two threaded computations, keeping separate concerns that don’t matter as opposed to bundling them up in structs or lists or whatever you have to do if you have only one. At first I thought #:as would you let you do so, but that’s not correct. It just names the threaded value. Did I overlook anything?

— Matthias

zeRusski

unread,
May 7, 2019, 1:29:41 PM5/7/19
to Racket Users
It just names the threaded value. Did I overlook anything?

That's right, nothing fancy. Think let-binding the threaded value at that point. #:with id ~ would achieve the same thing, so as it is now #:as is redundant. With #:do both #:with and #:as are redundant, really.
 
Let me point out that if ~> specified an identifier (as suggested in my first response) you could co-mingle two threaded computations, keeping separate concerns that don’t matter as opposed to bundling them up in structs or lists or whatever you have to do if you have only one. At first I thought #:as would you let you do so, but that’s not correct.

Ok, this one I don't quite understand. My first thought went as far as to weave multiple computations where each #:as would capture continuations and macro would keep these "threads" separate, but now I'm thinking you mean this:

(~> 1 #:as ~a
    ;; now ~a is being threaded
    (add1 ~a)                           ;=> 2
    2 #:as ~b
    ;; now ~b is being threaded
    (add1 ~b)                           ;=> 3
    ;; simply use whatever ~a was last
    (+ ~a ~b)                           ;=> 5
    #:as ~a
    ;; continue to thread ~a
    (add1 ~a)                           ;=> 3
    (list ~a ~b))
;; => (list 3 5)

but then I think you can achieve this with current #:as semantics since a clause without a hole simply starts computation from that clause yet preserves any #:as bound vars in scope, so I'm guessing I'm way off. Could you expand please? 

zeRusski

unread,
May 7, 2019, 1:29:43 PM5/7/19
to Racket Users
It just names the threaded value. Did I overlook anything?

That's right, nothing fancy. Think let-binding the threaded value at that point. #:with id ~ would achieve the same thing, so as it is now #:as is redundant. With #:do both #:with and #:as are redundant, really.
 
Let me point out that if ~> specified an identifier (as suggested in my first response) you could co-mingle two threaded computations, keeping separate concerns that don’t matter as opposed to bundling them up in structs or lists or whatever you have to do if you have only one. At first I thought #:as would you let you do so, but that’s not correct.

Lehi Toskin

unread,
May 7, 2019, 1:33:38 PM5/7/19
to Racket Users
I like this. Reminds me of `rackjure/threading`, but more involved.

Greg Hendershott

unread,
May 7, 2019, 2:00:14 PM5/7/19
to Racket Users

> I like this. Reminds me of `rackjure/threading`, but more involved.

A few years ago, after Alexis released

https://github.com/lexi-lambda/threading

I updated rackjure to re-provide that.

Speaking of which, a couple issues there might be interesting for you,
Vlad.

For example, it's better if a threading macro expands using the `#%app`
bound at the macro use site. (Whereas by default, macros expand using
identifiers bound where the macro is defined.)

https://github.com/lexi-lambda/threading/issues/3

Also you might want to treat `quote` forms specially, and not "thread
into" them.

https://github.com/lexi-lambda/threading/issues/2

Matthias Felleisen

unread,
May 7, 2019, 8:13:03 PM5/7/19
to zeRusski, Racket Users


On May 7, 2019, at 1:29 PM, zeRusski <vladile...@gmail.com> wrote:

It just names the threaded value. Did I overlook anything?

That's right, nothing fancy. Think let-binding the threaded value at that point. #:with id ~ would achieve the same thing, so as it is now #:as is redundant. With #:do both #:with and #:as are redundant, really.
 
Let me point out that if ~> specified an identifier (as suggested in my first response) you could co-mingle two threaded computations, keeping separate concerns that don’t matter as opposed to bundling them up in structs or lists or whatever you have to do if you have only one. At first I thought #:as would you let you do so, but that’s not correct.

Ok, this one I don't quite understand. My first thought went as far as to weave multiple computations where each #:as would capture continuations and macro would keep these "threads" separate, but now I'm thinking you mean this:

(~> 1 #:as ~a
    ;; now ~a is being threaded
    (add1 ~a)                           ;=> 2
    2 #:as ~b
    ;; now ~b is being threaded
    (add1 ~b)                           ;=> 3
    ;; simply use whatever ~a was last
    (+ ~a ~b)                           ;=> 5
    #:as ~a
    ;; continue to thread ~a
    (add1 ~a)                           ;=> 3
    (list ~a ~b))
;; => (list 3 5)

Think: 

(~> (x 0)
      (add1 x) 
      (sin 
            (~> (y  1)
                  (sub1 y] 
                  (+ x y)))
      (* pi x))

This looks equally simple and is equally easy to read. If you wish to emphasize the “hole variable” make sure its name has a certain shape, say ~x. 
But now the threading has precise syntactic boundaries, and the implementation gets away without any assignments (which I assume #:as uses). 

— Matthias

zeRusski

unread,
May 12, 2019, 8:24:53 AM5/12/19
to Racket Users

For example, it's better if a threading macro expands using the `#%app`
bound at the macro use site. (Whereas by default, macros expand using
identifiers bound where the macro is defined.)

  https://github.com/lexi-lambda/threading/issues/3


Got this one right in the ~> macro, but not in define~>, nor in lambda~>. Thanks Greg, you totally nailed a bug ))
 
Also you might want to treat `quote` forms specially, and not "thread
into" them.

  https://github.com/lexi-lambda/threading/issues/2

This one is a non-issue since the macro disallows naked expressions, that is every clause is expected to be consistently wrapped in parens, and hole-markers either need to appear in the clause or the clause "re-strarts" the threading e.g. (~> ht 'x) is the same as (~> ht (quote x)) => 'x, since (quote x) has no marker, ht gets thrown out.

If you really want Clojure style hash-table accessor a-la keywords, assuming you have appropriate #%app in place, you'd write: (~> ht ('x ~)), that is you have to be explicit with your hole-markers.
Reply all
Reply to author
Forward
0 new messages