Skip to content

Commit 601584b

Browse files
committed
1 parent b1ca608 commit 601584b

File tree

10 files changed

+465
-0
lines changed

10 files changed

+465
-0
lines changed

flaggable-app/flaggable-app-test.rkt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#lang racket/base
2+
(module+ test
3+
(require rackunit syntax/macro-testing syntax-parse-example/flaggable-app/flaggable-app)
4+
5+
(test-case "example"
6+
(define (f c #:a [a #f] #:b [b #f])
7+
(list c a b))
8+
(check-equal? (f 0 #:a #:b) '(0 #t #t))
9+
(check-equal? (f 0 #:a) '(0 #t #f))
10+
(check-equal? (f 0 #:b) '(0 #f #t))
11+
(check-equal? (f 0 #:a 10 #:b) '(0 10 #t))
12+
(check-equal? (f 0 #:a #:b 20) '(0 #t 20))
13+
(check-equal? (f 0 #:a 10 #:b 20) '(0 10 20))
14+
(check-equal? (f 0) '(0 #f #f))
15+
(check-exn exn:fail:syntax?
16+
(lambda () (convert-compile-time-error (f #:a 0 1)))))
17+
18+
)

flaggable-app/flaggable-app.rkt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#lang racket/base
2+
(provide #%app)
3+
4+
(require syntax/parse/define
5+
(only-in racket [#%app racket:#%app])
6+
(for-syntax racket/base))
7+
8+
(begin-for-syntax
9+
(define-splicing-syntax-class arg/keyword
10+
#:attributes (k v)
11+
;; first case: something like #:a 1
12+
(pattern {~seq k:keyword v:expr})
13+
;; second case: something like #:a.
14+
(pattern {~seq k:keyword}
15+
#:with v #'#t)))
16+
17+
(define-syntax-parse-rule
18+
(#%app f arg/no-keyword:expr ... arg/keyword:arg/keyword ...)
19+
(racket:#%app f arg/no-keyword ... {~@ arg/keyword.k arg/keyword.v} ...))
20+

flaggable-app/flaggable-app.scrbl

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#lang syntax-parse-example
2+
@require[
3+
(for-label (except-in racket/base #%app) syntax/parse syntax-parse-example/flaggable-app/flaggable-app)]
4+
5+
@(define plain-eval
6+
(make-base-eval '(require racket/string)))
7+
8+
@(define flaggable-app-eval
9+
(make-base-eval '(require racket/string syntax-parse-example/flaggable-app/flaggable-app)))
10+
11+
@title{@tt{flaggable-app}}
12+
@stxbee2021["sorawee" 14]
13+
14+
@; =============================================================================
15+
16+
@defmodule[syntax-parse-example/flaggable-app/flaggable-app]{}
17+
18+
@defform[(#%app fn expr ...+)]{
19+
Many functions accept optional boolean keyword arguments.
20+
These arguments are known as flags.
21+
As a simple example, the following function accepts two flags
22+
@racket[#:left] and @racket[#:right]:
23+
24+
@examples[#:label #f #:eval flaggable-app-eval
25+
(define (trim s #:left? [left? #f] #:right? [right? #f])
26+
(string-trim s #:left? left? #:right? right?))
27+
]
28+
@examples[#:hidden #:eval plain-eval
29+
(define (trim s #:left? [left? #f] #:right? [right? #f])
30+
(string-trim s #:left? left? #:right? right?))
31+
]
32+
33+
The function may be invoked with any number of flags, but if a flag keyword
34+
appears then it needs an argument as well:
35+
36+
@examples[#:label #f #:eval plain-eval
37+
(trim " 1 2 3 " #:left? #t)
38+
(eval:error (trim " 1 2 3 " #:left?))
39+
]
40+
41+
Flaggable @racket[#%app] allows users to instead write:
42+
43+
@examples[#:label #f #:eval flaggable-app-eval
44+
(trim " 1 2 3 " #:left?)
45+
(trim " 1 2 3 " #:left? #:right?)
46+
]
47+
48+
That is, a keyword that doesn't come with an argument will default the
49+
value to @racket[#t]. Arguments are still supported.
50+
51+
This does come at a cost: all keyword arguments must be specified after
52+
positional arguments to avoid ambiguity. Without this restriction, it is hard
53+
to tell whether:
54+
55+
@racketblock[
56+
(f #:a 1)
57+
]
58+
59+
is meant to be itself or:
60+
61+
@racketblock[
62+
(f 1 #:a #t)
63+
]
64+
65+
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"].
66+
67+
@racketfile{flaggable-app.rkt}
68+
69+
}

index.scrbl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,6 @@
3636
@include-example{dot-underscore}
3737
@include-example{try-catch-finally}
3838
@include-example{kw-ctc}
39+
@include-example{pyret-for}
40+
@include-example{flaggable-app}
41+
@include-example{js-dict}

js-dict/js-dict-test.rkt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#lang racket/base
2+
(module+ test
3+
(require rackunit syntax-parse-example/js-dict/js-dict)
4+
5+
(test-begin
6+
(define d 4)
7+
(define base-1 (js-dict [x '((10))] [b 20]))
8+
(define base-2 (js-dict [y 30] [a 40]))
9+
(define obj
10+
(js-dict
11+
[a 1]
12+
#:merge base-1
13+
[b 2]
14+
#:merge base-2
15+
[#:expr (string->symbol "c") 3]
16+
d))
17+
18+
(test-case "js-dict"
19+
(check-equal? obj '#hash((a . 40) (b . 2) (c . 3) (d . 4) (x . ((10))) (y . 30))))
20+
21+
(test-case "js-extract"
22+
(js-extract ([#:expr (string->symbol "a") f]
23+
c
24+
d
25+
[x (list (list x))]
26+
#:rest rst)
27+
obj)
28+
(check-equal? f 40)
29+
(check-equal? c 3)
30+
(check-equal? d 4)
31+
(check-equal? x 10)
32+
(check-equal? rst '#hash((b . 2) (y . 30)))))
33+
)

js-dict/js-dict.rkt

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#lang racket/base
2+
(provide js-dict js-extract)
3+
4+
(require syntax/parse/define
5+
racket/match
6+
racket/hash
7+
racket/splicing
8+
(for-syntax racket/base
9+
racket/list))
10+
11+
(begin-for-syntax
12+
(define-splicing-syntax-class key
13+
(pattern {~seq #:expr key:expr}
14+
#:with static #'())
15+
(pattern {~seq key*:id}
16+
#:with key #''key*
17+
#:with static #'(key*)))
18+
19+
(define-splicing-syntax-class construct-spec
20+
(pattern {~seq [key:key val:expr]}
21+
#:with code #'`[#:set ,key.key ,val]
22+
#:with (static ...) #'key.static)
23+
(pattern {~seq #:merge e:expr}
24+
#:with code #'`[#:merge ,e]
25+
#:with (static ...) #'())
26+
(pattern {~seq x:id}
27+
#:with code #'`[#:set x ,x]
28+
#:with (static ...) #'(x)))
29+
30+
(define-syntax-class extract-spec
31+
(pattern [key*:key pat:expr]
32+
#:with key #'key*.key
33+
#:with (static ...) #'key*.static)
34+
(pattern x:id
35+
#:with key #''x
36+
#:with pat #'x
37+
#:with (static ...) #'(x))))
38+
39+
(define (make-dict . xs)
40+
(for/fold ([h (hash)]) ([x (in-list xs)])
41+
(match x
42+
[`[#:set ,key ,val] (hash-set h key val)]
43+
[`[#:merge ,d] (hash-union h d #:combine (λ (a b) b))])))
44+
45+
(define-syntax-parse-rule (js-dict spec:construct-spec ...)
46+
#:fail-when
47+
(check-duplicate-identifier (append* (attribute spec.static)))
48+
"duplicate static key"
49+
(make-dict spec.code ...))
50+
51+
(define-syntax-parser extract
52+
[(_ () pat-rst rst-obj) #'(match-define pat-rst rst-obj)]
53+
[(_ (spec:extract-spec specs ...) pat-rst rst-obj)
54+
#'(splicing-let ([KEY spec.key]
55+
[OBJ rst-obj])
56+
(match-define spec.pat (hash-ref OBJ KEY))
57+
(extract (specs ...) pat-rst (hash-remove OBJ KEY)))])
58+
59+
(define-syntax-parse-rule (js-extract (spec:extract-spec ...
60+
{~optional {~seq #:rest e:expr}})
61+
obj:expr)
62+
#:fail-when
63+
(check-duplicate-identifier (append* (attribute spec.static)))
64+
"duplicate static key"
65+
(extract (spec ...) (~? e _) obj))
66+

js-dict/js-dict.scrbl

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
#lang syntax-parse-example
2+
@require[
3+
(for-label racket/base syntax/parse syntax-parse-example/js-dict/js-dict)]
4+
5+
@(define js-dict-eval
6+
(make-base-eval '(require syntax-parse-example/js-dict/js-dict)))
7+
8+
@(define (codeverb . elem*) (nested #:style 'code-inset (apply verbatim elem*)))
9+
10+
@title{JavaScript-Inspired Dictionary Syntax}
11+
@stxbee2021["sorawee" 17]
12+
13+
JavaScript (JS) has really elegant syntax to manipulate dictionaries.
14+
15+
16+
@bold{JS Dictionary Creation}
17+
18+
Given @tt{x = 42} the following syntax makes a dictionary with four entries:
19+
20+
@codeverb|{
21+
{a: 1 + 2, b: 3, ['a' + 'b']: 4, x}
22+
}|
23+
24+
@itemlist[
25+
@item{@tt{'a'} maps to @tt{3};}
26+
@item{@tt{'b'} maps to @tt{3};}
27+
@item{@tt{'ab'} maps to @tt{4}; and}
28+
@item{@tt{'x'} maps to @tt{42}}
29+
]
30+
31+
@bold{JS Dictionary Merging}
32+
33+
Other dictionaries can be merged as a part of dictionary creation.
34+
35+
Given:
36+
37+
@codeverb{
38+
let a = {a: 1, c: 2};
39+
let b = {b: 2, c: 3};
40+
}
41+
42+
Then the following dictionary has four entries:
43+
44+
@codeverb{
45+
{b: 42, ...a, ...b, a: 4, d: 5}
46+
}
47+
48+
@itemlist[
49+
@item{@tt{'a'} maps to @tt{4};}
50+
@item{@tt{'b'} maps to @tt{2};}
51+
@item{@tt{'c'} maps to @tt{3}; and}
52+
@item{@tt{'d'} maps to @tt{5}}
53+
]
54+
55+
Note that the merging syntax can be used to set a value functionally without
56+
mutating the dictionary.
57+
58+
@bold{JS Dictionary Extraction}
59+
60+
Given:
61+
62+
@codeverb{
63+
let x = {a: 1, b: 2, c: 3, d: 4};
64+
}
65+
66+
Then the following syntax:
67+
68+
@codeverb{
69+
`let {a, b: bp} = x;`
70+
}
71+
72+
binds @tt{a} to @tt{1} and @tt{bp} to @tt{2}.
73+
74+
75+
76+
@bold{JS Dictionary Extraction of the rest}
77+
78+
As a part of extraction, there can be at most one @tt{...}, which will function as
79+
the extraction of the rest
80+
81+
For example:
82+
83+
@codeverb{
84+
let {a, b: bp, ...y} = x;
85+
}
86+
87+
binds @tt{a} to @tt{1}, @tt{bp} to @tt{2}, @tt{y} to @tt{{c: 3, d: 4}}.
88+
89+
90+
@; =============================================================================
91+
92+
@defmodule[syntax-parse-example/js-dict/js-dict]{}
93+
94+
The @racket[js-dict] and @racket[js-extract] macros bring these operations to
95+
Racket, using immutable hash tables as the data structure.
96+
Additionally, the @racket[js-extract] macro improves upon JS by supporting
97+
arbitrary match pattern.
98+
99+
@defform[(js-dict construct-spec ...)
100+
#:grammar ([ccnstruct-spec [key expr]
101+
(#:merge expr)
102+
id]
103+
[key (#:expr expr) id])]{
104+
105+
@examples[#:eval js-dict-eval
106+
(define d 4)
107+
(define base-1 (js-dict [x '((10))] [b 20]))
108+
(define base-2 (js-dict [y 30] [a 40]))
109+
(define obj
110+
(js-dict
111+
[a 1]
112+
#:merge base-1
113+
[b 2]
114+
#:merge base-2
115+
[#:expr (string->symbol "c") 3]
116+
d))
117+
obj
118+
]
119+
120+
}
121+
122+
@defform[(js-extract (extract-spec ... maybe-rest) obj-expr)
123+
#:grammar ([extract-spec [key pattern-expr]
124+
id]
125+
[maybe-rest (code:line) #:rest expr]
126+
[key (#:expr expr) id])]{
127+
128+
With the above @racket[_obj], in the following code adds five definitions:
129+
130+
@examples[#:eval js-dict-eval
131+
(js-extract ([#:expr (string->symbol "a") f]
132+
c
133+
d
134+
[x (list (list x))]
135+
#:rest rst)
136+
obj)
137+
f
138+
c
139+
d
140+
x
141+
rst
142+
]
143+
}
144+
145+
Implementation:
146+
147+
@racketfile{js-dict.rkt}
148+

pyret-for/pyret-for-test.rkt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#lang racket/base
2+
(module+ test
3+
(require rackunit racket/list racket/string syntax-parse-example/pyret-for/pyret-for)
4+
5+
(test-case "no-match"
6+
(define things '(("pen") ("pineapple") ("apple") ("pen")))
7+
(define quantities '(1 2 3 5))
8+
(check-true
9+
(pyret-for andmap ([thing things] [quantity quantities])
10+
(or (string-contains? (first thing) "apple")
11+
(odd? quantity)))))
12+
13+
(test-case "match"
14+
(define things '(("pen") ("pineapple") ("apple") ("pen")))
15+
(define quantities '(1 2 3 5))
16+
(check-true
17+
(pyret-for andmap ([(list thing) things] [quantity quantities])
18+
(or (string-contains? thing "apple")
19+
(odd? quantity)))))
20+
)

0 commit comments

Comments
 (0)