Are temporary parameters a silly idea?

Hi, Racket Discourse.

I am experimenting with ways of exposing mutable variables to programmers within the body of procedures which themselves build-up objects (by which I am implying, that it is difficult to percolate the two sets of results up the nested calls without introducing a lot of machinery just for that).

One idea I had, was to use temporary parameters; surprisingly, I did not realize (although it is obvious in hindsight) one could create and parameterize parameters on the fly.

Take this macro, for example:

(define-syntax (let-parameters stx)
  (syntax-parse stx
    [(_ ([name:id init:expr] ...) body:expr ...)
     #'(let ([name (make-parameter #false)] ...)
         (parameterize ([name init] ...)
           body ...))]))

Which then might be used something like this:

(define page
  '(html
    (body (p ([class "baz"])
             "foo" "bat")
          (p "bar" (br) "qux"))

    (br)

    (body (p ([class "zab"]
              [id    "1"])
             "foo" "tab")
          (p "rab" (br) "xuq"))))

(let-parameters ([tag-indexes (hash)])
  (values
   (with-xe '(html body . p)
     (lambda (tag p-body)
       (with-parameters (tag-indexes)
         (define index (hash-count tag-indexes))
         (hash-set tag-indexes index tag))
       (println (cons tag p-body))
       p-body)
     page)
   {tag-indexes}))

; '(p "foo" "bat")
; '(p "bar" (br) "qux")
; '(p "foo" "tab")
; '(p "rab" (br) "xuq")
;=> '(html (body (p ((class "baz")) "foo" "bat") (p "bar" (br) "qux")) (br) (body (p ((class "zab") (id "1")) "foo" "tab") (p "rab" (br) "xuq")))
;=> '#hash((0 . p) (1 . p) (2 . p) (3 . p))

The with-parameters basically desugars to:

{parameter ... {parameter} ...}

assigning the results of the body to the parameters without needing to unwrap them.

Now, this is very cute, but is this silly?

Technically, all I am doing is avoiding the set!--which could be wrapped in similar machinery for the same effect. Moreover, this allows "spooky action at a distance", since if the parameters escape the immediate body of the bindings, they could be used to change the "locally global" values. Which could be cool, but also terrible.

Have you ever used temporary parameters before?

I don’t think parameters are really needed if all you want is a … well, mutable variable, hidden behind the (case-> (-> R) (R -> Void)) interface. At least the example looks like it really wants a simple mutable variable as opposed to parameter. Parameters are useful when you want to dynamically (in the sense of “dynamic extent/context”) change their values (they are like “dynamic bindings” in Common Lisp, in this respect), but the example doesn’t rely on this dynamic behavior at all (no significant use of parameterize). It’s very difficult to imagine how “temporary parameters” can be useful—if you just want to communicate values to a function, you can always pass them as arguments! (Or use boxes, if you love states :P) So I think I’ve personally never used parameters this way.

2 Likes

Indeed, it is entirely overkill.

Thank you for mentioning the case-> contract mechanism. It is the second time in a very short amount of time I have seen the identifier, which is always an interesting phenomenon.

I was flirting with the idea of exposing the XML-element (xexpr) attributes as such a temporary parameter, since it is kind of a parameterization of the element's tag, and it would allow one to hold on to parameterizations nestedly, unlike using a current-attributes parameter which would hide shallower attributes along the path. But ultimately, it doesn't make sense because it can be achieved more easily otherwise.

I am glad I am aware of the idea now, though.

As you rightly observe, I am currently playing around with just using the lambdas as looping mechanisms, kind of like one would with (let loop ...), with the optional arguments functioning as looping variables.

It isn't bad, but it feels brittle enough that I suspect there is a more elegant underlying principle yet to appear. Maybe what is necessary, is some form of #:result argument, like in a for/fold form. Time will tell.

(define page
  '(html
    (body (p (p (a 3 2 1) (b 5)   (c 3))
             (p (d 3 2 1) (e 3 1) (f 3))))
    
    (body (p (p (a 3)     (b 5)   (c 3))
             (p (d 1 2 3) (e)     (f 3))))))

(define-values (_ counts contents)
  (with-xe '(html body p p)
    (lambda (tag attrs p.* [counts null] [contents* null])
      (define-values (p.*′ count contents)
        (with-xe '(*)
          (lambda (tag attrs p.*.* [count 0] [content null])
            (values
             attrs
             p.*.*
             (+ count (if (not p.*.*) 0 1))
             (if (not p.*.*) content (cons p.*.* content))))
          p.*))
      (values attrs p.*′ (cons count counts) (cons (reverse contents) contents*)))
    page))

(match-define
  `((,html.body.p_0.p_0-counts
     ,html.body.p_0.p_1-counts)
    
    (,html.body.p_1.p_0-counts
     ,html.body.p_1.p_1-counts))
  counts)

html.body.p_0.p_0-counts
html.body.p_0.p_1-counts

html.body.p_1.p_0-counts
html.body.p_1.p_1-counts

#|
;=>
'(3 1 1)
'(3 2 1)
'(1 1 1)
'(3 0 1)
|#

(match-define
  `((,html.body.p_0.p_0-contents
     ,html.body.p_0.p_1-contents)

    (,html.body.p_1.p_0-contents
     ,html.body.p_1.p_1-contents))
  contents)

html.body.p_0.p_0-contents
html.body.p_0.p_1-contents

html.body.p_1.p_0-contents
html.body.p_1.p_1-contents

#|
'((3 2 1) (5) (3))
'((3 2 1) (3 1) (3))
'((3) (5) (3))
'((1 2 3) () (3))
|#

Edit: Thinking about it some more, maybe the shadowed parameters aren't a bad compromise, if it simplifies the function signatures.

Then, using a final #false result which is passed to the app procedure among the with-xe procedure's arguments, one can signal to the programmer that the results have been exhausted, providing a space to do clean-up of the accumulator arguments.

(with-xe '(p p *)
  (lambda (p.p.*.* [count 0])
    (if (not p.p.*.*)
        (values
         p.p.*.* (format "counted ~a things in a ~a-tag" count {<xel>}))
        (values
         p.p.*.* (+ count 1))))
  '(p
    (p
     (p ([class "upper"])
        (g "G for short")
        (g "Gee, normally"))
     (p ([class "lower"])
        (h "h for short")
        (g "... uh")
        (g "nevermind"))
     (br)
     (div))))

#|
'(p (p (p ((class "upper")) (g "G for short") (g "Gee, normally")) (p ((class "lower")) (h "h for short") (g "... uh") (g "nevermind")) (br) (div)))

'(("counted 2 things in a p-tag"
   "counted 3 things in a p-tag"
   "counted 0 things in a br-tag"
   "counted 0 things in a div-tag"))
|#

Programming is awesome.

Now just to come up with a way of flattening them nicely, or, accessing the nested results more seamlessly.

P.S. If anyone has not yet tried the debugger in DrRacket, I would recommend giving it a go. I did for the first time today, and it helped me sus out a bug when coalescing the values of the accumulator arguments in the code above.