syntax-objects / Summer2021

Syntax Parse Bee 2021
11 stars 3 forks source link

js-dict -- syntaxes to manipulate dictionary inspired by JavaScript #17

Open sorawee opened 3 years ago

sorawee commented 3 years ago

Background

JavaScript has really elegant syntaxes to manipulate dictionaries.

Dictionary creation:

Given:

 let x = 42;

{a: 1 + 2, b: 3, ['a' + 'b']: 4, x} is a dictionary with four entries:

Merging

Other dictionaries can also be merged as a part of dictionary creation.

Given:

let a = {a: 1, c: 2};
let b = {b: 2, c: 3};

Then {b: 42, ...a, ...b, a: 4, d: 5} has four entries:

Note that object merging can be used to set a value functionally without mutating the dictionary.

Extraction:

Given:

let x = {a: 1, b: 2, c: 3, d: 4};

let {a, b: bp} = x; binds a to 1 and bp to 2.

Extraction of the rest

As a part of extraction, there can be at most one ..., which will function as the extraction of the rest

For example:

let {a, b: bp, ...y} = x; binds a to 1, bp to 2, y to {c: 3, d: 4}.

js-dict and js-extract macros

The js-dict and js-extract macros bring these operations to Racket, using immutable hash tables as the data structure. Additionally, the js-extract macro improves upon JS by supporting arbitrary match pattern.

js-dict: dictionary creation

(define d 4)
(define base-1 (js-dict [x '((10))] [b 20]))
(define base-2 (js-dict [y 30] [a 40]))
(define obj
  (js-dict
   [a 1]
   #:merge base-1
   [b 2]
   #:merge base-2
   [#:expr (string->symbol "c") 3]
   d))

Then obj should be '#hash((a . 40) (b . 2) (c . 3) (d . 4) (x . ((10))) (y . 30))

js-extract: dictionary extraction

With the above obj, in the following code:

(js-extract ([#:expr (string->symbol "a") f]
             c
             d
             [x (list (list x))]
             #:rest rst)
            obj)

Macro

#lang racket/base

(require syntax/parse/define
         racket/match
         racket/hash
         racket/splicing
         (for-syntax racket/base
                     racket/list))

(begin-for-syntax
  (define-splicing-syntax-class key
    (pattern {~seq #:expr key:expr}
             #:with static #'())
    (pattern {~seq key*:id}
             #:with key #''key*
             #:with static #'(key*)))

  (define-splicing-syntax-class construct-spec
    (pattern {~seq [key:key val:expr]}
             #:with code #'`[#:set ,key.key ,val]
             #:with (static ...) #'key.static)
    (pattern {~seq #:merge e:expr}
             #:with code #'`[#:merge ,e]
             #:with (static ...) #'())
    (pattern {~seq x:id}
             #:with code #'`[#:set x ,x]
             #:with (static ...) #'(x)))

  (define-syntax-class extract-spec
    (pattern [key*:key pat:expr]
             #:with key #'key*.key
             #:with (static ...) #'key*.static)
    (pattern x:id
             #:with key #''x
             #:with pat #'x
             #:with (static ...) #'(x))))

(define (make-dict . xs)
  (for/fold ([h (hash)]) ([x (in-list xs)])
    (match x
      [`[#:set ,key ,val] (hash-set h key val)]
      [`[#:merge ,d] (hash-union h d #:combine (λ (a b) b))])))

(define-syntax-parse-rule (js-dict spec:construct-spec ...)
  #:fail-when
  (check-duplicate-identifier (append* (attribute spec.static)))
  "duplicate static key"

  (make-dict spec.code ...))

(define-syntax-parser extract
  [(_ () pat-rst rst-obj) #'(match-define pat-rst rst-obj)]
  [(_ (spec:extract-spec specs ...) pat-rst rst-obj)
   #'(splicing-let ([KEY spec.key]
                    [OBJ rst-obj])
       (match-define spec.pat (hash-ref OBJ KEY))
       (extract (specs ...) pat-rst (hash-remove OBJ KEY)))])

(define-syntax-parse-rule (js-extract (spec:extract-spec ...
                                       {~optional {~seq #:rest e:expr}})
                                      obj:expr)
  #:fail-when
  (check-duplicate-identifier (append* (attribute spec.static)))
  "duplicate static key"

  (extract (spec ...) (~? e _) obj))

(module+ test
  (require rackunit)
  (test-begin
   (define d 4)
   (define base-1 (js-dict [x '((10))] [b 20]))
   (define base-2 (js-dict [y 30] [a 40]))
   (define obj
     (js-dict
      [a 1]
      #:merge base-1
      [b 2]
      #:merge base-2
      [#:expr (string->symbol "c") 3]
      d))

   (let ()
     (js-extract ([#:expr (string->symbol "a") f]
                  c
                  d
                  [x (list (list x))]
                  #:rest rst)
                 obj)
     (check-equal? f 40)
     (check-equal? c 3)
     (check-equal? d 4)
     (check-equal? x 10)
     (println rst)
     (check-equal? rst (js-dict [y 30] [b 2])))))

Licence

MIT / CC-BY 4.0