[bug#34948,1/3] records: Allow thunked fields to refer to 'this-record'.

Message ID 20190322172719.11199-1-ludo@gnu.org
State Accepted
Commit abd4d6b33dba4de228e90ad15a8efb456fcf7b6e
Headers show
Series Turn 'essential-services' into an <operating-system> field | expand

Checks

Context Check Description
cbaines/applying patch success Successfully applied
cbaines/applying patch success Successfully applied
cbaines/applying patch success Successfully applied
cbaines/applying patch success Successfully applied
cbaines/applying patch success Successfully applied
cbaines/applying patch success Successfully applied

Commit Message

Ludovic Courtès March 22, 2019, 5:27 p.m. UTC
* guix/records.scm (this-record): New syntax parameter.
(make-syntactic-constructor)[wrap-field-value]: When F is thunked,
return a one-argument lambda instead of a thunk, and parameterize
THIS-RECORD.
(define-record-type*)[thunked-field-accessor-definition]: Pass X
to (real-get X).
* tests/records.scm ("define-record-type* & thunked & this-record")
("define-record-type* & thunked & default & this-record")
("define-record-type* & thunked & inherit & this-record"): New tests.
---
 guix/records.scm  | 24 ++++++++++++++++++++++--
 tests/records.scm | 40 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 62 insertions(+), 2 deletions(-)

Comments

Ricardo Wurmus March 22, 2019, 9:53 p.m. UTC | #1
Ludovic Courtès <ludo@gnu.org> writes:

> * guix/records.scm (this-record): New syntax parameter.
> (make-syntactic-constructor)[wrap-field-value]: When F is thunked,
> return a one-argument lambda instead of a thunk, and parameterize
> THIS-RECORD.

So the value of the thunked field is no longer strictly a thunk?

I’m having difficulties understanding how this works.  Why does the
“thunked field” now require an argument (“x”)?

We use the syntax parameter “this-record” to introduce a new binding
with this name in the context of the “value” of the field.  The
parameter value is … hard to make out.  How does the syntax-case macro
in the following syntax-parameterize expression evaluate to the record
itself?  Would #,x not be sufficient to refer to the argument of the
field accessor?

>           (define (wrap-field-value f value)
>             (cond ((thunked-field? f)
> -                  #`(lambda () #,value))
> +                  #`(lambda (x)
> +                      (syntax-parameterize ((this-record
> +                                             (lambda (s)
> +                                               (syntax-case s ()
> +                                                 (id
> +                                                  (identifier? #'id)
> +                                                  #'x)))))


--
Ricardo
Ludovic Courtès March 23, 2019, 3:18 p.m. UTC | #2
Hi!

Ricardo Wurmus <rekado@elephly.net> skribis:

> Ludovic Courtès <ludo@gnu.org> writes:
>
>> * guix/records.scm (this-record): New syntax parameter.
>> (make-syntactic-constructor)[wrap-field-value]: When F is thunked,
>> return a one-argument lambda instead of a thunk, and parameterize
>> THIS-RECORD.
>
> So the value of the thunked field is no longer strictly a thunk?

Indeed, it’s now a one-argument procedure.  It doesn’t matter much
though because users never see this procedure.

> I’m having difficulties understanding how this works.  Why does the
> “thunked field” now require an argument (“x”)?

This argument is the record itself, then bound to ‘this-record’ in the
lexical scope of the field.

> We use the syntax parameter “this-record” to introduce a new binding
> with this name in the context of the “value” of the field.  The
> parameter value is … hard to make out.  How does the syntax-case macro
> in the following syntax-parameterize expression evaluate to the record
> itself?  Would #,x not be sufficient to refer to the argument of the
> field accessor?
>
>>           (define (wrap-field-value f value)
>>             (cond ((thunked-field? f)
>> -                  #`(lambda () #,value))
>> +                  #`(lambda (x)
>> +                      (syntax-parameterize ((this-record
>> +                                             (lambda (s)
>> +                                               (syntax-case s ()
>> +                                                 (id
>> +                                                  (identifier? #'id)
>> +                                                  #'x)))))

Here ‘x’ is the identifier of a variable that exists at run time.  So we
cannot write #,x because we’d be referring to a variable ‘x’ that exists
at macro-expansion time, and there’s no such variable here.

The ‘syntax-case’ here is just so that ‘this-record’ matches only when
used as an identifier, like this:

  (foo this-record)

… and does not match when used like this:

  (this-record)

or like that:

  (this-record x y z)

We could just as well make it (identifier-syntax #'x) though that’s
slightly less precise.

A macro expansion is worth a thousand words :-), so:

--8<---------------cut here---------------start------------->8---
scheme@(guix records)> (define-record-type* <foo> foo make-foo foo?
			 (bar foo-bar (default 42))
			 (baz foo-baz (thunked)))
scheme@(guix records)> ,optimize (foo-baz x)
$11 = (let ((x x))
  ((if (eq? (struct-vtable x) <foo>)
     (struct-ref x 1)
     (throw 'wrong-type-arg
            '%foo-baz-real
            "Wrong type argument: ~S"
            (list x)
            (list x)))
   x))
scheme@(guix records)> ,optimize (foo (baz (+ 77 (foo-bar this-record))))
$12 = (begin
  (if (eq? #{% <foo> abi-cookie}# 2292347072401235576)
    (if #f #f)
    (throw 'record-abi-mismatch-error
           'abi-check
           "~a: record ABI mismatch; recompilation needed"
           (list <foo>)
           '()))
  (let ((s (allocate-struct <foo> 2)))
    (struct-set! s 0 42)
    (struct-set!
      s
      1
      (lambda (x)
        (+ 77
           (if (eq? (struct-vtable x) <foo>)
             (struct-ref x 0)
             (throw 'wrong-type-arg
                    'foo-bar
                    "Wrong type argument: ~S"
                    (list x)
                    (list x))))))
    s))
--8<---------------cut here---------------end--------------->8---

I hope this clarifies things!

Ludo’.
Ludovic Courtès March 23, 2019, 4:05 p.m. UTC | #3
I should mention that there are other craaaazzy applications of this!

For example, the ‘self-native-input?’ field of <package> becomes
useless, because now you can write:

  (package
    ;; …
    (native-inputs
      ;; Add self as a native input when cross-compiling.
      `(,@(if (%current-target-system)
              `(("this" ,this-record))
              '())
        ;; …
        )))

I think there are other cases in package definitions where this can be
useful, possibly things like the ‘make-lua-*’ procedures that we have.

Ludo’.
Ludovic Courtès March 30, 2019, 10:37 a.m. UTC | #4
Hello!

I’ve extended this a bit with these commits:

  d8bead6c5d system: Define 'this-operating-system'.
  adb6462c4c packages: Define 'this-package' and 'this-origin'.
  d2be7e3c4b records: Support custom 'this' identifiers.

Now you can refer to ‘this-package’ and it will refer to the closest
package in scope.  The good thing is that you can refer to
‘this-package’ from within, say, an <origin> field, and it will DTRT.

That also means you could have things such as:

  (define-record-type* <origin>
    ;; …
    (file-name origin-file-name (thunked)
               (default (string-append (package-name this-package)
                                       "-source"))))

… which is pretty fun when you think about it, since it allows you to
implicitly refer to the lexically surrounding package.

That reminds me of Scala’s “implicit parameters”:

  https://docs.scala-lang.org/tour/implicit-parameters.html

Ludo’.
Ludovic Courtès March 30, 2019, 2:20 p.m. UTC | #5
Ludovic Courtès <ludo@gnu.org> skribis:

> I should mention that there are other craaaazzy applications of this!
>
> For example, the ‘self-native-input?’ field of <package> becomes
> useless, because now you can write:
>
>   (package
>     ;; …
>     (native-inputs
>       ;; Add self as a native input when cross-compiling.
>       `(,@(if (%current-target-system)
>               `(("this" ,this-record))
>               '())
>         ;; …
>         )))

Done in a7646bc5e17a829d23519d0b199a576fb1edbd04!

Ludo'.

Patch

diff --git a/guix/records.scm b/guix/records.scm
index 0649c90ea3..244b124098 100644
--- a/guix/records.scm
+++ b/guix/records.scm
@@ -25,6 +25,8 @@ 
   #:use-module (ice-9 regex)
   #:use-module (ice-9 rdelim)
   #:export (define-record-type*
+            this-record
+
             alist->record
             object->fields
             recutils->alist
@@ -93,6 +95,17 @@  interface\" (ABI) for TYPE is equal to COOKIE."
            (()
             #t)))))))
 
+(define-syntax-parameter this-record
+  (lambda (s)
+    "Return the record being defined.  This macro may only be used in the
+context of the definition of a thunked field."
+    (syntax-case s ()
+      (id
+       (identifier? #'id)
+       (syntax-violation 'this-record
+                         "cannot be used outside of a record instantiation"
+                         #'id)))))
+
 (define-syntax make-syntactic-constructor
   (syntax-rules ()
     "Make the syntactic constructor NAME for TYPE, that calls CTOR, and
@@ -148,7 +161,14 @@  of TYPE matches the expansion-time ABI."
 
          (define (wrap-field-value f value)
            (cond ((thunked-field? f)
-                  #`(lambda () #,value))
+                  #`(lambda (x)
+                      (syntax-parameterize ((this-record
+                                             (lambda (s)
+                                               (syntax-case s ()
+                                                 (id
+                                                  (identifier? #'id)
+                                                  #'x)))))
+                        #,value)))
                  ((delayed-field? f)
                   #`(delay #,value))
                  (else value)))
@@ -308,7 +328,7 @@  inherited."
          (with-syntax ((real-get (wrapped-field-accessor-name field)))
            #'(define-inlinable (get x)
                ;; The real value of that field is a thunk, so call it.
-               ((real-get x)))))))
+               ((real-get x) x))))))
 
     (define (delayed-field-accessor-definition field)
       ;; Return the real accessor for FIELD, which is assumed to be a
diff --git a/tests/records.scm b/tests/records.scm
index d9469a78bd..45614093a0 100644
--- a/tests/records.scm
+++ b/tests/records.scm
@@ -170,6 +170,46 @@ 
            (parameterize ((mark (cons 'a 'b)))
              (eq? (foo-bar y) (mark)))))))
 
+(test-assert "define-record-type* & thunked & this-record"
+  (begin
+    (define-record-type* <foo> foo make-foo
+      foo?
+      (bar foo-bar)
+      (baz foo-baz (thunked)))
+
+    (let ((x (foo (bar 40)
+                  (baz (+ (foo-bar this-record) 2)))))
+      (and (= 40 (foo-bar x))
+           (= 42 (foo-baz x))))))
+
+(test-assert "define-record-type* & thunked & default & this-record"
+  (begin
+    (define-record-type* <foo> foo make-foo
+      foo?
+      (bar foo-bar)
+      (baz foo-baz (thunked)
+           (default (+ (foo-bar this-record) 2))))
+
+    (let ((x (foo (bar 40))))
+      (and (= 40 (foo-bar x))
+           (= 42 (foo-baz x))))))
+
+(test-assert "define-record-type* & thunked & inherit & this-record"
+  (begin
+    (define-record-type* <foo> foo make-foo
+      foo?
+      (bar foo-bar)
+      (baz foo-baz (thunked)
+           (default (+ (foo-bar this-record) 2))))
+
+    (let* ((x (foo (bar 40)))
+           (y (foo (inherit x) (bar -2)))
+           (z (foo (inherit x) (baz -2))))
+      (and (= -2 (foo-bar y))
+           (=  0 (foo-baz y))
+           (= 40 (foo-bar z))
+           (= -2 (foo-baz z))))))
+
 (test-assert "define-record-type* & delayed"
   (begin
     (define-record-type* <foo> foo make-foo