Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add pyret-for, flaggable-app, js-dict #38

Merged
merged 1 commit into from
Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions flaggable-app/flaggable-app-test.rkt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#lang racket/base
(module+ test
(require rackunit syntax/macro-testing syntax-parse-example/flaggable-app/flaggable-app)

(test-case "example"
(define (f c #:a [a #f] #:b [b #f])
(list c a b))
(check-equal? (f 0 #:a #:b) '(0 #t #t))
(check-equal? (f 0 #:a) '(0 #t #f))
(check-equal? (f 0 #:b) '(0 #f #t))
(check-equal? (f 0 #:a 10 #:b) '(0 10 #t))
(check-equal? (f 0 #:a #:b 20) '(0 #t 20))
(check-equal? (f 0 #:a 10 #:b 20) '(0 10 20))
(check-equal? (f 0) '(0 #f #f))
(check-exn exn:fail:syntax?
(lambda () (convert-compile-time-error (f #:a 0 1)))))

)
20 changes: 20 additions & 0 deletions flaggable-app/flaggable-app.rkt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#lang racket/base
(provide #%app)

(require syntax/parse/define
(only-in racket [#%app racket:#%app])
(for-syntax racket/base))

(begin-for-syntax
(define-splicing-syntax-class arg/keyword
#:attributes (k v)
;; first case: something like #:a 1
(pattern {~seq k:keyword v:expr})
;; second case: something like #:a.
(pattern {~seq k:keyword}
#:with v #'#t)))

(define-syntax-parse-rule
(#%app f arg/no-keyword:expr ... arg/keyword:arg/keyword ...)
(racket:#%app f arg/no-keyword ... {~@ arg/keyword.k arg/keyword.v} ...))

69 changes: 69 additions & 0 deletions flaggable-app/flaggable-app.scrbl
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#lang syntax-parse-example
@require[
(for-label (except-in racket/base #%app) syntax/parse syntax-parse-example/flaggable-app/flaggable-app)]

@(define plain-eval
(make-base-eval '(require racket/string)))

@(define flaggable-app-eval
(make-base-eval '(require racket/string syntax-parse-example/flaggable-app/flaggable-app)))

@title{@tt{flaggable-app}}
@stxbee2021["sorawee" 14]

@; =============================================================================

@defmodule[syntax-parse-example/flaggable-app/flaggable-app]{}

@defform[(#%app fn expr ...+)]{
Many functions accept optional boolean keyword arguments.
These arguments are known as flags.
As a simple example, the following function accepts two flags
@racket[#:left] and @racket[#:right]:

@examples[#:label #f #:eval flaggable-app-eval
(define (trim s #:left? [left? #f] #:right? [right? #f])
(string-trim s #:left? left? #:right? right?))
]
@examples[#:hidden #:eval plain-eval
(define (trim s #:left? [left? #f] #:right? [right? #f])
(string-trim s #:left? left? #:right? right?))
]

The function may be invoked with any number of flags, but if a flag keyword
appears then it needs an argument as well:

@examples[#:label #f #:eval plain-eval
(trim " 1 2 3 " #:left? #t)
(eval:error (trim " 1 2 3 " #:left?))
]

Flaggable @racket[#%app] allows users to instead write:

@examples[#:label #f #:eval flaggable-app-eval
(trim " 1 2 3 " #:left?)
(trim " 1 2 3 " #:left? #:right?)
]

That is, a keyword that doesn't come with an argument will default the
value to @racket[#t]. Arguments are still supported.

This does come at a cost: all keyword arguments must be specified after
positional arguments to avoid ambiguity. Without this restriction, it is hard
to tell whether:

@racketblock[
(f #:a 1)
]

is meant to be itself or:

@racketblock[
(f 1 #:a #t)
]

Note: inspired by @hyperlink["https://www.reddit.com/r/Racket/comments/oytknk/keyword_arguments_without_values/h7w67dd/" "reddit.com/r/Racket/comments/oytknk/keyword_arguments_without_values/h7w67dd"].

@racketfile{flaggable-app.rkt}

}
3 changes: 3 additions & 0 deletions index.scrbl
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@
@include-example{dot-underscore}
@include-example{try-catch-finally}
@include-example{kw-ctc}
@include-example{pyret-for}
@include-example{flaggable-app}
@include-example{js-dict}
33 changes: 33 additions & 0 deletions js-dict/js-dict-test.rkt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#lang racket/base
(module+ test
(require rackunit syntax-parse-example/js-dict/js-dict)

(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))

(test-case "js-dict"
(check-equal? obj '#hash((a . 40) (b . 2) (c . 3) (d . 4) (x . ((10))) (y . 30))))

(test-case "js-extract"
(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)
(check-equal? rst '#hash((b . 2) (y . 30)))))
)
66 changes: 66 additions & 0 deletions js-dict/js-dict.rkt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#lang racket/base
(provide js-dict js-extract)

(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))

148 changes: 148 additions & 0 deletions js-dict/js-dict.scrbl
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#lang syntax-parse-example
@require[
(for-label racket/base syntax/parse syntax-parse-example/js-dict/js-dict)]

@(define js-dict-eval
(make-base-eval '(require syntax-parse-example/js-dict/js-dict)))

@(define (codeverb . elem*) (nested #:style 'code-inset (apply verbatim elem*)))

@title{JavaScript-Inspired Dictionary Syntax}
@stxbee2021["sorawee" 17]

JavaScript (JS) has really elegant syntax to manipulate dictionaries.


@bold{JS Dictionary Creation}

Given @tt{x = 42} the following syntax makes a dictionary with four entries:

@codeverb|{
{a: 1 + 2, b: 3, ['a' + 'b']: 4, x}
}|

@itemlist[
@item{@tt{'a'} maps to @tt{3};}
@item{@tt{'b'} maps to @tt{3};}
@item{@tt{'ab'} maps to @tt{4}; and}
@item{@tt{'x'} maps to @tt{42}}
]

@bold{JS Dictionary Merging}

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

Given:

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

Then the following dictionary has four entries:

@codeverb{
{b: 42, ...a, ...b, a: 4, d: 5}
}

@itemlist[
@item{@tt{'a'} maps to @tt{4};}
@item{@tt{'b'} maps to @tt{2};}
@item{@tt{'c'} maps to @tt{3}; and}
@item{@tt{'d'} maps to @tt{5}}
]

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

@bold{JS Dictionary Extraction}

Given:

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

Then the following syntax:

@codeverb{
`let {a, b: bp} = x;`
}

binds @tt{a} to @tt{1} and @tt{bp} to @tt{2}.



@bold{JS Dictionary Extraction of the rest}

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

For example:

@codeverb{
let {a, b: bp, ...y} = x;
}

binds @tt{a} to @tt{1}, @tt{bp} to @tt{2}, @tt{y} to @tt{{c: 3, d: 4}}.


@; =============================================================================

@defmodule[syntax-parse-example/js-dict/js-dict]{}

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

@defform[(js-dict construct-spec ...)
#:grammar ([ccnstruct-spec [key expr]
(#:merge expr)
id]
[key (#:expr expr) id])]{

@examples[#:eval js-dict-eval
(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))
obj
]

}

@defform[(js-extract (extract-spec ... maybe-rest) obj-expr)
#:grammar ([extract-spec [key pattern-expr]
id]
[maybe-rest (code:line) #:rest expr]
[key (#:expr expr) id])]{

With the above @racket[_obj], in the following code adds five definitions:

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

Implementation:

@racketfile{js-dict.rkt}

20 changes: 20 additions & 0 deletions pyret-for/pyret-for-test.rkt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#lang racket/base
(module+ test
(require rackunit racket/list racket/string syntax-parse-example/pyret-for/pyret-for)

(test-case "no-match"
(define things '(("pen") ("pineapple") ("apple") ("pen")))
(define quantities '(1 2 3 5))
(check-true
(pyret-for andmap ([thing things] [quantity quantities])
(or (string-contains? (first thing) "apple")
(odd? quantity)))))

(test-case "match"
(define things '(("pen") ("pineapple") ("apple") ("pen")))
(define quantities '(1 2 3 5))
(check-true
(pyret-for andmap ([(list thing) things] [quantity quantities])
(or (string-contains? thing "apple")
(odd? quantity)))))
)
Loading