Sugar
(require sugar) | package: sugar |
(require (submod sugar safe)) |
A collection of small functions to help make Racket code simpler & more readable. Well, according to me, anyhow.
Sugar can be invoked two ways: as an ordinary library, or as a library with contracts (using the safe submodule).
1 Installation & updates
raco pkg install sugar |
raco pkg update sugar |
2 Cache
(require sugar/cache) | package: sugar |
(require (submod sugar/cache safe)) |
If, like Ricky Bobby and me, you want to go fast, then try using more caches. They’re wicked fast.
procedure
(make-caching-proc proc) → procedure?
proc : procedure?
In the example below, notice that both invocations of slow-op take approximately the same time, whereas the second invocation of fast-op gets its value from the cache, and is thus nearly instantaneous.
> (define (slow-op x) (for/sum ([i (in-range 100000000)]) i)) > (time (slow-op 42)) cpu time: 252 real time: 252 gc time: 0
4999999950000000
> (time (slow-op 42)) cpu time: 268 real time: 268 gc time: 0
4999999950000000
> (define fast-op (make-caching-proc slow-op)) > (time (fast-op 42)) cpu time: 278 real time: 278 gc time: 3
4999999950000000
> (time (fast-op 42)) cpu time: 0 real time: 0 gc time: 0
4999999950000000
Keep in mind that the cache is only available to external callers of the resulting function. So if proc calls itself recursively, these calls are not accelerated by the cache. If that’s the behavior you need, use define/caching to create a new recursive function.
syntax
(define/caching (name arg ... . rest-arg) body ...)
In the example below, fib is a recursive function. Notice that simply wrapping the function in make-caching-proc doesn’t work in this case, because fib’s recursive calls to itself bypass the cache. But fib-fast is rewritten to recur on the caching function, and the caching works as expected.
> (define (fib x) (if (< x 2) 1 (+ (fib (- x 1)) (fib (- x 2))))) > (define fibber (make-caching-proc fib))
> (define/caching (fib-fast x) (if (< x 2) 1 (+ (fib-fast (- x 1)) (fib-fast (- x 2))))) > (time (fib 32)) cpu time: 72 real time: 72 gc time: 0
3524578
> (time (fibber 32)) cpu time: 86 real time: 86 gc time: 0
3524578
> (time (fib-fast 32)) cpu time: 0 real time: 0 gc time: 0
3524578
3 Coercion
(require sugar/coerce) | package: sugar |
(require (submod sugar/coerce safe)) |
Functions that coerce the datatype of a value to another type. Racket already has type-specific conversion functions. But if you’re handling values of indeterminate type — as sometimes happens in an untyped language — then handling the possible cases individually gets to be a drag.
3.1 Values
Numbers are rounded down to the nearest integer.
Stringlike values — paths, symbols, and strings — are converted to numbers and rounded down.
> (->int "3.5") 3
> (->int '3.5) 3
> (->int (string->path "3.5")) 3
Characters are directly converted to integers.
Lists, vectors, and other multi-value datatypes return their length (using len).
> (->int #t) ->int: contract violation:
expected: intish?
given: #t
argument position: 1st
> (->string "string") "string"
> (->string 'symbol) "symbol"
> (->string 98.6) "98.6"
> (->string (string->path "stdio.h")) "stdio.h"
> (->string #\A) "A"
> (->string #t) ->string: contract violation:
expected: stringish?
given: #t
argument position: 1st
> (->symbol "string") 'string
> (->symbol 'symbol) 'symbol
> (->symbol 98.6) '|98.6|
> (->symbol (string->path "stdio.h")) 'stdio.h
> (->symbol #\A) 'A
> (->symbol #t) ->symbol: contract violation:
expected: symbolish?
given: #t
argument position: 1st
> (->path "string") #<path:string>
> (->path 'symbol) #<path:symbol>
> (->complete-path 98.6) #<path:/home/root/user/.local/share/racket/8.15.0.3/pkgs/sugar/sugar/98.6>
> (->complete-path (string->path "stdio.h")) #<path:/home/root/user/.local/share/racket/8.15.0.3/pkgs/sugar/sugar/stdio.h>
> (->complete-path #\A) #<path:/home/root/user/.local/share/racket/8.15.0.3/pkgs/sugar/sugar/A>
> (->complete-path #t) ->complete-path: contract violation:
expected: complete-pathish?
given: #t
argument position: 1st
Note that a string is treated as an atomic value rather than decomposed with string->list. This is done so the function handles strings the same way as symbols and paths.
> (->list '(a b c)) '(a b c)
> (->list (list->vector '(a b c))) '(a b c)
> (->list (make-hash '((k . v) (k2 . v2)))) '((k . v) (k2 . v2))
> (->list "string") '("string")
> (->list 'symbol) '(symbol)
> (->list (string->path "path")) '(#<path:path>)
> (->list +) '(#<procedure:+>)
> (->vector '(a b c)) '#(a b c)
> (->vector (list->vector '(a b c))) '#(a b c)
> (->vector (make-hash '((k . v) (k2 . v2)))) '#((k . v) (k2 . v2))
> (->vector "string") '#("string")
> (->vector 'symbol) '#(symbol)
> (->vector (string->path "path")) '#(#<path:path>)
> (->vector +) '#(#<procedure:+>)
> (->boolean "string") #t
> (->boolean 'symbol) #t
> (->boolean +) #t
> (->boolean '(l i s t)) #t
> (->boolean #f) #f
procedure
v : any/c
procedure
(stringish? v) → boolean?
v : any/c
procedure
(symbolish? v) → boolean?
v : any/c
procedure
v : any/c
procedure
(complete-pathish? v) → boolean?
v : any/c
procedure
v : any/c
procedure
(vectorish? v) → boolean?
v : any/c
> (map intish? (list 3 3.5 #\A "A" + #t)) '(#t #t #t #t #f #f)
> (map stringish? (list 3 3.5 #\A "A" + #t)) '(#t #t #t #t #f #f)
> (map symbolish? (list 3 3.5 #\A "A" + #t)) '(#t #t #t #t #f #f)
> (map pathish? (list 3 3.5 #\A "A" + #t)) '(#t #t #t #t #f #f)
> (map complete-pathish? (list 3 3.5 #\A "A" + #t)) '(#t #t #t #t #f #f)
> (map listish? (list 3 3.5 #\A "A" + #t)) '(#t #t #t #t #t #t)
> (map vectorish? (list 3 3.5 #\A "A" + #t)) '(#t #t #t #t #t #t)
3.2 Coercion contracts
procedure
(coerce/int? v) → integer?
v : any/c
procedure
(coerce/string? v) → string?
v : any/c
procedure
(coerce/symbol? v) → symbol?
v : any/c
procedure
(coerce/path? v) → path?
v : any/c
procedure
(coerce/boolean? v) → boolean?
v : any/c
procedure
(coerce/list? v) → list?
v : any/c
> (define/contract (add-ints x y) (coerce/int? coerce/int? . -> . any/c) (+ x y)) ; Input arguments will be coerced to integers, then added > (add-ints 1.6 3.8) 4
> (define/contract (int-sum x y) (any/c any/c . -> . coerce/int?) (+ x y)) ; Input arguments will be added, and the result coerced to an integer > (int-sum 1.6 3.8) 5
Please note: this is not an officially sanctioned way to use Racket’s contract system, because contracts aren’t supposed to mutate their values (see make-contract).
But coercion contracts can be useful in two situations:
You want to be liberal about input types, but don’t want to deal with the housekeeping and manual conversions between types.
Your contract involves an expensive operation that you’d rather avoid performing twice.
4 Debug
(require sugar/debug) | package: sugar |
(require (submod sugar/debug safe)) |
Debugging utilities.
For instance, suppose you wanted to see how first-condition? was being evaluted in this expression:
(if (and (first-condition? x) (second-condition? x)) (one-thing) (other-thing))
You can wrap it in report and find out:
(if (and (report (first-condition? x)) (second-condition? x)) (one-thing) (other-thing))
This code will run the same way as before. But when it reaches first-condition?, you willl see in current-error-port:
(first-condition? x) = #t
You can also add standalone calls to report as a debugging aid at points where the return value will be irrelevant, for instance:
(report x x-before-function) (if (and (report (first-condition? x)) (second-condition? x)) (one-thing) (other-thing))
x-before-function = 42
(first-condition? x) = #t
But be careful — in the example below, the result of the if expression will be skipped in favor of the last expression, which will be the value of x:
(if (and (report (first-condition? x)) (second-condition? x)) (one-thing) (other-thing)) (report x)
syntax
(report/line expr)
(report/line expr maybe-name)
syntax
(report/file expr)
(report/file expr maybe-name)
syntax
(report* expr ...)
syntax
(report*/line expr ...)
syntax
(report*/file expr ...)
syntax
(repeat num expr ...)
syntax
(time-repeat num expr ...)
> (time-repeat 1000 (for/product ([i (in-range 1000)]) i) (for/sum ([i (in-range 1000)]) i)) cpu time: 5 real time: 5 gc time: 0
499500
syntax
(time-repeat* num expr ...)
> (time-repeat* 1000 (for/product ([i (in-range 1000)]) i) (for/sum ([i (in-range 1000)]) i))
cpu time: 2 real time: 2 gc time: 0
cpu time: 2 real time: 2 gc time: 0
0
499500
syntax
(compare expr id id-alt ...)
> (define (fib x) (if (< x 2) 1 (+ (fib (- x 1)) (fib (- x 2)))))
> (define/caching (fib-fast x) (if (< x 2) 1 (+ (fib-fast (- x 1)) (fib-fast (- x 2))))) > (compare (time (fib 34)) fib fib-fast)
cpu time: 190 real time: 190 gc time: 0
cpu time: 0 real time: 0 gc time: 0
9227465
9227465
5 File extensions
(require sugar/file) | package: sugar |
(require (submod sugar/file safe)) |
These functions don’t access the filesystem. Warning: these functions adopt the simplifying assumption that the paths are encoded as ASCII or UTF-8. A fully precise treatment of paths would need to handle them as byte strings. If you need that, see the functions in racket/path. This library will remain naive.
Arguments that are pathish? can take either a string or a path. For clarity below, I’ve used strings.
> (get-ext "foo.txt") "txt"
> (get-ext "/path/to/foo.txt") "txt"
> (get-ext "/path/to/foo.txt.bar") "bar"
> (get-ext "/path/to/file-without-extension") #f
> (get-ext "/path/to/directory/") #f
procedure
file-path : pathish? ext : stringish?
> (has-ext? "foo.txt" "txt") #t
> (has-ext? "foo.txt" "TXT") #t
> (has-ext? "foo.txt" "jpg") #f
> (has-ext? "foo.jpg.txt" "jpg") #f
procedure
(remove-ext file-path) → path?
file-path : pathish?
> (remove-ext "foo.txt") #<path:foo>
> (remove-ext "/path/to/foo.txt") #<path:/path/to/foo>
> (remove-ext "/path/to/foo.txt.bar") #<path:/path/to/foo.txt>
> (remove-ext (remove-ext "/path/to/foo.txt.bar")) #<path:/path/to/foo>
procedure
(remove-ext* file-path) → path?
file-path : pathish?
> (remove-ext* "foo.txt") #<path:foo>
> (remove-ext* "/path/to/foo.txt") #<path:/path/to/foo>
> (remove-ext* "/path/to/foo.txt.bar") #<path:/path/to/foo>
> (remove-ext* (remove-ext* "/path/to/foo.txt.bar")) #<path:/path/to/foo>
procedure
file-path : pathish? ext : stringish?
> (add-ext "foo" "txt") #<path:foo.txt>
> (add-ext "foo.txt" "jpg") #<path:foo.txt.jpg>
> (add-ext (remove-ext "foo.txt") "jpg") #<path:foo.jpg>
6 Lists
(require sugar/list) | package: sugar |
(require (submod sugar/list safe)) |
procedure
lst : list? pred : procedure?
> (trimf '(1 2 3 a b c 4 5 6) integer?) '(a b c)
> (trimf '(1 2 3 a b c) integer?) '(a b c)
> (trimf '(a b c) integer?) '(a b c)
> (trimf '(a b c 1 2 3 d e f) integer?) '(a b c 1 2 3 d e f)
procedure
(filter-split lst pred) → (listof list?)
lst : list? pred : procedure?
> (filter-split '(1 a b c 2 d e f 3) integer?) '((a b c) (d e f))
> (filter-split '(1 a b c 2 d e f 3) (negate integer?)) '((1) (2) (3))
procedure
(partition* pred lst) →
list? list? pred : procedure? lst : list?
Same as (values (filter-split lst pred) (filter-split lst (negate pred))), but only traverses the list once.
> (partition* integer? '(1 a b c 2 d e f 3))
'((1) (2) (3))
'((a b c) (d e f))
> (partition* (negate integer?) '(1 a b c 2 d e f 3))
'((a b c) (d e f))
'((1) (2) (3))
> (slice-at (range 5) 1) '((0) (1) (2) (3) (4))
> (slice-at (range 5) 2) '((0 1) (2 3) (4))
> (slice-at (range 5) 2 #t) '((0 1) (2 3))
> (slice-at (range 5) 3) '((0 1 2) (3 4))
> (slice-at (range 5) 5) '((0 1 2 3 4))
> (slice-at (range 5) 5 #t) '((0 1 2 3 4))
> (slice-at (range 5) 100000) '((0 1 2 3 4))
> (slice-at (range 5) 100000 #t) '()
procedure
lst : list? pred : procedure?
> (slicef '(1 2 2 1 2) even?) '((1) (2 2) (1) (2))
> (slicef (range 5) odd?) '((0) (1) (2) (3) (4))
> (slicef (range 5) string?) '((0 1 2 3 4))
procedure
lst : list? pred : procedure? force? : boolean? = #f
If none of the elements match pred, there is no slice to be made, and the result is the whole input list.
> (slicef-at (range 5) even?) '((0 1) (2 3) (4))
> (slicef-at '(1 2 2 1 2) even?) '((1) (2) (2 1) (2))
> (slicef-at '(1 2 2 1 2) even? #t) '((2) (2 1) (2))
> (slicef-at (range 5) odd?) '((0) (1 2) (3 4))
> (slicef-at (range 5) odd? #t) '((1 2) (3 4))
procedure
(slicef-after lst pred [force?]) → (listof list?)
lst : list? pred : procedure? force? : boolean? = #f
If none of the elements match pred, there is no slice to be made, and the result is the whole input list.
> (slicef-after '(1 2 2 1 2) even?) '((1 2) (2) (1 2))
> (slicef-after (range 5) odd?) '((0 1) (2 3) (4))
> (slicef-after (range 5) odd? #true) '((0 1) (2 3))
> (slicef-after (range 5) string?) '((0 1 2 3 4))
procedure
(frequency-hash lst) → hash?
lst : list?
> (frequency-hash '(a b b c c c)) '#hash((a . 1) (b . 2) (c . 3))
> (frequency-hash '(c b c a b c)) '#hash((a . 1) (b . 2) (c . 3))
> (members-unique? '(a b c d e f)) #t
> (members-unique? '(a b c d e f a)) #f
> (members-unique?/error '(a b c d e f)) #t
> (members-unique?/error '(a b c d e f a)) members-unique? failed because item isn't unique: '(a)
> (members-unique?/error '(a b c d e f a b)) members-unique? failed because items aren't unique: '(a b)
syntax
(values->list values)
> (split-at '(a b c d e f) 3)
'(a b c)
'(d e f)
> (values->list (split-at '(a b c d e f) 3)) '((a b c) (d e f))
procedure
lst : list? start-idx : (and/c integer? (not/c negative?)) end-idx : (and/c integer? (not/c negative?))
Bear in mind that sublist is built for convenience, not performance. If you need to do a lot of random access into the middle of an ordered sequence of items, you’d be better off putting them into a vector and using vector-copy.
> (sublist '(0 1 2 3 4 5 6 7 8) 0 8) '(0 1 2 3 4 5 6 7)
> (sublist '(0 1 2 3 4 5 6 7 8) 8 9) '(8)
> (sublist '(0 1 2 3 4 5 6 7 8) 2 5) '(2 3 4)
> (sublist '(0 1 2 3 4 5 6 7 8) 5 2) format: format string requires 0 arguments, given 1;
arguments were: '(5 2)
> (sublist '(0 1 2 3 4 5 6 7 8) 2 10) sublist: ending index 10 exceeds length of list
> (break-at '(0 1 2 3 4 5 6 7 8) 3) '((0 1 2) (3 4 5 6 7 8))
> (break-at '(0 1 2 3 4 5 6 7 8) '(3)) '((0 1 2) (3 4 5 6 7 8))
> (break-at '(0 1 2 3 4 5 6 7 8) '(3 6)) '((0 1 2) (3 4 5) (6 7 8))
> (break-at '(0 1 2 3 4 5 6 7 8) '(3 6 8)) '((0 1 2) (3 4 5) (6 7) (8))
> (break-at '(0 1 2 3 4 5 6 7 8) '(3 6 8 10)) break-at: contract violation
expected: breakpoints not greater than or equal to input
list length = 9
given: '(3 6 8 10)
> (define xs (range 5)) > (shift xs 2) '(#f #f 0 1 2)
> (shift xs -2 0) '(2 3 4 0 0)
> (shift xs 2 'boing) '(boing boing 0 1 2)
> (shift xs 2 'boing #t) '(3 4 0 1 2)
> (shift xs 0) '(0 1 2 3 4)
> (shift xs 42) shift: contract violation
expected: index not larger than list length 5
given: 42
procedure
(shift-left lst how-far [fill-item cycle?]) → list?
lst : list? how-far : integer? fill-item : any/c = #f cycle? : boolean? = #f
> (define xs (range 5)) > (shift-left xs 2) '(2 3 4 #f #f)
> (shift-left xs -2 0) '(0 0 0 1 2)
> (shift-left xs 2 'boing) '(2 3 4 boing boing)
> (shift-left xs 2 'boing #t) '(2 3 4 0 1)
> (shift-left xs 0) '(0 1 2 3 4)
> (shift-left xs 42) shift-left: contract violation
expected: index not larger than list length 5
given: 42
procedure
(shift-cycle lst how-far) → list?
lst : list? how-far : integer?
procedure
(shift-left-cycle lst how-far) → list?
lst : list? how-far : integer?
> (define xs (range 5)) > (shift-cycle xs 2) '(3 4 0 1 2)
> (shift-cycle xs -2) '(2 3 4 0 1)
> (shift-cycle xs 0) '(0 1 2 3 4)
> (shift-cycle xs 42) '(3 4 0 1 2)
> (shift-left-cycle xs 2) '(2 3 4 0 1)
> (shift-left-cycle xs -2) '(3 4 0 1 2)
> (shift-left-cycle xs 0) '(0 1 2 3 4)
> (shift-left-cycle xs 42) '(2 3 4 0 1)
> (define xs (range 5)) > (shifts xs '(-2 2)) '((2 3 4 #f #f) (#f #f 0 1 2))
> (shifts xs '(-2 2) 0) '((2 3 4 0 0) (0 0 0 1 2))
> (shifts xs '(-2 2) 'boing) '((2 3 4 boing boing) (boing boing 0 1 2))
> (shifts xs '(-2 2) 'boing #t) '((2 3 4 0 1) (3 4 0 1 2))
procedure
(shift/values lst how-far [fill-item]) → any
lst : list? how-far : (or/c integer? (listof integer?)) fill-item : any/c = #f
> (define xs (range 5)) > (shift xs 1) '(#f 0 1 2 3)
> (shift/values xs 1)
#f
0
1
2
3
> (shifts xs '(-1 0 1)) '((1 2 3 4 #f) (0 1 2 3 4) (#f 0 1 2 3))
> (shift/values xs '(-1 0 1))
'(1 2 3 4 #f)
'(0 1 2 3 4)
'(#f 0 1 2 3)
7 XML
(require sugar/xml) | package: sugar |
(require (submod sugar/xml safe)) |
Making it easier to do the simplest kind of round-trip with XML: convert an XML string to X-expressions, manipulate, and then convert these X-expressions back to an XML string.
procedure
(xml-string->xexprs xml-string) →
xexpr? xexpr? xml-string : string?
> (define str "<?xml encoding=\"utf-8\"?>\n<root>hello</root>") > (xml-string->xexprs str)
(prolog
(list (p-i (location 1 0 1) (location 1 24 25) 'xml "encoding=\"utf-8\""))
#f
'())
'(root () "hello")
> (define root-only "<root>hello</root>") > (xml-string->xexprs root-only)
(prolog '() #f '())
'(root () "hello")
> (define prolog-only "<?xml encoding=\"utf-8\"?>") > (xml-string->xexprs prolog-only) read-xml: parse-error: expected root element - received
#<eof>
procedure
(xexprs->xml-string prolog-xexpr root-xexpr) → string? prolog-xexpr : xexpr? root-xexpr : xexpr?
> (define str "<?xml encoding=\"utf-8\"?>\n<root>hello</root>") > (define-values (prolog doc) (xml-string->xexprs str)) > prolog
(prolog
(list (p-i (location 1 0 1) (location 1 24 25) 'xml "encoding=\"utf-8\""))
#f
'())
> doc '(root () "hello")
> (xexprs->xml-string prolog doc) "<?xml encoding=\"utf-8\"?>\n<root>hello</root>"
8 License & source code
This module is licensed under the LGPL.
Source repository at http://github.com/mbutterick/sugar. Suggestions & corrections welcome.