rackjure
1 Introduction
2 Using as a language vs. as a library
3 Threading macros
~>
~>>
some~>
some~>>
4 Applicable dictionaries
4.1 Not-found values
5 Dictionary initialization using {}
current-curly-dict
alist
6 Dictionary utilities
dict-merge
dict-merge-delete-value
dict->curly-string
7 Strings
str
8 Conditionals
if-let
when-let
if-not
when-not
9 Operational equivalence
egal?
9.1 egal? and structs
10 Other
10.1 Partial application
partial
10.2 Atomic swap
box-swap!
11 Reader function literals
7.7

rackjure

Source.

This is tested on Racket versions 6.0 and newer.

    1 Introduction

    2 Using as a language vs. as a library

    3 Threading macros

    4 Applicable dictionaries

      4.1 Not-found values

    5 Dictionary initialization using {}

    6 Dictionary utilities

    7 Strings

    8 Conditionals

    9 Operational equivalence

      9.1 egal? and structs

    10 Other

      10.1 Partial application

      10.2 Atomic swap

    11 Reader function literals

1 Introduction

This package provides a few Clojure-inspired ideas in Racket.

Asumu Takikawa’s #lang clojure showed me what’s possible and was the original basis. Why not just use that? Because I wanted to use some Clojure ideas in Racket, not use Clojure.

When it must choose, #lang rackjure chooses to be more Rackety. 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".

2 Using as a language vs. as a library

 (require rackjure) package: rackjure

Most features work if you merely (require rackjure) or a specific module such as (require rackjure/threading) in any module language such as racket or racket/base.

However a few features only work as a module language — by using #lang rackjure at the start of your source file, or by supplying rackjure as the language in a module form. This is because they depend on redefining #%app or extending the Racket reader. These are:

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

Threads expression through the forms. Inserts expression as the second item in the first form, making a list of it if it is not a list already. If there are more forms, inserts the first form as the second item in second form, etc.

syntax

(~>> expression form ...)

Like ~> but inserting as the last item in each form.


The "threading" macros let you thread values through a series of applications in data-flow order. Sometimes this is a clearer than deeply 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. A function that takes just one argument can be specified this way: The ~> macro automatically adds the parentheses.

syntax

(some~> expression form ...)

Analogous to some-> in Clojure, i.e. stop threading at a #f value.

syntax

(some~>> expression form ...)

Analogous to some->> in Clojure, i.e. stop threading at a #f value.

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:

(dict-ref (dict-ref (dict-ref dict 'a) 'b) 'c)

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 #:else isn’t supplied and the key isn’t found we return #f (whereas dict-ref raises an error). Rationale: Returning #f is more convenient when used with threading macros like some~>. Admittedly, one person’s "convenience" is another person’s "magic behavior" and/or "latent bug".

5 Dictionary initialization using {}

#lang rackjure provides a more-concise way to create dictionaries.

You can write

((k0 . v0)(k1 . v1) ...)

as

{k0 v0 k1 v1 ... ...}

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?
Defaults to alist. May be set to hash, hasheq or anything with the same (f k v ... ...) signature.

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

procedure

(alist key val ... ...)  (listof (cons any/c any/c))

  key : any/c
  val : any/c
Creates an association list.

Example:
> (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?
Functionally merge d1 into d0. Values in d0 are overriden by values with the same key in d1. Nested dicts are handled recursively.

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

Defaults to 'DELETE. Used to tell dict-merge that a key/value pair with that key should be deleted.

> (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?
Returns a {} style string describing the dict d, including any nested dicts.

> (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 : ""

Idiomatic Racket would probably use ~a.

str can be a succinct alternative to string-append and/or format.

Also, it returns an immutable string (created via string->immutable-string).

Examples:
> (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:

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

Idiomatic Racket would probably use match.

Combines if and let:

(let ([identifier test-expr])
  (if identifier
      then-expr
      else-expr))

syntax

(when-let [identifier test-expr] body ...+)

Idiomatic Racket would probably use match.

Combines when with let:

(let ([identifier test-expr])
  (when identifier
    body ...))

syntax

(if-not test-expr then-expr else-expr)

A shortcut for:

(if (not test-expr)
    then-expr
    else-expr)

syntax

(when-not test-expr body ...+)

Idiomatic Racket would use unless.

A shortcut for:

(when (not test-expr)
  body ...)

9 Operational equivalence

 (require rackjure/egal) package: rackjure

procedure

(egal? v1 v2)  boolean?

  v1 : any/c
  v2 : any/c
An implementation of egal? as described in Equal Rights for Functional Objects.

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:

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
Function for partial application. Differs from curry in that it doesn’t care about function arity.

((partial + 1) 2) <=> (+ 1 2)

10.2 Atomic swap

procedure

(box-swap! box proc v ...)  any/c

  box : box?
  proc : procedure?
  v : any/c
Like swap! in Clojure, but for box?.

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

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)