unofficial mirror of guile-user@gnu.org 
 help / color / mirror / Atom feed
* Contracts macro example
@ 2022-07-14 23:55 Zelphir Kaltstahl
  2022-07-19 15:20 ` Maxime Devos
  0 siblings, 1 reply; 6+ messages in thread
From: Zelphir Kaltstahl @ 2022-07-14 23:55 UTC (permalink / raw)
  To: Guile User

Hello Guile users!

I've tried myself again at writing some macros. Some post about programming 
language features inspired me to try and write a macro, which adds contracts (or 
my naive idea of what they are) to function definitions.

The code is at: 
https://notabug.org/ZelphirKaltstahl/guile-examples/src/d749de48307cebe279215ab5df50853c9d100b2f/macros/contract.scm.

Or here, so that this e-mail can stand on its own:

~~~~
;; Suppose we wanted to check assumptions about arguments to
;; our function. What kind of form could we write to express
;; this?

;; (define-with-contract account-withdraw
;;   (requires (< amount account-balance))
;;   (ensures (>= account-balance 0))
;;   (lambda (amount account-balance)
;;     ...))

;; Or abstractly:

;; (define-with-contract func
;;   (requires req-pred* ...)
;;   (ensures ensure-pred* ...)
;;   lambda-expr)


(import
  (except (rnrs base) let-values)
  (only (guile)
        lambda* λ)
  (ice-9 exceptions)
  (srfi srfi-1))


;; and-raise needs to be a macro, because its arguments must
;; not be immediately evaluated, otherwise we cannot raise
;; an exception containing the failing check.
(define-syntax and-raise
   (syntax-rules ()
     [(_ (op args* ...) check-expr* ...)
      (cond
       [(not (op args* ...))
        (raise-exception
         (make-exception
          (make-assertion-failure)
          (make-exception-with-message "assertion failed")
          (make-exception-with-irritants (quote (op args* ...)))))]
       [else
        (and-raise check-expr* ...)])]
     [(_ #|nothing|#)
       #t]))


;; `ensure` builds up an `and` expression, which contains
;; all the conditions.
(define-syntax ensure-with-result
   (syntax-rules (ensure)
     [(_ identifier expr* ... (op args* ...))
      (and-raise
       ;; insert identifier on the left
       (op identifier args* ...)
       (ensure-with-result identifier expr* ...))]
     ;; If there is only one more ensure clause, transform
     ;; it, and do not place another macro call.
     [(_ identifier (op args* ...))
      ;; insert identifier on the left
      (op identifier args* ...)]
     ;; If there are no more ensure clauses, transform to
     ;; `#t`, the neutral element of `and`.
     [(_ identifier)
      #t]))


(define-syntax define-with-contract
   (syntax-rules (require ensure <?>)
     ;; first process ensure (post-conditions)
     [(_ function-name
         (require reqs* ...)
         (ensure ensu-expr* ...)
         (lambda (args* ...)
           lambda-body-expr* ...))
      (define function-name
        (lambda (args* ...)
          ;; temporarily store result of the function
          (let ([result
                 (cond
                  ;; check pre-conditions (requirements)
                  [(not (and-raise reqs* ...))
                   (raise-exception
                    (make-exception
                     (make-assertion-failure)
                     (make-exception-with-message "assertion failed")
                     (make-exception-with-irritants (list args* ...))
                     (make-exception-with-origin (syntax->datum function-name))))]
                  ;; otherwise run the body
                  [else
                   lambda-body-expr* ...])])
            (cond
             ;; check post-conditions (ensures)
             [(not (ensure-with-result result ensu-expr* ...))
              ;; Problem: Cannot know which post-condition
              ;; failed. Could be improved.
              (raise-exception
               (make-exception
                (make-assertion-failure)
                (make-exception-with-message "assertion failed")
                (make-exception-with-irritants (list args* ...))
                (make-exception-with-origin (syntax->datum function-name))))]
             ;; return result if post conditions are true
             [else result]))))]))


;; Lets make an example definition: Withdrawing an amount of
;; money from an account, returning the new account balance
;; (although not really mutating the account or anything,
;; really just a toy example).
(define-with-contract account-withdraw
   (require (< amount account-balance)
            (>= amount 0))
   (ensure (>= 0))  ; depends on what the function returns
   (lambda (amount account-balance)
     (- account-balance amount)))


;; Using the defined function just like any other function.
(display (account-withdraw 10 20)) (newline)
(display (account-withdraw 30 20)) (newline)
~~~~

Are there any, for the more experienced eye, obvious mistakes or bad practices 
in there, that should be improved? (especially regarding macros).

Best regards,
Zelphir

-- 
repositories: https://notabug.org/ZelphirKaltstahl




^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: Contracts macro example
  2022-07-14 23:55 Contracts macro example Zelphir Kaltstahl
@ 2022-07-19 15:20 ` Maxime Devos
  2022-07-20  8:39   ` Zelphir Kaltstahl
  0 siblings, 1 reply; 6+ messages in thread
From: Maxime Devos @ 2022-07-19 15:20 UTC (permalink / raw)
  To: Zelphir Kaltstahl, Guile User

Zelphir Kaltstahl schreef op do 14-07-2022 om 23:55 [+0000]:
>           (make-assertion-failure)
>           (make-exception-with-message "assertion failed")
>           (make-exception-with-irritants (quote (op args* ...)))))]

Instead of a generic 'assertion failure', I believe a more specific
&contract-failure to be better.

Also, define*-with-contract / lambda*-with-contract would be nice.  

It would also be nice to define a global 'require' and 'ensure' and
'<?>' somewhere (e.g.: (define-syntax require (identifier-syntax
(syntax-error "'require' can only be used as part of a contract
construct")))), that way, require / ensure / <?> can be renamed during
importing, so all contract things could be prefixed with, say,
contract:.

     ;; `#t`, the neutral element of `and`.

Guile supports Unicode and UTF-8, so you can write ‘#t’ here instead of
`#t`.  Additionally, maybe #t -> #true?

Greetings,
Maxime.



^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: Contracts macro example
  2022-07-19 15:20 ` Maxime Devos
@ 2022-07-20  8:39   ` Zelphir Kaltstahl
  2022-07-20  8:55     ` Maxime Devos
  0 siblings, 1 reply; 6+ messages in thread
From: Zelphir Kaltstahl @ 2022-07-20  8:39 UTC (permalink / raw)
  To: Maxime Devos; +Cc: Guile User

Hello Maxime!

On 7/19/22 17:20, Maxime Devos wrote:
> Zelphir Kaltstahl schreef op do 14-07-2022 om 23:55 [+0000]:
>>            (make-assertion-failure)
>>            (make-exception-with-message "assertion failed")
>>            (make-exception-with-irritants (quote (op args* ...)))))]
> Instead of a generic 'assertion failure', I believe a more specific
> &contract-failure to be better.
Ah yes, I'll do that!
> Also, define*-with-contract / lambda*-with-contract would be nice.
I am not sure how to pattern match optional arguments (maybe not any special 
way) an keyword arguments (probably a special notation or way to do it, I 
guess). But I do seem to remember, that there were some procedures for building 
keyword argument procedures and that I should have an example somewhere. Perhaps 
I need to pattern match on literally #:optional and #:key and combinations of these.
> It would also be nice to define a global 'require' and 'ensure' and
> '<?>' somewhere (e.g.: (define-syntax require (identifier-syntax
> (syntax-error "'require' can only be used as part of a contract
> construct")))), that way, require / ensure / <?> can be renamed during
> importing, so all contract things could be prefixed with, say,
> contract:.

I thought about implementing <?> for the insertion location of the result in a 
predicate, but initially wanted to keep it simple and get a simple version to 
work. I think I have seen this for pipelining in an SRFI before … *checks* … 
Maybe in https://srfi.schemers.org/srfi-197/srfi-197.html, or 
https://srfi.schemers.org/srfi-26/srfi-26.html, or maybe in some other repository.

Now that the basic version works, I can try to introduce the placeholder.

The idea is to define these globally in the module, so that they can be exported 
separately, so that they can be renamed upon import, correct?

How could a macro check, whether it is used inside something else? If the 
pattern matching only looks at the form of the macro itself, how can I get the 
"context", in which it was used and check, whether that is inside a 
`define-with-contract`? I think I have not yet unlocked that knowledge yet : )

>       ;; `#t`, the neutral element of `and`.
>
> Guile supports Unicode and UTF-8, so you can write ‘#t’ here instead of
> `#t`.  Additionally, maybe #t -> #true?
Do you think I should not use #t here, but another value? My idea was, that 
using #t would not influence the result of an "and operation" at all. But 
perhaps using another value can be useful for checking against it in some 
situation?
> Greetings,
> Maxime.

Thank you for the input!
Zelphir

-- 
repositories: https://notabug.org/ZelphirKaltstahl




^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: Contracts macro example
  2022-07-20  8:39   ` Zelphir Kaltstahl
@ 2022-07-20  8:55     ` Maxime Devos
  2022-07-24  1:21       ` Zelphir Kaltstahl
  0 siblings, 1 reply; 6+ messages in thread
From: Maxime Devos @ 2022-07-20  8:55 UTC (permalink / raw)
  To: Zelphir Kaltstahl; +Cc: Guile User


[-- Attachment #1.1.1: Type: text/plain, Size: 2582 bytes --]

On 20-07-2022 10:39, Zelphir Kaltstahl wrote:

>> It would also be nice to define a global 'require' and 'ensure' and
>> '<?>' somewhere (e.g.: (define-syntax require (identifier-syntax
>> (syntax-error "'require' can only be used as part of a contract
>> construct")))), that way, require / ensure / <?> can be renamed during
>> importing, so all contract things could be prefixed with, say,
>> contract:.
>
> I thought about implementing <?> for the insertion location of the 
> result in a predicate, but initially wanted to keep it simple and get 
> a simple version to work. I think I have seen this for pipelining in 
> an SRFI before … *checks* … Maybe in 
> https://srfi.schemers.org/srfi-197/srfi-197.html, or 
> https://srfi.schemers.org/srfi-26/srfi-26.html, or maybe in some other 
> repository.
>
> Now that the basic version works, I can try to introduce the placeholder.
>
> The idea is to define these globally in the module, so that they can 
> be exported separately, so that they can be renamed upon import, correct?
Yes.
>
> How could a macro check, whether it is used inside something else? If 
> the pattern matching only looks at the form of the macro itself, how 
> can I get the "context", in which it was used and check, whether that 
> is inside a `define-with-contract`? I think I have not yet unlocked 
> that knowledge yet : ) 

That's one way to implement things (syntax-parameterize sounds useful 
here), but that sounds way more complicated than needs to be.  All you 
need to do is:

  * keep the original code
  * Add:
    (define <?> "consider define-syntax+identifier-syntax+syntax-error
    for better error messages but this will do for now)
  * Export <?> (at least, once your code is turned into a module, if the
    users of define-with-contract are in the same module as
    define-with-contract then exporting isn't required though harmless)
  * Likewise for 'require' and 'ensure'

By doing that, syntax-rules knows that its '<?>', 'require' and 'ensure' 
is not just the symbol '<?>' 'require' and 'ensure', but the 
_identifier_ (which keeps being the same identifier after renaming) 
'<?>', 'require' and 'ensure'.

(Note that as a consequence,  if you do that, (let ((require 0)) 
(define-with-contract foo (require) (ensure) (lambda _ 0))) will be a 
syntax error, because the 'require' in define-with-contract now refers 
to the variable 'require' from the let, not the identifier from your 
RnRS module).

That's all you need to do (untested)!


Greetings,
Maxime


[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 929 bytes --]

[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 236 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: Contracts macro example
  2022-07-20  8:55     ` Maxime Devos
@ 2022-07-24  1:21       ` Zelphir Kaltstahl
  2022-08-04 16:19         ` Maxime Devos
  0 siblings, 1 reply; 6+ messages in thread
From: Zelphir Kaltstahl @ 2022-07-24  1:21 UTC (permalink / raw)
  To: Maxime Devos; +Cc: Guile User

Hello Maxime!

I needed some time to implement the things you explained/mentioned, but I think 
I've got it now:

https://notabug.org/ZelphirKaltstahl/guile-examples/src/87304cc573086b5ef60c1d3aad25d4431fd5bca8/macros/contract.scm

There is still one issue though: The placeholder of the ensure clauses is 
assumed to be at the top level. So I will have to rewrite the macro, which looks 
at the clauses and replaces the placeholder with the result of the function, so 
that it can replace the placeholder in an arbitrarily nested structure. Maybe it 
will be easy, maybe it will be difficult.

Thanks for all your ideas!

I don't understand the following thing or how it works yet:

~~~~
(define-syntax require
   (identifier-syntax
    (syntax-error "'require' can only be used as part of a contract construct")))
~~~~

Can you explain how it works?

Best regards,
Zelphir

On 7/20/22 10:55, Maxime Devos wrote:
>
> On 20-07-2022 10:39, Zelphir Kaltstahl wrote:
>
>>> It would also be nice to define a global 'require' and 'ensure' and
>>> '<?>' somewhere (e.g.: (define-syntax require (identifier-syntax
>>> (syntax-error "'require' can only be used as part of a contract
>>> construct")))), that way, require / ensure / <?> can be renamed during
>>> importing, so all contract things could be prefixed with, say,
>>> contract:.
>>
>> I thought about implementing <?> for the insertion location of the result in 
>> a predicate, but initially wanted to keep it simple and get a simple version 
>> to work. I think I have seen this for pipelining in an SRFI before … *checks* 
>> … Maybe in https://srfi.schemers.org/srfi-197/srfi-197.html, or 
>> https://srfi.schemers.org/srfi-26/srfi-26.html, or maybe in some other 
>> repository.
>>
>> Now that the basic version works, I can try to introduce the placeholder.
>>
>> The idea is to define these globally in the module, so that they can be 
>> exported separately, so that they can be renamed upon import, correct?
> Yes.
>>
>> How could a macro check, whether it is used inside something else? If the 
>> pattern matching only looks at the form of the macro itself, how can I get 
>> the "context", in which it was used and check, whether that is inside a 
>> `define-with-contract`? I think I have not yet unlocked that knowledge yet : ) 
>
> That's one way to implement things (syntax-parameterize sounds useful here), 
> but that sounds way more complicated than needs to be.  All you need to do is:
>
>   * keep the original code
>   * Add:
>     (define <?> "consider define-syntax+identifier-syntax+syntax-error for
>     better error messages but this will do for now)
>   * Export <?> (at least, once your code is turned into a module, if the users
>     of define-with-contract are in the same module as define-with-contract
>     then exporting isn't required though harmless)
>   * Likewise for 'require' and 'ensure'
>
> By doing that, syntax-rules knows that its '<?>', 'require' and 'ensure' is 
> not just the symbol '<?>' 'require' and 'ensure', but the _identifier_ (which 
> keeps being the same identifier after renaming) '<?>', 'require' and 'ensure'.
>
> (Note that as a consequence,  if you do that, (let ((require 0)) 
> (define-with-contract foo (require) (ensure) (lambda _ 0))) will be a syntax 
> error, because the 'require' in define-with-contract now refers to the 
> variable 'require' from the let, not the identifier from your RnRS module).
>
> That's all you need to do (untested)!
>
>
> Greetings,
> Maxime
>
-- 
repositories:https://notabug.org/ZelphirKaltstahl


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: Contracts macro example
  2022-07-24  1:21       ` Zelphir Kaltstahl
@ 2022-08-04 16:19         ` Maxime Devos
  0 siblings, 0 replies; 6+ messages in thread
From: Maxime Devos @ 2022-08-04 16:19 UTC (permalink / raw)
  To: Zelphir Kaltstahl; +Cc: Guile User


[-- Attachment #1.1.1: Type: text/plain, Size: 751 bytes --]

On 24-07-2022 03:21, Zelphir Kaltstahl wrote:

> ~~~~
> (define-syntax require
>    (identifier-syntax
>     (syntax-error "'require' can only be used as part of a contract construct")))
> ~~~~
>
> Can you explain how it works?
>
If you meant: how can identifier-syntax + syntax-error be used? -- I 
don't have more information than the explanations in the manual on 
identifier-syntax and syntax-error.

If you meant: how does it work?  I don't know how Guile's macro system 
is implemented, you'll have to look at the source code of (ice-9 
psyntax), maybe read the paper it cites.

If you meant: how to use it? All you have to do is replace your (define 
require ...) by the (define-syntax require ...)

Greetings,
Maxime


[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 929 bytes --]

[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 236 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2022-08-04 16:19 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-07-14 23:55 Contracts macro example Zelphir Kaltstahl
2022-07-19 15:20 ` Maxime Devos
2022-07-20  8:39   ` Zelphir Kaltstahl
2022-07-20  8:55     ` Maxime Devos
2022-07-24  1:21       ` Zelphir Kaltstahl
2022-08-04 16:19         ` Maxime Devos

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).