An SRFI is a “Scheme Request for Implementation”, a standard way in which the community extends the functionality of the Scheme programming language. Today I want to briefly talk about some SRFIs that I’ve found useful. Example uses of the SRFIs I’ll discuss can be found in a script I wrote for use with the Scheme Shell.
A common scenario is to try and compute a value and, if successful, use that value in following expressions. Depending on those expressions this can be costly as you may end up computing said value over and over for each expression. For example, the script I linked to above works with ‘records’ (similar to structures in C) which may contain their own functions, such as their
install-function property. If a given record has a value for that property then I want to call it, but first I need to see if the value exists and make sure that it has the correct type.
(and (get-install-function software) (procedure? (get-install-function software)) (apply (get-install-function software) (list software)))
get-install-function three times for this is not ideal. We could instead write it like this:
(let ((install-function (get-install-function software))) (and install-function (procedure? install-function) (apply install-function (list software))))
get-install-function only once, binding it to a name we can use throughout the
and expression. This is a much better approach because we avoid invoking a potentially costly function multiple times.
and-let* lets us take this approach with a single construct:
(and-let* ((install-function (get-install-function software)) ((procedure? install-function)) ((apply install-function (list software)))))
Each form within
and-let* can create a binding usable by the following forms. In the example above, the first form binds
install-function, which the other forms go on to use. This is the behavior of
let*, but like
and-let* construct will also terminate the moment any expression evaluates to a boolean false value.
I will admit that my own example doesn’t look like a useful gain, but if your
and forms involve the computation of multiple values then
and-let* really shines in its usefulness.
As I said above, records in Scheme are similar to structures in other languages. There have been various implementations of records in Scheme which still exist today. But SRFI-9 provides a way to create records that is compatible across different Schemes and platforms.
Here’s an example from my own program:
(define-record-type :software (new-software directory install-prefix) software? (directory get-directory) (install-prefix get-install-prefix) (update-function get-update-function set-update-function!) (build-function get-build-function set-build-function!) (install-function get-install-function set-install-function!))
This form accomplishes the following:
It defines the type
:softwarefor the record.
It defines the constructor
(new-software …)which accepts two arguments. It will create and return a new
:softwarerecord with two of its properties set to the values given to the constructor.
software?is a predicate of one argument that is true for all instances of the record and false for everything else.
All of the other forms define the properties of the record. Their syntax is:
(name getter [setter])
In my example the record has five properties. Two have only getters, functions for accessing the value of those properties from instances of the record; they have no setters because those two properties are set as part of the constructor. The other three properties have both getter and setter functions; they are optional and none are set by the constructor, hence the definition of both getters and setters.
Creating one of the records looks like so:
(define emacs (new-software "/home/eric/Software/Emacs" "/usr/local")) ;; (get-directory emacs) => "/home/eric/Software/Emacs" ;; (get-install-prefix emacs) => "/usr/local"
And an example of a function which uses other getters of the record along with the aforementioned
(define (update software) (with-cwd (get-directory software) (or (and-let* ((update-function (get-update-function software)) ((procedure? update-function)) ((apply update-function (list software))))) (begin (run (git fetch origin)) (run (git checkout master)) (run (git merge --ff-only origin/master))))))
Note: This is also an example of using
(with-cwd …) and
(run …) in the Scheme Shell to execute shell commands within a given directory.
The final SRFI that I want to discuss is the most complex of the three simply in terms of the amount of functions it defines. R5RS Scheme, the standard used by the Scheme Shell, provides little in the way of string functions. There are functions for creating strings, simple modifications and access to sub-strings, and for performing basic comparison. This reflects Scheme’s philosophy of minimalism, especially when compared to other Lisps like Common Lisp and all the symbols it defines, to say nothing of third-party additions by implementations like Steel Bank Common Lisp (SBCL).
(Note: SBCL is a terrific implementation of Common Lisp in my opinion, if you ever have need for one.)
Nearly all applications work with strings in some capacity. When using Scheme for a task such as writing shell scripts the paucity of Scheme’s string functions quickly becomes apparent. SRFI-13 builds upon Scheme’s string library by adding a number of useful utilities, such as:
string-null?: A predicate that tells you if a string is empty or not.
string-join: Takes a list of strings and combines them, inserting a delimiting string between each element, e.g.
(string-join '("usr" "local" "bin") "/" 'prefix) ;; Evaluates to: "/usr/local/bin"
string-trim: Removes characters from the start and end of a string, and by default removes whitespace.
string-prefix?: A predicate to determine if a string begins with a given prefix. The SRFI also provides
string-suffix?, and versions of both which perform case-insensitive tests.
string-contains: Tells you if a sub-string appears within a string, and if so, the start and end indices for that sub-string.
string-tokenize: Splits a string into a list of sub-strings based on delimiters of your choice, defaulting to whitespace.
Such utilities are common fixtures in the standard libraries of many high-level programming languages. Personally I like Scheme’s minimalism and the simplicity it affords the language. But the string functions of R5RS Scheme are severely lacking, making SRFI-13 a treasure chest of valuable tools.
If you use Scheme then I strongly recommend taking a look at which SRFIs your choice implementation supports. Many of them are quite useful, so I also suggest reading the full list of accepted SRFIs. A lot of them can be implemented as macros in the event you’re using a Scheme that does not provide a certain SRFI, and every SRFI comes with details about how you can implement it if needed.