1

After reading Python's range() analog in Common Lisp, I went thinking that I didn't really like the function interfaces used on the answers.

Three different lambda lists appear there:

  1. (start end &optional (step 1)): both the start and end arguments are mandatory.

  2. (end &key (start 0) (step 1)): IMHO, using keyword arguments seems overkill for such simple function, and they are there just to hide the fact that end and start do not appear in the natural order (i.e. first start, then end)

  3. (n &key (start 0) (step 1)) (from alexandria:iota): here, the optionality and order of the arguments are right, but at the expense of using a different abstraction.

The thing is that I would like to write (range 6) to generate (0 1 2 3 4 5) but also (range 3 6) to generate (3 4 5). And actually, it is easily doable; for instance:

(defun range (start_or_end &optional end (step 1))
  (multiple-value-bind (start end)
      (if end
          (values start_or_end end)
          (values 0 start_or_end))
    (loop for n from start below end by step collect n)))

But well, I haven't seen this kind of argument fiddling in others code, and as a Lisp newbie I would like to know if that is an acceptable idiom or not.

Update: I have just discovered that Racket provides a range function similar to the one I was proposing (an also the in-range generator).

Community
  • 1
  • 1
salva
  • 9,943
  • 4
  • 29
  • 57
  • 1
    [lgtm](http://www.lgtm.in/) – sds Apr 30 '15 at 10:59
  • Looks ugly to me. Invites errors. Hard to read. I would look at a call in the code and not know what it does. Lisp is a 'symbolic language': be as descriptive as possible, when in doubt. Avoid overly 'clever' code. – Rainer Joswig Apr 30 '15 at 16:14

2 Answers2

4

As Alessio Stalla pointed out, there's nothing the matter with this, but it's not something you'll see very often. Overloading by arity gets more complicated when the language permits optional and rest arguments.

I think the way that a case like this would typically be handled is to define things in terms of designators. You could state that a range is determined by three values: a start, an end, and a step. Then you can say that a range designator is a list of length at most three, with the following semantics:

  • (n) designates (:start 0 :end n :step 1)
  • (m n) designates (:start m :end n :step 1)
  • (m n s) designates (:start m :end n :step s)

Then you can do something like:

(defun range (&rest range-designator)
  (destructuring-bind (a &optional (b nil bp) (c nil cp))
      range-designator
    (multiple-value-bind (start end step)
        (cond
          (cp (values a b c))
          (bp (values a b 1))
          (t  (values 0 a 1)))
      (loop for x from start to end by step
           collect x))))

CL-USER> (range 5)
(0 1 2 3 4 5)
CL-USER> (range 2 7)
(2 3 4 5 6 7)
CL-USER> (range 2 7 3)
(2 5)

If you anticipate using range designators in other places, you can pull that inside stuff out a bit:

(defun to-range (designator)
  (destructuring-bind (a &optional (b nil bp) (c nil cp))
      designator
    (cond
      (cp (values a b c))
      (bp (values a b 1))
      (t  (values 0 a 1)))))

(defun range (&rest range-designator)
  (multiple-value-bind (start end step)
      (to-range range-designator)
    (loop for x from start to end by step collect x)))
Community
  • 1
  • 1
Joshua Taylor
  • 84,998
  • 9
  • 154
  • 353
2

It is acceptable, although you don't encounter it very frequently. I'm pretty sure there are functions in the standard with such a signature but I can't remember any at the moment.

One example that I do remember is the JFIELD primitive in ABCL: http://abcl.org/trac/wiki/JavaFfi#FunctionJFIELDJFIELD-RAWSETFJFIELD

If you're concerned about performance, since "parsing" the lambda list has a cost, you can use compiler macros to avoid paying it, especially in a case like yours where the behaviour of the function is driven only by the number of arguments (as opposed to their types).

Alessio Stalla
  • 1,022
  • 6
  • 22