Experiment with defines: define unity

I went through a phase (which may now be over) where I thought it was really odd that we had a whole bunch of define-like forms. (Why? Mostly for reasons of getting indentation right in vim: vim uses an option lispwords to know what forms to indent like define or cond (special forms) and what should indented like function application. Every new language or DSL's define-like form meant making sure lispwords has enough names. It's possible that raco fmt will fix this, but I haven't tried it yet. With the following idea, lispwords only needs define and the handful of forms like cond, for/list, etc.)

During that phase, I created the following possibly monstrous code, which I nicknamed "define unity" (obvious pun intended). The code redefines define to carry a notion of "transformer" (keyword argument) that controls how the define functions. It includes some sample uses.

There are obvious problems:

  • no arrows from keywords, since they aren't identifiers with bindings. Could this even be solved? I only recently learned about #:source for format-id, maybe that will do it?
  • I would like to write (define #:values (x y z) (values 1 2 3)) and not (define #:from-values (x y z) (values 1 2 3)). Idem. for match/define and #:match vs. #:from-match.
  • (define #:list 1 2 3) is syntactically correct, and is not a definition at all

OTOH, I imagined a world of (require define/unity) followed by a list of "define unity" libraries, providing the kind of syntax-transformers that cooperate well as keyword arguments to the new define (which, by the way, still also functions as a regular define). Patterns emerged, like using (define #:k x v) for defining values but (define (f x) #:k b) for functions and macros.

I'm sharing the code here for feedback on the idea, and in case anyone is interested.

#lang racket

(require syntax/parse/define
         (only-in racket/base [define def])
         (for-syntax racket/syntax)
         rackunit)

(define-syntax define
  (syntax-parser
    [(_ {~or {~seq t:keyword head}
             {~seq head t:keyword}}
        . body)
     #:with transformer (format-id #'t "~a" (syntax->datum #'t))
     (syntax/loc this-syntax
       (transformer head . body))]
    [(_ head . body)
     (syntax/loc this-syntax
       (def head . body))]))

(define (rename-transformer name:id impl:id)
  #:define-syntax-parse-rule
  (define #:define-syntax name (make-rename-transformer #'impl)))

(define (strip-define sans-define:id)
  #:define-syntax-parse-rule
  #:with name (format-id #'sans-define "define-~a" #'sans-define)
  (define #:rename-transformer sans-define name))

(define #:strip-define syntax-parse-rule)
(define #:strip-define syntax)

(define #:rename-transformer from-match match-define)

;; {{{ define-steps impl
(module steps racket
  (provide define-steps)

  (require syntax/parse/define
           (for-syntax syntax/parse/lib/function-header))

  (define step-level (make-parameter -1))
  (define (indent) (build-string (* (step-level) 3) (λ _ #\space)))

  (begin-for-syntax
    (define-splicing-syntax-class step
      #:datum-literals (step)
      (pattern {~seq step message:string {~and form:expr {~not step}} ...})))

  (define-syntax-parse-rule (define-steps header:function-header step:step ...+)
    (define header
      (parameterize ([step-level (add1 (step-level))])
        (define step-number -1)
        (begin
          (set! step-number (add1 step-number))
          (displayln (format "~a~a. ~a" (indent) step-number step.message))
          step.form ...)
        ...)))) ;; }}}

(require 'steps)
(define #:rename-transformer with-steps define-steps)
(define #:strip-define steps)

;; --------------------------------------------------

(define foo 123)
(check-equal? foo 123)

(define (bar x) (list foo x))
(check-equal? (bar 456) (list 123 456))

(define (baz x)
  #:syntax-parse-rule
  (bar x))
(check-equal? (baz 456) (list 123 456))

(define #:from-match `(,a ,b, c) (list 1 2 3))
(check-equal? (list a b c) (list 1 2 3))

(define #:rename-transformer from-values define-values)
;; don't want to redefine values
;; suggests a change: transform #:values into values-define-transformer or
;; similar, so that we can use #:values, #:match, etc., without issue.
;; test for binding? preferred order?
;; (define #:strip-define values)

(define #:from-values (x y z) (values 1 2 3))
(check-equal? (list x y z) (list 1 2 3))

;; ??
(check-equal? (define #:list 1 2 3) (list 1 2 3))

(define (qux bar)
  #:steps
  step "Setup"
  (define a 1)
  (define b 2)
  step "Make a list"
  (list a b bar))

(define #:with-steps (zam zoink)
  step "My variables"
  (define c 3)
  step "Make a list with sublists"
  (list (qux c) zoink))


(check-equal? (with-output-to-string (thunk (check-equal? (qux 3) '(1 2 3))))
              "0. Setup\n1. Make a list\n")

(check-equal? (with-output-to-string (thunk (check-equal? (zam 3) '((1 2 3) 3))))
              #<<EOS
0. My variables
1. Make a list with sublists
   0. Setup
   1. Make a list

EOS
                )
4 Likes

If I may steal @soegaard 's thunder, they recently posted GitHub - soegaard/bind: Bind: Let with binding clause transformers as "bindings galore" in Discord.

2 Likes

@stchang @AlexKnauth

Stephen Chang and Alexander Knauth has this related package:

https://docs.racket-lang.org/generic-bind/index.html

The documentation has moved:
https://soegaard.github.io/docs/bind/bind.html