Sugar:   readability & convenience library
1 Installation & updates
2 Coercion
2.1 Values
->int
->string
->symbol
->path
->complete-path
->list
->vector
->boolean
intish?
stringish?
symbolish?
pathish?
complete-pathish?
listish?
vectorish?
2.2 Coercion contracts
coerce/  int?
coerce/  string?
coerce/  symbol?
coerce/  path?
coerce/  boolean?
3 Container
get
in?
4 Debug
report
5 File
get-ext
has-ext?
remove-ext
remove-ext*
add-ext
get-enclosing-dir
6 Len
len
7 List
trimf
filter-split
slice-at
slicef-at
frequency-hash
members-unique?
members-unique?/  error
when/  splice
values->list
8 String
starts-with?
ends-with?
capitalized?
9 License & source code
6.1.0.5

Sugar: readability & convenience library

Matthew Butterick <mb@mbtype.com>

 (require sugar) package: sugar

A collection of small functions to help make Racket code simpler & more readable.

1 Installation & updates

At the command line:

raco pkg install sugar

After that, you can update the package from the command line:

raco pkg update sugar

2 Coercion

 (require sugar/coerce) package: sugar

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.

2.1 Values

procedure

(->int v)  integer?

  v : any/c
Convert v to an integer in the least surprising way, or raise an error if no conversion is possible.

Numbers are rounded down to the nearest integer.

Examples:

> (->int 3)

3

> (->int 3.5)

3

> (->int -2.5)

-3

> (->int (+ 3 (/ 1 2)))

3

Stringlike values — paths, symbols, and strings — are converted to numbers and rounded down.

Examples:

> (->int "3.5")

3

> (->int '3.5)

3

> (->int (string->path "3.5"))

3

Characters are directly converted to integers.

Examples:

> (->int #\A)

65

> (->int #\◊)

9674

Lists, vectors, and other multi-value datatypes return their length (using len).

Examples:

> (->int (list 5 6 7))

3

> (->int (hash 'a 1 'b 2 'c 3))

3

The function will raise an error if no sensible conversion is possible.

Example:

> (->int #t)

Can’t convert #t to integer

procedure

(->string v)  string?

  v : any/c
Return the most natural string representation of v, or raise an error if none exists.

Examples:

> (->string "string")

"string"

> (->string 'symbol)

"symbol"

> (->string 98.6)

"98.6"

> (->string (string->path "stdio.h"))

"stdio.h"

> (->string #\A)

"A"

> (->string #t)

Can’t convert #t to string

procedure

(->symbol v)  symbol?

  v : any/c
Same as ->string, but return a symbol rather than a string.

Examples:

> (->symbol "string")

'string

> (->symbol 'symbol)

'symbol

> (->symbol 98.6)

'|98.6|

> (->symbol (string->path "stdio.h"))

'stdio.h

> (->symbol #\A)

'A

> (->symbol #t)

Can’t convert #t to symbol

procedure

(->path v)  path?

  v : any/c

procedure

(->complete-path v)  complete-path?

  v : any/c
Same as ->string, but return a path (or complete path) rather than a string.

Examples:

> (->path "string")

#<path:string>

> (->path 'symbol)

#<path:symbol>

> (->complete-path 98.6)

#<path:/Users/MB/git/sugar/98.6>

> (->complete-path (string->path "stdio.h"))

#<path:/Users/MB/git/sugar/stdio.h>

> (->complete-path #\A)

#<path:/Users/MB/git/sugar/A>

> (->complete-path #t)

Can’t convert #t to complete-path

procedure

(->list v)  list?

  v : any/c
If v is a listlike data type — a vector, set, stream, sequence, or list — convert it to a list. A hash or dictionary becomes a list using dict->list. If v is an atomic value, turn it into a single-member list.

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.

Examples:

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

procedure

(->vector v)  vector?

  v : any/c
Same as ->list, but returns a vector rather than a list.

Examples:

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

procedure

(->boolean v)  boolean?

  v : any/c
Return #t for all v except #f, which remains #f.

Examples:

> (->boolean "string")

#t

> (->boolean 'symbol)

#t

> (->boolean +)

#t

> (->boolean '(l i s t))

#t

> (->boolean #f)

#f

procedure

(intish? v)  boolean?

  v : any/c

procedure

(stringish? v)  boolean?

  v : any/c

procedure

(symbolish? v)  boolean?

  v : any/c

procedure

(pathish? v)  boolean?

  v : any/c

procedure

(complete-pathish? v)  boolean?

  v : any/c

procedure

(listish? v)  boolean?

  v : any/c

procedure

(vectorish? v)  boolean?

  v : any/c
Predicates that report whether v can be coerced to the specified type.

Examples:

> (map intish? (list 3 3.5 #\A "A" + #t))

'(#t #t #t #f #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)

2.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
If v can be coerced to the specified type, change it to that type, then return it. If not, raise the usual contract error. These contracts can be used with input or output values.

Examples:

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

3 Container

 (require sugar/container) package: sugar

Type-neutral functions for getting elements out of a container, or testing membership.

procedure

(get container which [end_which])  any/c

  container : (or/c list? vector? sequence? dict? string? symbol? path?)
  which : any/c
  end_which : (or/c (and/c integer? positive?) #f) = #f
For a container that’s a dict?, retrieve the element associated with the key which. Raise an error if the key doesn’t exist.

Examples:

> (get (make-hash '((a . 1) (b . 2) (c  . 3))) 'b)

2

> (get (make-hash '((a . 1) (b . 2) (c  . 3))) 'z)

get: couldn’t retrieve item z from #hash((c . 3) (b . 2) (a

. 1))

For other container types — which are all sequence-like — retrieve the element located at which. Or if the optional end_which argument is provided, retrieve the elements from which to (sub1 end_which), inclusive (i.e., make a slice). Raise an error if which or end_which is out of bounds.

Examples:

> (get '(0 1 2 3 4 5) 2)

2

> (get '(0 1 2 3 4 5) 2 4)

'(2 3)

> (get '(0 1 2 3 4 5) 100)

get: couldn’t retrieve item 100 from (0 1 2 3 4 5)

> (get '(0 1 2 3 4 5) 2 100)

get: couldn’t retrieve items 2 through 100 from (0 1 2 3 4

5)

> (get (list->vector '(0 1 2 3 4 5)) 2)

2

> (get (list->vector '(0 1 2 3 4 5)) 2 4)

'#(2 3)

> (get "purple" 2)

"r"

> (get "purple" 2 4)

"rp"

> (get 'purple 2)

'r

> (get 'purple 2 4)

'rp

When container is a path, it’s treated as a list of path elements (created by explode-path), not as a stringlike value.

Examples:

> (get (string->path "/root/foo/bar/file.txt") 1)

#<path:root>

> (get (string->path "/root/foo/bar/file.txt") 0 3)

'(#<path:/> #<path:root> #<path:foo>)

To slice to the end of container, use (len container) as the value of end_which.

Examples:

> (define xs '(0 1 2 3 4 5))
> (get xs 2 (len xs))

'(2 3 4 5)

> (get (list->vector xs) 2 (len (list->vector xs)))

'#(2 3 4 5)

> (define color "purple")
> (get color 2 (len color))

"rple"

procedure

(in? item container)  boolean?

  item : any/c
  container : (or/c list? vector? sequence? set? dict? string? symbol? path?)
Return #t if item is in container, or #f otherwise.

Examples:

> (in? 2 '(0 1 2 3 4 5))

#t

> (in? 'a '(0 1 2 3 4 5))

#f

> (in? 2 (list->vector '(0 1 2 3 4 5)))

#t

> (in? "pu" "purple")

#t

> (in? "zig" "purple")

#f

> (in? 'b (make-hash '((a . 1) (b . 2) (c  . 3))))

#t

> (in? 'z (make-hash '((a . 1) (b . 2) (c  . 3))))

#f

As with get, when container is a path, it’s treated as a list of exploded path elements, not as a stringlike value.

Examples:

> (in? "foo" (string->path "/root/foo/bar/file.txt"))

#t

> (in? "zam" (string->path "/root/foo/bar/file.txt"))

#f

4 Debug

 (require sugar/debug) package: sugar

Debugging utilities.

syntax

(report expr)

Print the name and value of expr to current-error-port, but also return the evaluated result of expr as usual. This lets you see the value of an expression or variable at runtime without disrupting any of the surrounding code.

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)
(if (and (report (first-condition? x)) (second-condition? x))
  (one-thing)
  (other-thing))

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

5 File

 (require sugar/file) package: sugar

File utilities, mostly in the realm of file extensions. These functions don’t access the filesystem.

Arguments that are pathish? can take either a string or a path. For clarity below, I’ve used strings.

procedure

(get-ext file-path)  (or/c #f string?)

  file-path : pathish?
Return the last file extension of file-path as a string, or #f if it has no extension. Omit the intervening . separator.

Examples:

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

(has-ext? file-path ext)  boolean?

  file-path : pathish?
  ext : stringish?
Return #t if the last file extension of file-path is ext, otherwise #f.

Examples:

> (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 the last file extension of file-path, and return the path that remains. If file-path has no extension, you just get the same file-path. Does not use the filesystem.

Examples:

> (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?
Like remove-ext, just more. Remove all file extensions from file-path, and return the path that remains. If file-path has no extensions, you just get the same file-path. Does not use the filesystem.

Examples:

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

(add-ext file-path ext)  path?

  file-path : pathish?
  ext : stringish?
Return a new file-path with ext appended. Note that this does not replace an existing file extension. If that’s what you want, then do (add-ext (remove-ext file-path) ext).

Examples:

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

procedure

(get-enclosing-dir path)  path?

  path : pathish?
Return the enclosing directory of path. Does not consult the filesystem about whether path is valid. If you reach the root directory, then (get-enclosing-dir root) will just return root again.

Examples:

> (define bin (string->path "/usr/bin"))
> bin

#<path:/usr/bin>

> (get-enclosing-dir bin)

#<path:/usr/>

> (get-enclosing-dir (get-enclosing-dir bin))

#<path:/>

> (get-enclosing-dir (get-enclosing-dir (get-enclosing-dir bin)))

#<path:/>

6 Len

 (require sugar/len) package: sugar

procedure

(len x)  integer?

  x : (or/c list? vector? set? string? symbol? path? hash?)
Calculate the length of x in the least surprising way possible, or if it can’t be done, raise an error. Named in honor of the original discoverer of the length-reticulation algorithm, Prof. Leonard Spottiswoode.

Examples:

> (len '(a b c))

3

> (len (list->vector '(a b c)))

3

> (len 'abc)

3

> (len "abc")

3

> (len (string->path "abc"))

3

> (len (make-hash `((a . 1)(b . 2)(c . 3))))

3

Perhaps ironically, positive integers do not have a length.

Example:

> (len 3)

len: can’t calculate length of 3

7 List

 (require sugar/list) package: sugar

procedure

(trimf lst pred)  list?

  lst : list?
  pred : procedure?
Drop elements from each end of lst that satisfy pred. Exactly equivalent to (dropf-right (dropf lst pred) pred).

Examples:

> (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?
Like string-split, but for lists. Drop elements from anywhere in lst that satisfy pred — ends, middle, you name it — and return a list of the sublists that remain.

Examples:

> (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) (compose not integer?))

'((1) (2) (3))

> (filter-split '(a b c 1 2 3 d e f) integer?)

'((a b c) (d e f))

> (filter-split '(a b c 1 2 3 d e f) (compose not integer?))

'((1 2 3))

procedure

(slice-at lst len [force?])  (listof list?)

  lst : list?
  len : (and/c integer? positive?)
  force? : boolean? = #f
Divide lst into sublists of length len. If lst cannot be divided evenly by len, the last sublist will be shorter. If this displeases you, set force? to #t and a stumpy final sublist will be ignored.

Examples:

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

(slicef-at lst pred [force?])  (listof list?)

  lst : list?
  pred : procedure?
  force? : boolean? = #f
Divide lst into sublists starting with elements matching pred. The first element of the first sublist may not match pred. Or, if you really & truly want only the sublists starting with an element matching pred, set force? to #t.

Examples:

> (slicef-at (range 5) even?)

'((0 1) (2 3) (4))

> (slicef-at (range 5) odd?)

'((0) (1 2) (3 4))

> (slicef-at (range 5) odd? #t)

'((1 2) (3 4))

procedure

(frequency-hash lst)  hash?

  lst : list?
Count the frequency of each element in lst, and return a hash whose keys are the unique elements of lst, and each value is the frequency of that element within lst.

Examples:

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

procedure

(members-unique? container)  boolean?

  container : (or/c list? vector? string?)
Return #t if every element in container is unique, otherwise #f.

Examples:

> (members-unique? '(a b c d e f))

#t

> (members-unique? '(a b c d e f a))

#f

procedure

(members-unique?/error container)  boolean?

  container : (or/c list? vector? string?)
Same as members-unique?, but if the members are not unique, raises a descriptive error rather than returning #f.

Examples:

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

(when/splice test expr)

A special version of when that you can use inside quasiquote to suppress void values when test is #f. As the name suggests, it works in conjunction with the @ splicing operator.

Examples:

> `(,(when (even? 2) "hooray"))

'("hooray")

> `(,(when (even? 3) "hooray"))

'(#<void>)

> `(,@(when/splice (even? 2) "hooray"))

'("hooray")

> `(,@(when/splice (even? 3) "hooray"))

'()

syntax

(values->list values)

Convert values to a simple list.

Examples:

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

8 String

 (require sugar/string) package: sugar

procedure

(starts-with? str starter)  boolean?

  str : stringish?
  starter : stringish?
Return #t if str starts with starter, otherwise #f.

Examples:

> (starts-with? "foobar" "foo")

#t

> (starts-with? "foobar" "foobar")

#t

> (starts-with? "foobar" "zam")

#f

> (starts-with? "foobar" "foobars")

#f

procedure

(ends-with? str ender)  boolean?

  str : stringish?
  ender : stringish?
Return #t if str ends with ender, otherwise #f.

Examples:

> (ends-with? "foobar" "foo")

#f

> (ends-with? "foobar" "foobar")

#t

> (ends-with? "foobar" "zam")

#f

> (ends-with? "foobar" "foobars")

#f

procedure

(capitalized? str)  boolean?

  str : stringish?
Return #t if str starts with a capital letter, otherwise #f.

Examples:

> (capitalized? "Brennan")

#t

> (capitalized? "Brennan stinks")

#t

> (capitalized? "stinks")

#f

9 License & source code

This module is licensed under the LGPL.

Source repository at http://github.com/mbutterick/sugar. Suggestions & corrections welcome.