~technomancy/fennel

5 3

Clojure-like loop/recur or Scheme named-let in Fennel?

Details
Message ID
<CA+GZiYozEZ3OqxDrHbP1vAUYVxkCxcwJJUnXy=vP6ANubH9bhg@mail.gmail.com>
DKIM signature
missing
Download raw message
So! A topic I noticed a few times in the IRC channel (once that I
started, even~) was about adding something like Scheme's `let` form
(https://wiki.call-cc.org/man/5/Module%20scheme#iteration), or
Clojure's `loop` + `recur` (https://clojuredocs.org/clojure.core/loop)
into Fennel!

The tl;dr version of the idea is that the `fn` form in Fennel has an
optional name, so both

    (fn [<args>] <body>)

...and...

    (fn <name> [<args>] <body>)

are valid! And in Scheme, the `let` form works this way too! So both

    (let [<args>] <body>)

...and...

    (let <name> [<args>] <body>)

...are also valid! And when you call the "<name>", it starts the form
over again, specifying new bindings for each iteration.

But like, why would you want this, right? Well! It turns out this
combined with Fennel's (Or Clojure's!) extremely good destructuring is
actually super-nice for recursive iteration in extremely controlled
ways, and works generally in a large number of situations where the
current forms are kind of cumbersome. (imo)

(Here's an example of this type of form being used for complex
recursive iteration in Clojure:
https://gist.github.com/Archenoth/19bf137701063d6b0bd42358e92d7125#day-9-rope-bridge)

Some things Fennel currently has (And some limitations!):

- `accumulate` and `faccumulate` only allow for changing one of the
bindings per iteration, and can only advance by one item per iteration
- `fn` with another `fn` inside solves the destructuring and iteration
limitations, but the initial values of their bindings get farther and
farther away from their destructure patterns
- `collect` and `icollect` can only emit collections equal in size, or
smaller than their input iterator

If Fennel had something like a named let, these could be solved like so:

- Changing multiple things at a time per iteration (Also! This is
iterating `numbers` by 2 items at a time!):

    (let loop [[first second & rest] numbers
               sum 0
               pairs []]
      (if (nil? second)
        {: sum : pairs}
        (loop rest
             (+ sum first second)
             [(table.unpack pairs) [first second]])))


- Distance from the destructured initial values:

    (let loop [[{: name : score} & rest] (get-players *world*)]
      <50 lines of code here>)

vs:

    ((fn loop [[{: name : score} & rest]]
       <50 lines of code here>)
    (get-players *world*))

...or if you wanted the data next to where it gets destructured so you
could see them at a glance...

    (let [players (get-players *world*)]
      (fn loop [[{: name : score} & rest]]
        <50 lines of code here>)
      (loop players))


- Making collections that are bigger than their iterators:

    (let loop [[first & rest] numbers
               acc []]
      (if (nil? first)
        acc
        (loop rest [(table.unpack acc) first (* 2 first)])))


This form also has the benefit of being able to decouple a function's
arity with its recursion logic. For example, here's a function that
counts the number of times each character appears in a string:

    (fn word-freqs [str]
      (let loop [[word & rest] (icollect [k _ (string.gmatch str "%a+")] k)
                 acc {}]
        (if (nil? word)
          acc
          (let [count (+ 1 (or (. acc word) 0))]
            (loop rest (doto acc (tset word count)))))))

Notice that the function only needs a string passed in, and, and the
loop itself has names that only relate to the recursion logic? This is
kinda like how `(let [] ...)` is already used at the start of
functions, so it's pretty convenient!

tl;dr - it's an extremely general iteration form that can conveniently
allow us to cleanly do quite a few things that currently are actually
kinda tricky in Fennel, and make some things that are already possible
require less mental context to write (imo, of course! Very similarly
to how things like `->`, `->>`, and `doto` affect code readability)

Also, also! If `let` is different enough from `fn` in Fennel that we
don't want to reuse the name, the way that Clojure does it is that it
gives you a `(loop [] ...)` form, which acts exactly like `(let []
...)`, except, inside, you can call `(recur ...)` to start the loop
over with the new bindings.

Whaddaya think? Does something like this feel like it would be a good
fit for Fennel itself..? Or is this more the domain of third-party
libraries?
Details
Message ID
<A6E9926F-FC21-4BF1-BE5C-652AEDC428F3@gmail.com>
In-Reply-To
<CA+GZiYozEZ3OqxDrHbP1vAUYVxkCxcwJJUnXy=vP6ANubH9bhg@mail.gmail.com> (view parent)
DKIM signature
missing
Download raw message
I have such macro as part of cljlib. It has a bit of trickery and unfortunate
outcome of each destructuring happening twice because of the sequential
binding and the absence of goto labels in Fennel. With all of that in mind, 
you can notice that such macro won't be able to support multiple-value
destructuring at all.

See: https://gitlab.com/andreyorst/fennel-cljlib/-/blob/master/doc/macros.md#limitations

Fennel/Lua already has TCO, so it's not that big of a deal to write an
anonymous function and immediately call it (IIFE).
-- 
Andrey Listopadov
Details
Message ID
<CA+GZiYpvEh1_jmK8-wk-sT3Rct0S4MS2fL2jsfv09yunoyBRpQ@mail.gmail.com>
In-Reply-To
<A6E9926F-FC21-4BF1-BE5C-652AEDC428F3@gmail.com> (view parent)
DKIM signature
missing
Download raw message
Oh yeah! (I was the one who wrote it! Ahah)
I remember seeing these limitations the last time I referenced the
library to talk about this in #fennel~

I wonder if there's a better way to implement it? (Or maybe even a
better place?)
(Actually, where would I even need to look if there was?)
Details
Message ID
<57B88117-BCD0-4A5B-8BE9-C17437982B5B@gmail.com>
In-Reply-To
<CA+GZiYpvEh1_jmK8-wk-sT3Rct0S4MS2fL2jsfv09yunoyBRpQ@mail.gmail.com> (view parent)
DKIM signature
missing
Download raw message
>Oh yeah! (I was the one who wrote it! Ahah)

Oh, haha, you're right!

>I remember seeing these limitations the last time I referenced the
>library to talk about this in #fennel~
>
>I wonder if there's a better way to implement it? (Or maybe even a
>better place?)
>(Actually, where would I even need to look if there was?)

Well, IIRC I was sitting and thinking of a different way, and the only one that
comes to mind is literally using GOTO labels. This will turn named let into a
simple jump + rebind, and it will probably be possible to support
multiple-value binding. Probably. (Un)fortunately, lua escape hatch doesn't
allow defining goto labels.


-- 
Andrey Listopadov
Details
Message ID
<CA+GZiYpuZ4B12epmy+beZr=NMJPkC2iiRjCoxkwfCMADQpTY_w@mail.gmail.com>
In-Reply-To
<57B88117-BCD0-4A5B-8BE9-C17437982B5B@gmail.com> (view parent)
DKIM signature
missing
Download raw message
So uh, it seems like I have No Idea how to send patches to a mailing
list while also having them be a part of the main thread, but I've
sent a patch to https://lists.sr.ht/~technomancy/fennel/patches/41464
that should basically do all of those things, and should be usable to
get around the limitations of the implementation in cljlib without the
need for GOTO labels

(Speaking of which, sorry about the double post! I ran `make test` to
make sure that I didn't break anything with my patch, but didn't
notice that there was also a linter that would break the build)

I tried pretty hard to not affect any existing code, and to make it
have as minimal a footprint as possible too, so hopefully it doesn't
seem too wild of a proposed patch

On Sat, May 27, 2023 at 1:38 AM Andrey <andreyorst@gmail.com> wrote:
>
>
> >Oh yeah! (I was the one who wrote it! Ahah)
>
> Oh, haha, you're right!
>
> >I remember seeing these limitations the last time I referenced the
> >library to talk about this in #fennel~
> >
> >I wonder if there's a better way to implement it? (Or maybe even a
> >better place?)
> >(Actually, where would I even need to look if there was?)
>
> Well, IIRC I was sitting and thinking of a different way, and the only one that
> comes to mind is literally using GOTO labels. This will turn named let into a
> simple jump + rebind, and it will probably be possible to support
> multiple-value binding. Probably. (Un)fortunately, lua escape hatch doesn't
> allow defining goto labels.
>
>
> --
> Andrey Listopadov
Details
Message ID
<87y1l76rap.fsf@hagelb.org>
In-Reply-To
<CA+GZiYozEZ3OqxDrHbP1vAUYVxkCxcwJJUnXy=vP6ANubH9bhg@mail.gmail.com> (view parent)
DKIM signature
missing
Download raw message
Hey, thanks for submitting this!

While I don't have time to get into a detailed discussion due to the
ongoing game jam, I will definitely come back to this. I think we should
talk thru the trade-offs.

But one thing I feel pretty strongly about is that Scheme's approach of
overloading the `let` form to mean this in addition to its normal
behavior is a mistake, and we need to find another way to express this
which makes different things look different.

-Phil
Reply to thread Export thread (mbox)