#lang rackjure
Provide a few Clojure-inspired ideas in Racket. Where Racket and Clojure conflict, prefer Racket.
This is tested on Racket versions 6.0 and newer.
1 Background/philosophy
Asumu Takikawa’s #lang clojure showed me what’s possible and is the basis for much of this. Why not just use that? Because I wanted to use some Clojure ideas in Racket, not use Clojure.
For example the threading macros are ~> and ~>> (using ~ instead of -) because Racket already uses -> for contracts. Plus as Danny Yoo pointed out to me, ~ is more "thready".
When it must choose, #lang rackjure chooses to be more Rackety.
2 #lang rackjure vs. require
(require rackjure) | package: rackjure |
Most features work if you merely (require rackjure) —
However a few features only work as a module language —
Of course, because they must make #%app do more work at runtime, there is some performance overhead.
However the overhead is only for function applications within a module using rackjure as its language —
not for function applications in other modules.
If you do not need those features, you can (require rackjure) or even just the specific modules you use, in a "leaner" lang such as racket/base.
For example you can use just the threading macros ~> and ~>> in racket/base:
#lang racket/base (require rackjure/threading)
3 Threading macros
(require rackjure/threading) | package: rackjure |
As of version 0.9, instead of providing its own implementation, this module now re-provides all of the threading package, which has additional features not described here. Please refer to its documentation.
syntax
(~> expression form ...)
syntax
(~>> expression form ...)
The "threading" macros let you thread values through a series of applications in data-flow order. This can be a really refreshing way to express a series of transforms, instead of nested function calls.
Although similar to the thrush combinator function (and you may hear them described that way), these are actually macros (in both Clojure and #lang rackjure).
And although similar to compose, the order is reversed, and again, these are macros.
The ~> form "threads" values through a series of forms as the second item of each form. (If a form is a function application, remember that the second item is the first argument.)
For example, instead of:
(string->bytes/utf-8 (number->string (bytes-length #"foobar") 16))
You can write:
(~> #"foobar" bytes-length (number->string 16) string->bytes/utf-8)
Or if you prefer on one line:
(~> #"foobar" bytes-length (number->string 16) string->bytes/utf-8)
Notice that bytes-length and string->bytes/utf-8 aren’t enclosed in parentheses. They could be, but if they’re not, the ~> macro adds them automatically. A function that takes just one argument can be specified this way.
syntax
(some~> expression form ...)
syntax
(some~>> expression form ...)
4 Applicable dictionaries
#lang rackjure redefines #%app to make applications work differently when a dict? is in the first position:
; When (dict? d) is #t ; Set (d key val) => (dict-set d key val) ; Get (d key) => (dict-ref d key #f) (d key #:else default) => (dict-ref d key default)
And also when a dict? is in the second position:
; Get (key d) => (dict-ref d key) (key #f) => #f ; unless (or (procedure? ‘key‘) (dict? ‘key‘))
These last two variants, in combination with the ~> threading macro, provide concise notation for accessing nested dictionary (for example the nested hasheqs from Racket’s json module):
(~> dict 'a 'b 'c)
expands to:
('c ('b ('a dict)))
which in turn is applied as:
Note that dictionary keys are not required to be Clojure style :keywords. They may be anything.
This application syntax doesn’t work for a dict? that stores procedure? as keys or values. The reason is that #lang rackjure must provide its own #%app. The only way (AFAIK) it can distinguish a normal function application from a dictionary application is to check for procedure? in the first position. As a result, in those cases you’ll have to use dict-ref and dict-set.
Keep in mind that a dict? is a Racket generic that covers a variety of things besides hash tables and association lists, such as vectors and lists. As a result if v is a vector then (vector-ref v 2) can be written simply as (v 2).
4.1 Not-found values
One issue is how to handle the optional last argument to dict-ref, which is the value to use if the key is not found. We handle this slightly differently than dict-ref:
1. We use an optional keyword argument, #:else. This leaves arity 3 available to mean dict-set.
2. If this arg isn’t supplied and the key isn’t found we return #f (whereas dict-ref raises an error). Returning #f is more convenient, especially when used with threading macro ~>. It’s smart that dict-ref lets you supply a specific value to mean not-found, because what if #f or 'not-found or whatever could be a valid value in the dictionary? But it’s even smarter to have not-found default to something other than raising an error. That way, the burden is only on code that needs to store #f as values in a dict, and such code can use the #:else keyword.
5 Dictionary initialization using {}
#lang rackjure provides a more-concise way to create dictionaries.
You can write
((k0 . v0)(k1 . v1) ...)
as
Especially handy with nested dicts:
{'key "value" 'key1 {'key "value" 'key1 "value1"}}
The current-curly-dict parameter says what this expands to.
parameter
(current-curly-dict) → procedure?
(current-curly-dict v) → void? v : procedure?
Examples:
> (parameterize ([current-curly-dict alist]) {'k0 0 'k1 1}) '((k0 . 0) (k1 . 1)) > (parameterize ([current-curly-dict hasheq]) {'k0 0 'k1 1}) '#hasheq((k0 . 0) (k1 . 1))
(require rackjure/alist) | package: rackjure |
> (alist 'k0 0 'k1 1 'k2 2) '((k0 . 0) (k1 . 1) (k2 . 2))
6 Dictionary utilities
(require rackjure/dict) | package: rackjure |
A few utility functions for dicts.
procedure
(dict-merge d0 d1) → dict?
d0 : dict? d1 : dict?
> (dict-merge {} {'type 'line}) '((type . line)) > (dict-merge {'type 'triangle 'sides 3} {'type 'square 'sides 4}) '((type . square) (sides . 4)) > (dict-merge {'people {'john {'age 10} 'mary {'age 7}}} {'people {'john {'age 11}}}) '((people (john (age . 11)) (mary (age . 7))))
Setting a value in d1 to the current value of the dict-merge-delete-value parameter – which defaults to 'DELETE – causes the key/value in d0 with that key to be deleted from the returned dictionary.
> (dict-merge '([a . a][b . b]) '([b . DELETE])) '([a . a])
parameter
(dict-merge-delete-value v) → void? v : any/c
> (parameterize ([dict-merge-delete-value 'DELETE]) (dict-merge '([a . a] [b . b]) '([b . DELETE]))) '([a . a]) > (parameterize ([dict-merge-delete-value 'FOO]) (dict-merge '([a . a] [b . b]) '([a . DELETE] [b . FOO]))) '((a . DELETE))
procedure
(dict->curly-string d) → string?
d : dict?
> (define sample-dict '([a . 0] [b . 0] [c . ([a . 0] [b . 0] [c . ([a . 0] [b . 0] [c . 0])])])) > (displayln (dict->curly-string sample-dict)) {'a 0 'b 0 'c {'a 0 'b 0 'c {'a 0 'b 0 'c 0}}}
7 Strings
(require rackjure/str) | package: rackjure |
procedure
(str expression ... #:fmt fmt #:sep sep)
→ (and/c string? immutable?) expression : any/c fmt : ~a sep : ""
Also, it returns an immutable string (created via string->immutable-string).
> (str) ""
> (str "hi") "hi"
> (str 1) "1"
> (str #f) "#f"
> (str "Yo" "Yo") "YoYo"
> (str "Yo" "Yo" "Ma") "YoYoMa"
> (apply str '(0 1 2 3)) "0123"
> (str 0 1 2 3) "0123"
> (str '(0 1 2 3)) "(0 1 2 3)"
Our version adds optional keyword arguments, the defaults of which behave like Clojure’s str:
#:fmt: The function to apply to each argument. Defaults to ~a. May be any (-> any/c string?) function, e.g. ~v.
#:sep: A string? to add between each. Defaults to "".
> (str #:fmt ~v "Yo" "Yo") "\"Yo\"\"Yo\""
> (str #:sep " " "Yo" "Yo") "Yo Yo"
> (str #:fmt ~v #:sep " " "Yo" "Yo") "\"Yo\" \"Yo\""
8 Conditionals
(require rackjure/conditionals) | package: rackjure |
syntax
(if-let [identifier test-expr] then-expr else-expr)
(let ([identifier test-expr]) (if identifier then-expr else-expr))
syntax
(when-let [identifier test-expr] body ...+)
(let ([identifier test-expr]) (when identifier body ...))
syntax
(if-not test-expr then-expr else-expr)
(if (not test-expr) then-expr else-expr)
syntax
(when-not test-expr body ...+)
(when (not test-expr) body ...)
However in colloquial Racket we’d simply use unless:
(unless test-expr body ...)
9 Operational equivalence
(require rackjure/egal) | package: rackjure |
An alternative to equal? and eq? that says whether two things are "operationally equivalent", by taking into account mutability.
In general, two things that are equal? will also be egal? only if they are both immutable. Some things in Racket aren’t immutable by default. For example, although "string-constants" are immutable, strings returned by string or string-join are mutable, unless you also run them through string->immutable-string. Same with bytes. Other things come in both mutable and immutable variants, such as hashes and vectors.
For more details, see egal.rkt for the implementation and test cases. A few examples:
> (require rackjure/egal) ; Although "string" literals are immutable... > (egal? "a" "a") #t
; string is mutable... > (egal? (string #\a) (string #\a)) #f
; Immutable strings are (you guessed it) immutable...
> (egal? (string->immutable-string (string #\a)) (string->immutable-string (string #\a))) #t
9.1 egal? and structs
For two structs to be egal?, all of the following must be true:
1. They must have the same field values.
2. They must be instances of the same structure type.
3. The structure type must be #:transparent. (Regular equal? does a field comparison for Racket structs only if they are #:transparent. Otherwise the structs are opaque and eq? is used.)
4. The structure type must not be #:mutable, nor must any of the individual fields be #:mutable.
10 Other
(require rackjure/utils) | package: rackjure |
10.1 Partial application
procedure
(partial proc v ...) → procedure?
proc : procedure? v : any/c
((partial + 1) 2) <=> (+ 1 2)
10.2 Atomic swap
procedure
box : box? proc : procedure? v : any/c
Essentially it is:
(define (box-swap! box f . args) (let loop () (let* ([old (unbox box)] [new (apply f old args)]) (if (box-cas! box old new) new (loop)))))
11 Reader function literals
The Clojure reader lets you succinctly define anonymous function literals. For example
#(+ % %2)
is equivalent to this in Clojure:
(fn [% %2] (+ % %2))
or in Racket:
(λ (% %2) (+ % %2)) (lambda (% %2) (+ % %2))
%1 through %n are positional arguments
% is a synonym for %1
%& is a rest argument
%#:keyword is a #:keyword argument
The Racket reader already uses #( ) for vector literals. Therefore Rackjure instead uses your choice of #fn( ), #λ( ), or #lambda( ).
Examples:
> (map #λ(+ % 1) '(1 2 3)) |
'(2 3 4) |
> (map #λ(+ % %2) '(1 2 3) '(1 2 3)) |
'(2 4 6) |
|
;; Rest argument |
> (#λ(apply list* % %&) 1 '(2 3)) |
'(1 2 3) |
|
;; Keyword argument |
> (#λ(* 1/2 %#:m (* %#:v %#:v)) #:m 2 #:v 1) |
1 |
|
;; Ignores unused arguments |
> (#λ(begin %2) "ignored" "used") |
"used" |
|
;; Handles an arbitary number of arguments |
> (apply #λ(list %1 %42) (build-list 42 add1)) |
(list 1 42) |