Writing Scheme Functions With Optional Arguments

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 lambda, 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))))))

This defines 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:

  1. git-update is no longer a function which can accept no value. It could still be written to use case-lambda to handle that situation, calling new-git-repository and setting appropriate values for it before handing it back to git-update again.

  2. 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.

Conclusion

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)

First are some great comments by ThatGeoGuy on /r/scheme. I strongly recommend you read his posts.

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!

Advertisements

4 thoughts on “Writing Scheme Functions With Optional Arguments

  1. When I have a multitude of potential arguments, I define a long version, then define a short version fitting my needs at the time.

    (define (list-remove/ el xs e? count-max) …)

    (define (list-remove1 el xs) (list-remove/ el xs equal? 1))

    For your example, fluid variables might feel more right, but I haven’t played with them.

    1. Good point/idea re. defining long and short versions of functions, a technique I’ve seen in various libraries before.

      As for fluid variables, does Scheme even support such a thing? I’m used to using such variables in Emacs Lisp, but it’s my understanding that Scheme doesn’t support them (not R5RS anyway, I don’t know about later standards).

      1. Yep, you’re correct that I’m using scsh (and thus Scheme48). I wasn’t aware Scheme48 supported fluid variables. Thanks for the info!

Add Your Thoughts

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s