syntax-objects / Summer2021

Syntax Parse Bee 2021
11 stars 3 forks source link

`define/curry` - Defines an automatically currying procedure #5

Open agj opened 3 years ago

agj commented 3 years ago

Macro

Defines an automatically currying procedure in a single step. Uses curry)) internally.

(begin-for-syntax
  (define-syntax-class name-params
    #:description "name and parameters clause"
    (pattern (name:id params:id ...+)
             #:fail-when (check-duplicate-identifier
                          (syntax->list #'(params ...)))
             "duplicate parameter name")))

(define-syntax (define/curry stx)
  (syntax-parse stx
    [(_ np:name-params body ...+)
     #`(define np.name
         (curry #,(syntax/loc stx
                    (λ (np.params ...)
                      body ...))))]))

Example

;; Define a procedure like this:

(define/curry (insert-between mid left right)
  (string-join (list left mid right) ""))

;; To use it like this:

(define dash-between (insert-between "-"))

(dash-between "left" "right") ; "left-right"

;; Or currying wherever necessary:

(((insert-between "-") "left") "right") ; "left-right"

Before and After

This code-cleaning macro reduces some boilerplate necessary to define a curried function. Code like this:

(define insert-between
  (curry
   (λ (mid left right)
     (string-join (list left mid right) ""))))

Is simplified to this, identical in form to defining a regular procedure:

(define/curry (insert-between mid left right)
  (string-join (list left mid right) ""))

I wrote an earlier version of this macro when implementing some Haskell code in Racket, and thought it was useful enough to share!

Licence

I release the above code under the MIT license, and accompanying text under the Creative Commons Attribution 4.0 International license.

agj commented 3 years ago

I'm not very experienced writing Racket or macros, so any improvement suggestions are welcome. 😊

Fictitious-Rotor commented 3 years ago

I'm not very experienced writing Racket or macros, so any improvement suggestions are welcome. 😊

This is a great little macro! Did you know, however, that there's another form for define that can achieve this?

(define ((insert-between2 mid) left right)
    (string-join (list left mid right) ""))

The docs show that the head form in the grammar is recursive :)

agj commented 3 years ago

Hi, Rotor! Thanks! Yes, I'm aware of that syntax. However as far as I understand it, that syntax doesn't allow you to do something like this. All of the following are equivalent:

(insert-between "-" "left" "right")
((insert-between "-" "left") "right")
((insert-between "-") "left" "right")
((insert-between) "-" "left" "right")
Fictitious-Rotor commented 3 years ago

Hi, Rotor! Thanks! Yes, I'm aware of that syntax. However as far as I understand it, that syntax doesn't allow you to do something like this. All of the following are equivalent:

(insert-between "-" "left" "right")
((insert-between "-" "left") "right")
((insert-between "-") "left" "right")
((insert-between) "-" "left" "right")

Oh wow you're totally right! Perhaps you should add some of those examples to your original post!

agj commented 3 years ago

@Fictitious-Rotor I updated with a link to the curry function's docs so that it doesn't look like my macro is doing any magic, and extended the example code a tiny bit. 👍

bennn commented 3 years ago

Looks great!

Small suggestion: move the syntax class to the toplevel. It'll have to go inside a (begin-for-syntax ....) and you will probably need to add for-syntax requires.

Annoying plumbing suggestion: use syntax/loc or quasisyntax/loc so that errors for things like (insert-between 1 2 3 4) point to the define/curry line instead of pointing to the lambda in the macro body.

agj commented 3 years ago

Hi, @bennn! Thanks! I tried your suggestions. As for syntax/loc (which I didn't know about before, so that was cool to learn), I didn't notice any change adding it, and without it it seems to behave well, so I guess this is a case in which it's not needed? Like this:

(require (for-syntax syntax/parse))

(begin-for-syntax
  (define-syntax-class name-params
    #:description "name and parameters clause"
    (pattern (name:id params:id ...+)
             #:fail-when (check-duplicate-identifier
                          (syntax->list #'(params ...)))
             "duplicate parameter name")))

(define-syntax (define/curry stx)
  (syntax-parse stx
    [(_ np:name-params body ...+)
     #'(define np.name
         (curry (λ (np.params ...)
                  body ...)))]))

(define/curry (insert-between mid left right)
  (string-join (list left mid right) ""))

(insert-between 2 3 3 4 4 44) ; This line gets highlighted with an error
bennn commented 3 years ago

Oh, that's great that the line with the function call gets highlighted.

When I run on the command line, though, it prints the function and says that function is defined on line 17 (inside the define-syntax)

curried:...test.rkt:17:16: arity mismatch;
 the expected number of arguments does not match the given number

Maybe a better way to phrase it is: get (displayln insert-between) and (displayln (object-name insert-between)) to print the right line.

If the definition of insert-between is in a different module than the macro, I think the name will be even more confusing.

agj commented 3 years ago

Yeah you're right… I tried using syntax/loc but the behavior was the same. Maybe I'm just not using it right though. I tried surrounding both the define and the curry parts. I'll try again later using those functions, as it does seem like a useful thing in general to know for API ergonomics.

bennn commented 3 years ago

Hint: the λ is what needs a loc

agj commented 3 years ago

@bennn Sorry for taking so long! I got a bit busy and distracted, but I tried your hint just now and it worked great! Thanks for the tip.