https://new.jameshunt.us

Variadic Functions & Their Representation By Source Code

This is an article in the ongoing series on implementing Rook Lisp.

I can't live with a language that doesn't support functions of variable arity, and I refuse to create a language that forces support for variadic functions through list processing.

In Rook Lisp, I want to do this:

(printf "Hello, %s\n" name)

And I definitely don't want to do this:

(printf "Hello, %s\n" (list name))

So, Rook needs variadic functions, both in the core of the language, and for users to define. To the latter, we need a syntax. I figured I'd do what all language designers do best: go steal an idea from another language.

Thankfully, Rosetta Code makes this a lot easier than it used to be. After reading through all the examples of definition and usage of variadic functions in different languages, I've come to group them (the languages) into six categories.

The No Dice Language

There are, of course, languages in which you can't express multi-variadic functions.

The Explicit List Languages

In languages that don't support variadic function, but do support arrays of variable lengths, or even lists, you can fake n-ary functions by passing a list as the last parameter.

ALGOL-68 does that:

PROC printall = (FLEX[]STRING argv) VOID: (
  FOR i TO UPB argv DO
    print(argv[i])
  OD
);

printall(("Algol-68", "uses", "lists"))


In Perl, there are two things about function and lists that make variadic support possible, without first-class support in the language:

  1. Lists automatically flatten (nested lists are impossible)
  2. Function arguments are passed as the (list) array @_

because of this, all functions in Perl are variadic, it's just that most Perl programmers don't abuse it.

sub printall {
  print "$_\n" for @_;
}

printall("Perl", "uses", "lists");


The Missing Arguments Languages

Some languages fake it by initializing unspecific positional parameters to a default value. AWK does this; any parameters the caller omits will be set to the empty string, "".

function printall(a,b,c,d,e,f){
  if (a != "") print a
  if (b != "") print b
  if (c != "") print c
  if (d != "") print d
  if (e != "") print e
  if (f != "") print f
}

printall("AWK", "handles", "missing", "arguments")

There are some severe downsides to this method. For starters, you can't define a truly variadic function; there is always an upper bounds on the number of arguments a caller can supply. Additionally, callers cannot pass the default value explicitly.

The Special Variable Languages

JavaScript takes a novel approach: all functions can be given multiple arguments, but you can't lexically bind them to formal parameters. Instead, the function just accesses the special variable arguments, an array-like special object (it has a .length!)

function printall() {
  for (var i = 0; i < arguments.length; i++) {
    console.log("%v\n", arguments[i]);
  }
}

printall("js", "uses", "a", "special", "variable");

As with Perl, since arguments is array-like, you can pass it around, slice it, dice it, and even use it with apply(). Not bad for the introduction of a reserved keyword!

The Modified Symbol Languages

Ruby has what's called a splat operator. If you prefix the name of the last formal parameter with the * sigil, Ruby accumulates all of the variable parameters, in each call, into that parameter, as a list.

def printall(*args)
  args.each do |arg|
    puts arg
  end
end

printall("Ruby", "uses", "symbol", "modifiers")


Go prefixes the type of the formal parameter with three dots, but otherwise it works the same way as Ruby:

func PrintAll(args ...string) {
  for _, arg := range args {
    fmt.Printf("%s\n", arg)
  }
}

PrintAll("Go", "uses", "symbol", "modifiers")


The Sigil Symbol Languages

Other languages (notably Lisp dialects) introduce a sigil, or symbol, into the function signatures, and the variable after the symbol gets bound to the "overflow" parameters.

Clojure does it with &:

(defn print-all [& args]
  (doseq [a args]
    (println a)))

(print-all :clojure :uses :symbols)


Common Lisp and Emacs Lisp use &rest:

(defun print-all (&rest args)
  (dolist (arg args)
          (print arg)))

(print-all 'lisps 'use 'symbols)



Scheme uses the . operator, which mirrors the printed form of an improper list (a cons with a non-cons cdr):

(define (print-all . args)
  (for-each
    (lambda (x) (display x) (newline))
    args))

(print-all 'lisps 'use 'symbols)


Interesting side note: Scheme and other Lisp dialects differ on how they define functions. Scheme mimics the calling form in the definition, i.e:

(define (function arg1 arg2 etc) ...)

whereas other Lisps split the argument list from the function name:

(defun function (arg1 arg2 etc) ...)

This might be the main reason Scheme can use . and other Lisps uses & or &rest.

The Rook Language

After looking at how everyone else does it, and talking it over with some nerd friends of mine who also like dreaming up new languages, I've settled on a novel approach:

(fn (print-all (msgs))
    (for (m msgs)
         (printf "%s\n" m)))

The presence of a single, single-element list in the call signature of the definition indicates to the compiler that the rest of the variadic arguments go in the msgs parameter.

There are limits to the notation:

My favorite thing about this notation is that the intent is clear, without the addition of any new lexer syntax, or any new keywords.

Happy Hacking!

James (@iamjameshunt) works on the Internet, spends his weekends developing new and interesting bits of software and his nights trying to make sense of research papers.

Currently exploring Kubernetes, as both a floor wax and a dessert topping.