Skip to content

Commit 5f7d201

Browse files
committed
New iterating macros & starred forms of things
Also remove dependencies from utilities which is good thing metatronic exports some functionality to help you write your own more complex macros (and uses it internally)
1 parent 27fd40e commit 5f7d201

8 files changed

+495
-70
lines changed

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright 1989-2022 Tim Bradshaw
1+
Copyright 1989-2023 Tim Bradshaw
22

33
Permission is hereby granted, free of charge, to any person obtaining
44
a copy of this software and associated documentation files (the

README.md

+211-8
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,17 @@ I've always liked Scheme's named-`let` construct. It's pretty easy to provide a
395395

396396
Well, that's what it used to do: for a while I simply set the flag which controls whether it thinks an implementation supports tail-call elimination unilaterally to true, which means it will always create correct code, even if that code may cause stack overflows on implementations which don't eliminate tail calls[^6]. From 21st August 2021 the old code is now gone altogether (it is still available for inspection in old commits).
397397

398-
For a very long time I was confused about variable binding in `iterate`: I thought it was like `let*`, not like `let` although I'm not sure why. In the previous version of this code there was even an `iterate*` macro which claimed to be like `let*` and an `iterate` which claimed to be like `let`. But that was all just confusion, so `iterate*` is gone again.
398+
From March 2023 there are now four variants on this macro which provide various options of evaluation order and stepping.
399+
400+
**`iterate`** is the original macro: this is just like named-`let` in Scheme. In particular it binds in parallel. So
401+
402+
```lisp
403+
(let ((x 1))
404+
(iterate n ((x 2) (y x))
405+
(values x y)))
406+
```
407+
408+
evaluates to `2` and `1`.
399409

400410
```lisp
401411
(iterate foo ((x 1)
@@ -405,7 +415,7 @@ For a very long time I was confused about variable binding in `iterate`: I thoug
405415
...)
406416
```
407417

408-
turns into
418+
simply turns into
409419

410420
```lisp
411421
(labels ((foo (x y)
@@ -415,7 +425,150 @@ turns into
415425
(foo 1 2))
416426
```
417427

418-
Combined with `collecting`, `iterate` provides a surprisingly pleasant minimalist framework for walking over data structures in my experience.
428+
**`iterate*`** is a variation of `iterate` which binds sequentially for the initial binding:
429+
430+
```lisp
431+
(let ((x 1))
432+
(iterate* n ((x 2) (y x))
433+
(values x y)))
434+
```
435+
436+
will evaluate to `2` and `2` (and the outer binding of `x` is now unused). Recursive calls are just function calls and evaluate their arguments as you would expect in both cases.
437+
438+
Additionally there are now two fancier macros: `iterating` and `iterating*`. Both of these support a variation of `do`-style stepping arguments: a binding like `(v init)` will bind `v` to `init` and then, by default, step it also to `init`. A binding like `(v init step)` will bind to `init` and then step it to `step`. Note that this is *not the same* as `do`: for `do` `(v init)` will bind `v` to `init` and then to whatever its current value is. To achieve this with `iterating` you need to say `(v init v)`. See below for some rationale.
439+
440+
The local function now also takes keyword arguments with values which default to the stepping expressions. So:
441+
442+
```lisp
443+
(iterating n ((n 0 (1+ n)))
444+
(if (= n 10)
445+
n
446+
(n)))
447+
```
448+
449+
Does what you think. But you can also provide arguments to the local function:
450+
451+
```lisp
452+
(iterating n ((o t) (i 0 (1+ i)))
453+
(print o)
454+
(when (< i 10)
455+
(n :o (if (evenp i) t nil))))
456+
```
457+
458+
will print a succession of `t`s and `nil`s, for instance, `i` gets stepped automatically.
459+
460+
**`iterating`** is like `let` / `do`: all the binding that happens is in parallel. So in particular, as with `iterate`:
461+
462+
```lisp
463+
(let ((x 1))
464+
(iterating n ((x 2) (y x))
465+
(values x y)))
466+
```
467+
468+
evaluates to `2` and `1`. Similarly variable references in step forms are to the previous value of the variable:
469+
470+
```lisp
471+
(iterating n ((i 2 (1+ i)) (j 1 i))
472+
(when (< i 10)
473+
(format t "~&~D ~D~%" i j)
474+
(n)))
475+
```
476+
477+
will print `2 1` then `3 2` and so on.
478+
479+
**`iterating*`** is like `let*` / `do*`: all the binding that happens is sequential. So in particular as with `iterate*`:
480+
481+
```lisp
482+
(let ((x 1))
483+
(iterating* n ((x 2) (y x))
484+
(values x y)))
485+
```
486+
487+
evaluates to `2`and `2` and the outer binding is unused. Also, the step forms now refer to the new values of variables to their left:
488+
489+
```lisp
490+
(iterating* n ((i 2 (1+ i)) (j i i))
491+
(when (< i 10)
492+
(format t "~&~D ~D~%" i j)
493+
(n)))
494+
```
495+
496+
prints `2 2`, `3 3` and so on. This also applies to the optional arguments to the local function:
497+
498+
```lisp
499+
> (iterating* n ((i 2 (1+ i)) (j i i))
500+
(when (< i 10)
501+
(format t "~&~D ~D~%" i j)
502+
(if (evenp i)
503+
(n (+ i 3))
504+
(n :i (+ i 1)))))
505+
2 2
506+
5 5
507+
6 6
508+
9 9
509+
```
510+
511+
Combined with `collecting`, `iterate` provides a surprisingly pleasant minimalist framework for walking over data structures in my experience, and i have use it extensively. `iterate*` , `iterating` and `iterating*` are much newer and may be slightly experimental.
512+
513+
### An example of `iterating`
514+
Here is a simple sieve of Eratosthones:
515+
516+
```lisp
517+
(defun sieve (n)
518+
(declare (type (integer 2) n))
519+
(let ((l (isqrt n))
520+
(a (make-array (+ n 1) :element-type 'bit :initial-element 1)))
521+
(declare (type (integer 1) l)
522+
(type simple-bit-vector a))
523+
(iterating* next ((c 2 (1+ c))
524+
(marking (<= c l))
525+
(primes '() primes))
526+
(if (<= c n)
527+
(if (zerop (bit a c))
528+
;; not a prime
529+
(next)
530+
;; A prime
531+
(if marking
532+
(do ((m (* c c) (+ m c)))
533+
((> m n) (next :primes (cons c primes)))
534+
(setf (bit a m) 0))
535+
(next :primes (cons c primes))))
536+
(nreverse primes)))))
537+
```
538+
539+
### Notes
540+
The init and step forms for `iterating` and `iterating*` have different semantics than for `do` and `do*`, but the same semantics as for `doing` from my `simple-loops` hack. I am not sure that this is better -- simply being different is a bad thing -- but they work they way they do because I've ended up writing too many things which look like
541+
542+
```lisp
543+
(do ((var <huge form> <same huge form>))
544+
(...)
545+
...)
546+
```
547+
548+
which using `iterating` would be
549+
550+
```lisp
551+
(iterating next ((var <huge form>))
552+
...
553+
(next))
554+
```
555+
556+
This is one of the reasons that `iterating` is slightly experimental: I am not sure it's right and I won't know until I have used it more.
557+
558+
`iterating` and `iterating*` need to allow keyword arguments for what appears to be the local recursive function. Keyword argument parsing is clearly slightly hairy in general, so what they do is expand to something like this (this is the expansion for `iterating`:
559+
560+
```lisp
561+
(labels ((#:n (x)
562+
(flet ((n (&key ((:x #:x) 2))
563+
(#:n #:x)))
564+
(declare (inline n))
565+
...)))
566+
(#:n 2))
567+
```
568+
569+
where all the gensyms that look the same are the same. Both functions could have had the same name of course, but that seemed gratuitous, especially for anyone reading the macroexpansion. The hope is that by using the little ancillary function, which is inlined, the keyword argument parsing will be faster. However `iterating` & `iterating*` are meant to be expressive at the cost of perhaps being slow in some cases.
570+
571+
`iterating` & `iterating*` went through a brief larval stage where they used optional arguments, but keyword arguments are so much more compelling we decided the possible performance cost was worth paying.
419572

420573
### Package, module
421574
`iterate` lives in `org.tfeb.hax.iterate` and provides `:org.tfeb.hax.iterate`.
@@ -1736,6 +1889,8 @@ where, in this case, all the `#:<in>` symbols are the same symbol.
17361889

17371890
**`*default-metatronize-symbol-rewriter*`** is bound to the default symbol rewriter used by `metatronize`. Changing it will change the behaviour of `metatronize` and therefore of `defmacro/m` and `macrolet/m`. Reloading `metatronic` will reset it if you break things.
17381891

1892+
**`rewrite-sources`** and **`rewrite-targets`** return a list of sources and targets from the rewrite table returned by `metatronize`.
1893+
17391894
### Notes
17401895
Macros written with `defmacro/m` and `macrolet/m` in fact metatronize symbols *twice*: once when the macro is defined, and then again when it is expanded, using lists of rewritten & unique symbols from the first metatronization to drive a `rewriter` function. This ensures that each expansion has a unique set of gensymized symbols: with the above definition of `with-file-lines`, then
17411896

@@ -1753,10 +1908,56 @@ One consequence of this double-metatronization is that you should not use metatr
17531908

17541909
`metatronize` is *not a code walker*: it just blindly replaces some symbols with gensymized versions of them. Metatronic macros are typically easier to make more hygienic than they would otherwise be but they are very far from being hygienic macros.
17551910

1756-
The tables used by`metatronize` are currently alists, which will limit its performance on vast structure. They may not always be, but they probably will be since macro definitions are not usually vast. Do not rely on them being alists.
1911+
The tables used by`metatronize` are currently alists, which will limit its performance on vast structure. They may not always be, but they probably will be since macro definitions are not usually vast. Do not rely on them being alists, and in particular use `rewrite-sources` and `rewrite-targets` on the rewrite table.
17571912

17581913
`metatronize` does deal with sharing and circularity in list structure properly (but only in list structure). Objects which are not lists and not metatronic symbols are not copied of course, so if they were previously the same they still will be in the copy.
17591914

1915+
### Writing more complicated macros
1916+
All `defmacro/m` does is use `metatronize` to walk over the forms in the body of the macro, rewriting symbols appropriately. The expansion of the `with-file-lines` macro above is
1917+
1918+
```lisp
1919+
(defmacro with-file-lines ((line file) &body forms)
1920+
(m2 (progn
1921+
`(with-open-file (#:<in> ,file)
1922+
(do ((,line (read-line #:<in> nil #:<in>)
1923+
(read-line #:<in> nil #:<in>)))
1924+
((eq ,line #:<in>))
1925+
,@forms)))
1926+
'(#:<in>)
1927+
'nil))
1928+
```
1929+
1930+
where `m2` is an internal function which knows how to rewrite specific symbols: this is done so that even if you look at the expansion you can't extract the gensyms.
1931+
1932+
This is fine for macros like that, but there's a common style of macro which looks like this:
1933+
1934+
```lisp
1935+
(defmacro iterate (name bindings &body body)
1936+
(expand-iterate name bindings body nil))
1937+
```
1938+
1939+
Where`expand-iterate` is probably some function which expands variations on `iterate`[^19]. Well, the answer is that it's complicated. But where, in a function like `expand-iterate`, you have code like:
1940+
1941+
```lisp
1942+
(let ((secret-name (make-symbol ...)))
1943+
...
1944+
`(labels ((,secret-name ...))
1945+
(,secret-name ...)))
1946+
```
1947+
1948+
You can replace this with
1949+
1950+
```lisp
1951+
(values
1952+
(metatronize
1953+
`(labels ((<secret-name> ...))
1954+
(<secret-name> ...))))
1955+
```
1956+
1957+
for instance. Here `values` is just suppressing the other values from `metatronize` which you don't need in this case.
1958+
1959+
However in general metatronic macros are far more useful for simple macros where there is no complicated expander function like this: that's what it was intended for.
1960+
17601961
### Package, module
17611962
`metatronic` lives in and provides `:org.tfeb.hax.metatronic`.
17621963

@@ -1904,7 +2105,7 @@ If you want to have fine-grained control over log entry formats then two possibl
19042105
- you could make the value of `*log-entry-formatter*` be a generic function which can then dispatch on its second argument to return an appropriate log format;
19052106
- and/or you could define methods on `slog-to` for suitable `log-entry` subclasses which can select entry formats appropriately.
19062107

1907-
The second approach allows you, for instance, to select locale-specific formats by passing keyword arguments to specify non-default locales to `slog-to`, rather than just relying on its class alone[^19].
2108+
The second approach allows you, for instance, to select locale-specific formats by passing keyword arguments to specify non-default locales to `slog-to`, rather than just relying on its class alone[^20].
19082109

19092110
### The `logging` macro
19102111
**`(logging ([(typespec destination ...) ...]) form ...)`** establishes dynamic handlers for log entries which will log to the values of the specified destinations. Each `typespec` is as for `handler-bind`, except that the type `t` is rewritten as `log-entry`, which makes things easier to write. Any type mentioned in `typespec` must be a subtype of `log-entry`. The value of each destination is then found, with special handling for pathnames (see below) and these values are used as the destinations for calls to `slog-to`. As an example the expansion of the following form:
@@ -1969,7 +2170,7 @@ One important reason for this behaviour of `logging` is to deal with this proble
19692170

19702171
Because the absolute pathname of `foo.log`is computed and stored at the point of the logging macro you won't end up logging to multiple files: all the log entries will go to whatever the canonical version of `foo.log` was at the point that `logging` appeared.
19712172

1972-
Finally note that calls to `slog` are completely legal but will do nothing outside the dynamic extent of a `logging` macro[^20], but `slog-to` will work quite happily and will write log entries. This includes when given pathname arguments: it is perfectly legal to write code which just calls `(slog-to "/my/file.log" "a message")`. See below on file handling.
2173+
Finally note that calls to `slog` are completely legal but will do nothing outside the dynamic extent of a `logging` macro[^21], but `slog-to` will work quite happily and will write log entries. This includes when given pathname arguments: it is perfectly legal to write code which just calls `(slog-to "/my/file.log" "a message")`. See below on file handling.
19732174

19742175
Note that destinations which correspond to files (pathnames and strings) are opened by `logging`, with any needed directories being created and so on. This means that if those files *can't* be opened then you'll get an error immediately, rather than on the first call to `slog`.
19752176

@@ -2211,6 +2412,8 @@ The TFEB.ORG Lisp hax are copyright 1989-2022 Tim Bradshaw. See `LICENSE` for t
22112412

22122413
[^18]: So `(apply #'eq (metatronize '(<x> <x>)))` is true but `(apply #'eq (metatronize '(<> <>)))` is false.
22132414

2214-
[^19]: An interim version of `slog` had a generic function, `log-entry-formatter` which was involved in this process with the aim of being able to select formats more flexibly, but it did not in fact add any useful flexibility.
2415+
[^19]: This is in fact how `iterate` and `iterate*` work now, with `iterating` and `iterating*` sharing another expansion function.
2416+
2417+
[^20]: An interim version of `slog` had a generic function, `log-entry-formatter` which was involved in this process with the aim of being able to select formats more flexibly, but it did not in fact add any useful flexibility.
22152418

2216-
[^20]: Well: you could write your own `handler-bind` / `handler-case` forms, but don't do that.
2419+
[^21]: Well: you could write your own `handler-bind` / `handler-case` forms, but don't do that.

VERSION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
7.2.0
1+
8.0.0

0 commit comments

Comments
 (0)