How do you write a Scheme function which has a variable arity? That is, one which accepts a multiple number of arguments? In this short article I’ll show you how.
SRFI’s Save the Day Again
I recently wrote about “Scheme Requests for Implementation” (SRFI), proposals for a variety of extensions to Scheme. Today we’re interested in SRFI-16 which provides
case-lambda, supported by many Scheme implementations.
Unlike the normal
case-lambda lets us express different behavior based on the number of arguments given to the function. Here is a (hopefully) simple to understand example from a project of mine:
(define git-update (case-lambda (() (git-update "origin" "master")) ((remote) (git-update remote "master")) ((remote branch) (run (git fetch ,remote)) (run (git checkout ,branch)) (run (git merge --ff-only ,(string-append remote "/" branch))))))
git-update as a function which behaves differently based on its arity, thanks to
case-lambda. As you can see,
case-lambda consists of a number of expressions in the form of
((parameters) body). The most simple case is
(), i.e. when the function receives no arguments. As you can see in this case the function will then call itself with two arguments. The next case,
(remote), is for when the function receives only one argument; in that scenario we again re-call
git-update itself but pass along the one argument we received. The final scenario,
(remote branch), describes what to do when
git-update receives two arguments. This case contains the most logic.
My example demonstrates a useful application of
case-lambda: creating functions with optional arguments that take on default values. Thanks to
case-lambda the following function calls are equivalent:
(git-update) => (git-update "origin" "master") (git-update "ejmr") => (git-update "ejmr" "master")
This pattern, however, becomes unwieldy if you are creating a function that accepts a large number of optional arguments. Personally I would not use
case-lambda for this purpose if I were writing a function that accepted more than three optional arguments. In that situation I would possibly create a record encapsulating the data represented by those arguments, and then I would write the function so that it receives a single instance of that record and makes use of getters to check for optional values and supply defaults where need. Rewriting my example above in this style would look something like this:
(define-record-type :git-repository (new-git-repository) git-repository? (remote get-remote set-remote!) (branch get-branch set-branch!)) (define (git-update repository) (let ((remote (or (get-remote repository) "origin")) (branch (or (get-branch repository) "master"))) (run (git fetch ,remote)) (run (git checkout ,branch)) (run (git merge --ff-only ,(string-append remote "/" branch))))) ;;; Similar to (git-update "ejmr") from before. (let ((foo (new-git-repository))) (set-remote! foo "ejmr") (git-update foo)) ;;; Same as (git-update "origin" "test"). (let ((bar (new-git-repository))) (set-branch! bar "test") (git-update bar))
You can see how this is more work. Although there are some differences in this approach:
git-updateis no longer a function which can accept no value. It could still be written to use
case-lambdato handle that situation, calling
new-git-repositoryand setting appropriate values for it before handing it back to
The final example above,
(git-update bar), performs something which is not possible with my original implementation that relies only on
case-lambda. By using a record I am effectively not bound to any order with regard to the optional arguments, so in that example I provide a specific branch name but still rely on the default
"origin"for the remote.
Personally I prefer my original implementation of
git-update which uses
case-lambda since I am only dealing with a maximum of two arguments. But if you’re writing a function that deals with a potentially large number of optional data then I would recommend using records instead, or anything besides
case-lambda, because it simply doesn’t scale well in that scenario.
That’s pretty much it, really. If you need to define a Scheme function with a variable arity, you can. And since it’s well supported across Scheme implementations I personally consider it to be quite portable. Like many SRFI’s, it’s not a feature you need all the time, but when you do it’s nice to have an SRFI around to help you out.
Updates (8th of July 2015)
Second, a couple of people pointed out that it is also possible to achieve similar results by using “fluid variables”, i.e. dynamic scope somewhat like you would see in Emacs Lisp or Common Lisp. For example, I could define the
git-update function like so:
(define *git-remote* "origin") (define *git-branch* "master") (define (git-update) (run (git fetch ,*git-remote*)) (run (git checkout ,*git-branch*)))
Instead of accepting any arguments, this version of
git-update uses the values of two variables defined at the top-level. I could then change their values as needed before each call to
git-update, like so:
(define (main arguments) ;; Equivalent to (git-update "origin" "master") from the original ;; implementation at the start of the article. (with-cwd "/home/eric/Projects/LNVL" (git-update)) ;; This call is equivalent to (git-update "origin" "develop"). ;; Instead of passing any arguments, however, I change the value of ;; *git-branch* before calling (git-update). The function will ;; always use the current value of *git-branch*, so this is an ;; example of "fluid variables" at work. (with-cwd "/home/eric/Projects/Hypatia" (set! *git-branch* "develop") (git-update)) (exit 0))
Thank you to everyone for the feedback and additional information!