RUTILS is split into two parts: core (package rutils) and contrib (package rutilsx). These are aggregate packages that just re-export the symbols that are provided by the specific packages like rutils.anaphora or rutils.list. Overall, there are 17 parts of the core, which are described, in more detail, in this tutorial. They include (with some changes and additions) 3 facilities, which are also available from separate libraries: SPLIT-SEQUENCE, ITERATE, and ANAPHORA. Besides, it defines 2 lightweight wrapper data structures: pair and hash-set.
There's also the package rtl that includes the core plus short names for a lot of basic Lisp operations.
Contrib holds "experimental" stuff (in the sense that it's not totally conventional even for me) that, gradually, migrates to core. I won't talk more about it in the tutorial: those who are interested can check on their own or ask questions.
RUTILS is mainly intended to be used: either the package rutils/rtl or a particular package like rutils.hash-table. However, for easy REPL-based workflows, there are also packages rutils-user/rtl-user/rutilsx-user which are cl-user plus the appropriate RUTILS package.
The library is available from Quicklisp. The simplest sequence to get started, from the REPL, is:
CL-USER> (ql:quickload :rutils)
CL-USER> (in-package :rtl-user)
CL-USER> (named-readtables:in-readtable rutils-readtable)
If you want to work with the most current version (for instance, the latest 5.0 release isn't in Quicklisp yet) you can grab it from github. The recommended way is to put it in your ~/common-lisp/ folder. Afterwards, it will be automatically found by ql:quickload. Yet, if you want to be sure (or if you put it into a different folder) you may precede the quickload call with the following:
CL-USER> (push "~/common-lisp/rutils/" asdf:*central-registry*) ; or use the path where you've put rutils
The major external dependency of RUTILS is named-readtables, which facilitates managing of readtables (that are defined by the Lisp standard but don't have adequate high-level API there). Essentially, readtables are similar to packages, but, instead of providing new names, they provide new reader-macros i.e. additional syntax. named-readtables allows using readtables in the same manner as packages: there's in-readtable that acts similar to in-package and defreadtable that is, to some extent, an analog of defpackage. You can learn more on these from the library docs.
RUTILS defined the following syntactic extensions available from rutils-readtable.
- Literal lambdas
Clojure has introduced the idea of shorthand syntax for anonymous functions with predefined argument names. RUTILS provides 2 interchangeable versions of such syntax:
RTL-USER> ^(+ % %%)
#<FUNCTION (LAMBDA (&OPTIONAL % %%)) {1022395A6B}>
and
RTL-USER> #`(+ % %%)
#<FUNCTION (LAMBDA (&OPTIONAL % %%)) {1022395BAB}>
Unlike Clojure, only 2 implicit arguments are supported: % and %%. This covers more than 80% of use cases. And, think of it, when you have a lambda with more than 2 arguments, most probably, it's not so simple to be written in shorthand form. After all, it's supposed that these small functions should span a single expression and at most 3 lines of code...
Although, multiple-expression lambdas are also supported: ^((print %) (1+ %%)) => (lambda (&optional % %%) (print %) (1+ %%))
- Literal hash-tables
The ability to initialize the hash-table with values is essential to maintaining code declarativeness. Once again, similar to Clojure, Python, Ruby, and other languages provide it. So, we also do, and, also in 2 variants.
Originally, I came up with the #{} notation that should be very familiar to users of other languages:
RTL-USER> #{:a 1 :b 2}
#<HASH-TABLE :TEST EQL :COUNT 2>
;; holding 2 key/value pairs: ((:a . 1) (:b . 2))
RTL-USER> #{equal "a" 1 "b" 2}
#<HASH-TABLE :TEST EQUAL :COUNT 2>
;; holding 2 key/value pairs: (("a" . 1) ...)
However, it doesn't play very well with Lisp code formatting, in particular, in Emacs, so an alternative paren-based syntax was also added: `#h().
RTL-USER> #h(:a 1 :b 2)
#<HASH-TABLE :TEST EQL :COUNT 2>
RTL-USER> #h(equal "a" 1 "b" 2)
#<HASH-TABLE :TEST EQUALP :COUNT 2>
- Literal dynamic vectors
Lisp provides literal syntax for vectors: #(). Unfortunately, it creates only constant vectors, but vectors with variable contents are much more widely used. For this purpose, we introduce #v() — similar to #h() for hash-tables. It creates a non-adjustable normal vector. If you want the vector to also be adjustable use vec (from rutils.array). So, now there are 3 ways to easily create and initialize a vector in a declarative manner:
RTL-USER> #(1 2 3)
#(1 2 3)
RTL-USER> #v(1 2 3)
#(1 2 3)
RTL-USER> (vec 1 2 3)
#(1 2 3)
Don't be fooled by the same literal representation, though :D
- Dot notation for accessing struct/object slots
The absence of shorthand access to object fields was always one of the common complaints of outsiders. Now, you can show them RUTILS :)
We suggest the following syntax: @object.slot#index. It requires prepending of the object with @ (like it's done for variables in such languages like Perl or PHP). The dot is expanded into smart-slot-value (see below) and # into elt. So, this syntax covers both dot access and []-style array element access.
Compare (elt (nth 1 (foo-slot2 (bar-slot1 obj)) 0) (sometimes you have to write similar things) with @obj.slot1.slot2#1#0. May be considered cryptic and unlispy. But may be much easier to read also...
- Heredoc strings
Heredoc is a concept from the Shell and Perl that allows representing strings without the need for escaping. The variant RUTILS provides uses the open-close syntax: #/ ... /#.
RTL-USER> #/This is a string
which allows using the quote (") without escaping/#
"This is a string
which allows using the quote (\") without escaping"
It might be very useful for docstrings, to represent JSON and other documents, etc.
Note, that it may not always play well with some REPL (e.g. SLIME), which count open/close quotes.
The package rutils.symbol provides the basic utilities for manipulating symbols that are also used in most of the other packages.
First of all, it has the classic:
with-gensyms/with-unique-names/w/uniqs- and
once-only
If you don't know how these are used you can read more in PCL or On Lisp.
eval-alwaysiseval-whenwith all 3 modes onensure-symbol/mksymandensure-keyword/mkeywturn a string into a symbol or keyword (first upcasing it). It has a keyword:formatargument that allows specifying a format-string for the symbol transformation
Finally, it provides abbr that allows to easily define aliases for any functions and macros.
This package holds a number of random utilities that didn't fit into any other group.
It includes some logic shorthands:
trueis the complement ofnullxorand2,or2, andxor2are 2-element functions that may be used as:testarguments (usualandandorcan't)more/not-moreandless/not-lessare like>/>=/</<=but allow some arguments to be null
Clojure-style threading macros (-> and ->>):
RTL-USER> (-> 2
1+
(expt % 2))
9
And a function composition operator (=>): (=> fn1 fn2) is the same as (fn1 (fn2 x)).
There are also 2 case-like control-flow constructs:
pcase,dcaseswitch,cswitch,eswitch
And a couple of shorthands I use quite often:
voidis(setf x nil). But it seems much more semantic :)2ndis(nth-value 1 x)
Finally,
named-lambda/fnallows to created named anonymous functions, for what it's worth :) Their main use-case is improved debuggingcoercefcoerces and re-assigns the variable (there's also a similarappendffor lists)
There's a separate Lisp library called ANAPHORA that provides such control-flow constructs that allow performing simultaneous assignment and logical tests. RUTILS copies the basic and most widely-used ones. when-it and if-it are among the most used extensions for me. Here's an example:
(when-it (foo)
(bar it))
is the same as:
(let ((it (foo)))
(when it
(bar it)))
This is the implicit anaphora, in which it variable is created behind-the-scenes. Such an approach is not unseen in Lisp, as there's call-next-method that follows the same principle. Yet, RUTILS supports an explicit style, as well:
(if-let (x (foo))
(bar x)
(baz))
is the same as:
(let ((x (foo)))
(if x
(bar x)
(baz)))
Other supplied anaphoric macros are:
cond-it/cond-letdowhile-it/dowhile-let
The names should speak for themselves.
This library provides an extensible unification of all Lisp binding operations (let, let*, multiple-value-bind, destructuring-bind, with-slots,...) — bind/with (I prefer the second variant). Unlike iter or anaphora, I didn't take some of the existing binding libraries (e.g. METABANG-BIND) and decided to implement a novel version that provides more intuitive syntax for the common operations: primarily, multiple values, key-based and slot access.
Here's an example:
RTL-USER> (with ((a 1) ; => plain let
(b c (values 2 3)) ; more than 2 variables => multiple-value-bind
((d _ e &rest f) '(4 0 5 12 13 14 15)) ; a list => destructuring-bind
(((g :g) (h :h)) ? #h(:g 6 :h 7)) ; ? in 2nd position => generic-elt
((i j) @ (make-foo :i 8 :j 9)) ; @ in 2nd position => smart-slot-value
(((k k) (l l)) @ (make 'foo :k 10 :l 11)))
(list* a b c d e g h i j k l f)))
(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15)
It also supports ignoring some of the bindings by putting _ instead of the variable name — like in the third clause (destructuring-bind).
It is extensible as it allows to add more variants that support custom clauses. This is defined in bind-dispatch that uses the first 2 arguments to decide, which operation to emit. Let's define a binding clause for with-open-file that triggers when the second argument is a pathname:
(defmethod bind-dispatch ((arg1 list) (arg2 pathname) &rest args)
`(with-open-file (,arg1 ,arg2 ,@args)))
RTL-USER> (with ((str "hello world")
(out #p"/tmp/foo.txt" :direction :output))
(write-line str out))
error opening #P"/tmp/foo.txt": File exists
This package provides the generic accessor generic-elt with a shorter alias ? and setter generic-setf (also (setf (? ...). Also, generic copy function is defined and generic items count (or length, or size) — tally.
RTL-USER> (defstruct foo bar)
(? (make-foo :bar #h(:baz '(0 1 42)))
'bar :baz 2)
42
Access to struct/object slots is performed using smart-slot-value, also defined here. It is an enhanced version of the standard slot-value, which searches by the symbol name regardless of the package (using CLOSER-MOP machinery and caching the found accessor). So, (? (make-foo :bar 42) 'bar) and (? (make-foo :bar 42) :bar), and even (? (make-foo :bar 42) 'foo::bar) will yield the same result 42.
Alongside SPLIT-SEQUENCE, this is another external library originally called ITERATE that is incorporated by RUTILS. With an important modification of defining the clause keys in the keyword package. You can refer to the official documentation for the extensive examples of its usage. Just mind that instead of:
(iterate (for item in list)
(finding elt maximizing (length elt)))
you'll have to write:
(iter (:for item :in list)
(:finding elt :maximizing (length elt)))
Using keywords not only makes the iteration keys more visible, in the code, but also prevents name conflicts with the :common-lisp package.
Here, you will find some short aliases for common long/antiquetely-named Lisp operations.
These are the most impactful (and so controversial) ones:
:=forsetf(yes, it's using the syymbol from the keyword package, and the sky doesn't fall because of that):+/:-/:*/:/are the emoji aliases ofincf/decfand similar operations if they had been defined for multiplication and divisionmakeformake-instance(now, there's nice symmetry betweenmake 'objectandmake-struct)callforfuncall(it was fun to call functions by their symbol-name in the 60's but it has become the norm now)
These two are, primarily, REPL-oriented:
defparfordefparameterm1formacroexpand-1
And some more basic Lisp operations:
justforidentityds-bindfordestructuring-bindmv-bindformultiple-value-bind(although, simplebindand its recommended aliaswithis even shorter :)flet*forlabelssubforsubseqpushxforvector-push-extend
I have to say, I don't use all of these versions myself, but it's, at least, worth considering that not all standard Lisp names are optimal and the search for better ones should continue...
This package provides the pair struct that is intended to serve as a replacement for cons-cell. There are several reasons for its introduction:
- using
.for literal conses is unlispy and also prevents from using it in DSLs where it's often needed (unfortunately,paircan only slightly help here, but, still, if we know that we won't have to use conses, we can resort to ugly hacks to put the dot character to other use) - more semantic naming that is more understandable to newcomers (
carandcdr, unfortunately, are used to scare some people off Lisp)
The accessor functions are pair-left/lt and pair-right/rt. And the pair may be created by both calling (make-pair :left :foo :right 42) and a shorter (pair :foo 42).
pair is a list-based struct, which allows it to be used in destructuring similar to cons. At the drawback of somewhat worse performance (another dereference is needed to access the right part). So, if you need to squeeze the last bit of performance, in someplace, you might still need to use cons-pairs. There's also with-pair to destructure a single pair:
RTL-USER> (with-pair (l r) (pair :foo 42)
(cons l r))
(:FOO . 42)
The utility functions to transform a list of pairs to/from a hash-table are provided:
pairs->htht->pairs
This package accumulates many list-manipulation utilities from On Lisp, other books, and beyond. After all, the name Lisp originally meant "List Processing", and a lot of stuff was invented to make such manipulation more efficient. But not all of it got into the standard.
last1provides read-write access to the last element. It should be noted that it isn't efficient (O(n)) hence, possibly, its absence from the standardassoc1is the classic missing shortcut to not have to write(cdr (assoc ...)); it also works for pairssingle/dyadic/triadiccheck if list has exactly 1,2 or 3 elementsensure-list/mklistturns atom arguments into lists while preserving list arguments as is;atomizeis the opposite operationflattenturns the list into absolutely flat (removing all nesting) or removes just the number of levels supplied as the optional second argument
RTL-USER> (flatten '((1 (2 3)) 4) 1)
(1 (2 3) 4)
RTL-USER> (flatten '((1 (2 3)) 4)) ; here, same as 2 levels
(1 2 3 4)
interleaveturns a number of lists into 1 by interleaving their elementsinterposeadds a separator argument after each list's element
RTL-USER> (interleave '(1 2 3) '(4 5 6) '(:foo :bar :baz))
(1 4 :FOO 2 5 :BAR 3 6 :BAZ)
RTL-USER> (interpose :foo '(1 2 3))
(1 :FOO 2 :FOO 3)
take efficiently and without null-pointer exceptions returns a list of the first n (or less, if the original is shorter) elements of the list
Plist- and alist-related stuff:
plistpandalistpcheck if the appropriate conditions for a list being a plist or alist are met; there's alsoalist-to-plistandplist-to-alist- there's also
remove-from-plistanddelete-from-plist - finally, there's
doplistwhich iterates the plist by one key-value pair, at once
appendf/nconcf/unionf/nunionf/reversef/nreversef assign the result of the appropriate operation to the variable passed as their argument
set-equal compares 2 sets represented as lists for equality
zip/zip-with and zip*/zip*-with are like mapcar by list or an arbitrary function. Where zip finishes when at least one of the input lists ends, while zip* waits until the last one ends substituting the missing elements with nils.
RTL-USER> (zip '(1 2 3) '(4 5 6))
((1 4) (2 5) (3 6))
RTL-USER> (zip-with '+ '(1 2 3) '(4 5 6))
(5 7 9)
RTL-USER> (zip-with '+ '(1 2 3) '(4 5))
(5 7)
RTL-USER> (zip*-with '+ '(1 2 3) '(4 5))
; Evaluation aborted on #<TYPE-ERROR expected-type: NUMBER datum: NIL>.
maptimes is the mapping version of dotimes:
RTL-USER> (maptimes 10 '1+)
(1 2 3 4 5 6 7 8 9 10)
mapindex iterates the list and keeps track of the current position in it (there's also mapcanindex):
RTL-USER> (mapindex (lambda (i x)
(print (pair i x)))
'(:foo :bar :baz))
(0 :FOO)
(1 :BAR)
(2 :BAZ)
((0 :FOO) (1 :BAR) (2 :BAZ))
mappend/mapconcat or, better, flat-map is the non-destructive alternative to mapcan.
range produces a range of numbers from start to limit with an optional step.
concat is the shorthand for (concatenate 'list ...).
dcons and dlistp are for working with Ron Garret's D-Lists.
The two main utilities here are:
sliceperforms efficient vector subsequencing (also works for strings) by using array-displacementveccreates a dynamic adjustable vector from its arguments
There's also dovec wich iterates the elements of the vector — a more direct way than using dotimes + aref.
This package provides a number of very frequently used string utilities and also some things to make basic file-to-string/string-to-file operations more accessible.
String utilities:
blankptests whether the string is empty (i.e.(zerop (length string))strcatconcatenates strings, as well as characters and, actually, anything else that prints to string, including nil (yep, the name is taken from C)strjoinis a shortcut for(format nil "~{~A~^<delimiter>~}" ...)substris an efficient substring implementation (based onslice, i.e. using displaced arrays) that also allows using negative indices, like in Python, which signify index from the end of the stringwhite-char-ptests whether the character is one of the 5 basic whitespace-characters (#\Space,#\Tab,#\Newline,#\Return,#\Linefeed)starts-withandends-withare more intuitive wrappers aroundmismatchlast-charreturns the last character of the given stringfmtis the shortcut forformat nil
RTL-USER> (strcat "foo" nil "bar" #\4 #\2)
"foobar42"
RTL-USER> (strjoin #\Space '("foo" nil :bar 42))
"foo BAR 42"
File-related stuff (very commonly used by me):
read-file/slurpreads file contents into a stringwith-out-fileopens a file for writing with the most default parameters::if-exists :supersede :if-does-not-exist :create
This package contains the very popular split-sequence family of utilities (it's recommended to utilize it using the shorter split/split-if/split-if-not names), with a minor, but impactful change of the default value of remove-empty-subseqs argument to t (for me, it's the value I have to provide in 99% of the cases). More docs on split-sequence can be found on the internet, in particular, on Cliki.
Also provided is the partition-with function that splits the sequence into groups by a list of delimiters:
RTL-USER> (partition-with '(1 5 10) (range 0 20) :test '<=)
((0 1) (2 3 4 5) (6 7 8 9 10))
(1 5 10)
While group just splits it into groups of the supplied length.
removef/deletef are, basically, aliases for (setf x (remove x ... and the same with delete.
doindex iterates the sequence keeping track of the index of the current element (it coerces the argument to a vector). Compare it with mapindex from rutils.list.
RTL-USER> (doindex (i item '(:foo :bar :baz))
(print (pair i item)))
(0 :FOO)
(1 :BAR)
(2 :BAZ)
shuffle creates an algorithmically properly shuffled copy of the sequence and nshuffle does that in-place.
rotate returns a copy with elements shifted by n:
RTL-USER> (rotate '(:foo :bar :baz) 2)
(:BAZ :FOO :BAR)
Efficient length comparison is performed by the following utilities:
emptyptests if the sequence has length zero.length=checks if the sequence length is exactly the second argument- and
equal-lengthstests if the sequences have the same lengths
last-elt retrieves the last element (also setfable).
safe-sort is normal sort that copies the input sequence prior to its application (the normal sort in-place behavior was a source of many hard-to-find bugs, for me)
keep/keep-if/keep-if-not are the complements to remove/remove-if/remove-if-not.
sum/product reduce a sequence of numbers to a number by adding/multiplying them.
And map* is a DWIM version of map. It doesn't ask for the result type and just uses the type of its first argument.
Hash-tables have, sadly, very rudimentary API, in the standard, despite their current ubiquity. I guess, at the time they were introduced, there the understanding of their universal utility hadn't been formed yet. So, RUTILS bridges that gap.
Alongside the reader support, provided by the readtable, literal syntax is also supported by the printer with print-hash-table/print-ht. Besides, there's also a way to plug into normal printing, with toggle-print-hash-table:
RTL-USER> #h(:foo 42)
#<HASH-TABLE :TEST EQL :COUNT 1 {10251850A3}>
RTL-USER> (toggle-print-hash-table)
WARNING:
redefining PRINT-OBJECT (#<STRUCTURE-CLASS COMMON-LISP:HASH-TABLE>
#<SB-PCL:SYSTEM-CLASS COMMON-LISP:T>) in DEFMETHOD
T
RTL-USER> #h(:foo 42)
#{
:FOO 42
}
RTL-USER> (toggle-print-hash-table)
NIL
RTL-USER> #h(:foo 42)
#<HASH-TABLE :TEST EQL :COUNT 1 {10251F5F43}>
Some additional operations were added:
sethashis a shorthand for(setf (gethash ...))takehashis likeremhash, but it also returns the removed elementgetsethasheither gets the value from the table when it's present there or sets it and returns the newly set value; the setting is performed on-demand and no calculation is performed if the value is already there
Iterating a hash-table with maphash or loop :for keys :beign :the ... is not the most natural way. A do-style macro is missing, so it was introduced as dotable:
RTL-USER> (let ((rez ()))
(dotable (k v #h(1 :foo 2 :bar) rez)
(when (oddp k)
(push v rez))))
(:FOO)
It supports automatic ignoring of _ argument and, also, iterating over alists.
Another idea I had was to distinguish hash-table-related operations with a hash-sign at the end. It brought to the definition of a group of alternative hash-table operations:
get#-gethashset#-sethashgetset#-getsethashtake#-takehashin#- a new operation, which is a shorthand for(2nd (gethash ...p#-print-hash-table
Another important missing piece of hash-table UX was easy access to all the keys/vals. It is covered by hash-table-keys/ht-keys and hash-table-vals/ht-vals.
Besides, alongside pairs->ht/ht->pairs there's also:
hash-table-to-alist/ht->alistandhash-table-from-alist/alist->hthash-table-to-plist``ht->plistandhash-table-from-plist/plist->ht
Finally, is also sometimes needed merge-hash-tables/merge-hts, merge-hash-tables-with/merge-hts-with.
From the efficiency standpoint, hash-sets should be the goto set implementations, but, in Lisp, it seems like sets where introduced prior to hash-table so the simpler list-based variants are supported in the standard. Here, we provide a hash-based alternative based on the hash-table with all t values:
hash-set will create a set from a list of items (hash-table-test may be specified).
All the operations end in # similar to the hash-table aliases:
add#adds an item to the set (and the hash-tablerem#can be used to remove it)inter#/union#/diff#/xor#are the basic set-theoretic operations
Example usage of a hash-set:
RTL-USER> (let ((h1 (hash-set 'eql :foo))
(h2 (hash-set 'eql :bar)))
(format t "~&Original hash-set:~%")
(p# h1)
(add# :foo h1)
(format t "~&After item addition:~%")
(p# h1)
(format t "~&Union with another:~%")
(p# (union# h1 h2))
(format t "~&Intersect with another:~%")
(p# (inter# h1 h2)))
Original hash-set:
#{
:FOO T
}
After item addition:
#{
:FOO T
}
Union with another:
#{
:FOO T
:BAR T
}
Intersect with another:
#{
}
Key-values are more than just hash-tables and alists (although, these are the most wide-spread ones, in the Lisp world). A generic kv-access protocol is defined here: keys and vals allows to define methods to access exactly those, kvs returns key-value pairs as a list (possible flavors are: plist, alist, and dlist).
Besides, there's a generic mapkv, which iterates a single key-value structure and returns a similar structure with the calculated values assigned to the same keys, and dokv, which uses mapkv for do-style iteration. However, using dotable will be more efficient for hash-tables and alists (although not extensible).
RTL-USER> (mapkv ^(1+ %%) #h(:foo 0 :bar 1))
#{
:FOO 1
:BAR 2
}
As you see, mapkv expects a function of 2 arguments: the key and the value.
The tree utilities, mostly, automate list-based trees iteration.
dotree/maptree and doleaves/mapleaves allow iterating the tree by subtrees or just the leaves in do- and map-styles: i.e. map will also produce an altered copy of the iterated list.
RTL-USER> (dotree (subtree '(1 (2 3) (4 (5 6))))
(print subtree))
(1 (2 3) (4 (5 6)))
(2 3)
3
(4 (5 6))
(5 6)
6
NIL
RTL-USER> (doleaves (node '(1 (2 3) (4 (5 6))))
(print node))
3
6
NIL
RTL-USER> (maptree ^(if (oddp %) :foo :bar)
'(1 (2 3) (4 (5 6))))
(:FOO (:BAR :FOO) (:BAR (:FOO :BAR)))
In this tutorial, I've described, maybe, 80-90% of what RUTILS has to offer. If you want to learn more, browse the docs (but wait for the update), the tests or the codebase itself (I tried to make docstrings comprehensible and clear).
Also, I wanted to clearly state that the major part of RUTILS is not my invention: it was taken from other libraries, extracted from books, internet forum discussions. Many ideas were borrowed from other languages. My additions are not that big and are, mostly, variations on the existing topics. The principal idea was not to invent something new but, rather, to carefully gather the existing stuff scattered over many places.
I have used RUTILS in almost all of my Lisp projects, for more than 7 years already. I never had any problems or conflicts in connection to that. However, you should note that my main platform is SBCL and it's where I do most of the testing. Specific support for other Lisp compilers was occasionally contributed by other users.
This is my personal Top-10 of the most frequently used/useful features of RUTILS:
?with- Shorthand lambdas:
^() - Anaphoric
if-itandwhen-it toggle-print-hash-tableand the literal hash-table syntax#h()- The major abbreviations:
:=,make,call read-fileandwith-out-filesplitdotable,doindexget#/in#/set#/...
But that's just the tip of an iceberg: I could easily extend it to 30 items that I use regularly, in my code.
So, you may not like some of the choices made by RUTILS (that's one of the reasons it's called radical, after all), but, in fact, it nicely coexists with any other Lisp code and provides a plethora of small improvements that can make your day-to-day Lisp workflow more efficient and pleasant. You can use them as a whole if you happen to have a similar mindset to mine, or partially (for instance, by importing one of its packages like rutils.bind), or even cherry-pick a couple of items you need and copy-paste it to your own codebase. Anyway, I'd be glad if it comes handy...