8.12

2.7 More Function Arguments🔗ℹ

As we saw in Optional and Keyword Arguments, by using optional arguments and | clauses, you can define a function that accepts a varying number of arguments. When using only those features, however, the allowed number of arguments is still a fixed set of small numbers. To define or call a function that accepts any number of arguments, use the ... repetition form or & list-splicing form after other arguments.

For example, in the following definition of add, the argument x is bound as a repetition, which allows any number of arguments:

fun add(x :~ Number, ...):

  for values(total = 0):

    each v: [x, ...]

    total+v

> add(1, 2, 3, 4)

10

> def [n, ...] = [20, 30, 40]

> add(10, n, ..., 50)

150

> def ns = [20, 30, 40]

> add(10, & ns, 50)

150

As illustrated in the calls to add, ... and & work for function calls the same way that they work for constructors specifically. Elements of the repetition or list are spliced into the function call as separate arguments, as opposed to being passed as a list. While only one of ... or & can be used in a function definition, they can appear any number of times and in any order within a function call.

A function doesn’t have to accept an arbitrary number of arguments for ... or & to work in a call to the function, as long at the total number of spliced arguments matches the number that the function expects.

> math.expt(& [2, 10])

1024

The add function could also be written with & for its argument instead of ..., like this:

fun add(& xs :~ List.of(Number)):

  for values(total = 0):

    each v: xs

    total+v

> add(1, 2, 3, 4)

10

Note that the annotation on x as a repetition refers to an individual argument within the repetition, while the annotation on xs refers to the whole list of arguments.

To create a function that works with any number of keyword arguments, use ~& to bind an argument that receives all additional keyword arguments. The additional arguments are collected into a map with keywords as keys.

fun roster(~manager: who, ~& players):

  players

> roster(~pitcher: "Dave", ~manager: "Phil", ~catcher: "Johnny")

{#'~catcher: "Johnny", #'~pitcher: "Dave"}

Similarly, use ~& in a function call to pass keyword arguments that are in map. Using ~& to call a function is most useful when chaining from one keyword-accepting function to another.

fun

| circle_area(~radius): 3.14 * radius * radius

| circle_area(~diameter): (1/2) * 3.14 * diameter * diameter

fun

| rectangle_area(~width, ~height): width * height

fun

| shape_area(~type: "circle", ~& props): circle_area(~& props)

| shape_area(~type: "rectangle", ~& props): rectangle_area(~& props)

> shape_area(~type: "circle", ~radius: 1)

3.14

> shape_area(~type: "circle", ~diameter: 8.5)

113.4325

> shape_area(~type: "rectangle", ~width: 8.5, ~height: 11)

93.5

A function call can use ~& any number of times, and in any order compared to other arguments. A function definition can use ~& at most once, and only after all other arguments other than a & argument or ... repetition argument. A ~& for a function definition can appear in either order with a & or ... argument.

Functions can use | cases, annotations, and/or pattern matching to distinguish calls with the same number of arguments. Different cases use &, ..., and ~& independently.

fun

| avg(n :: Number, & ns :~ List.of(Number)):

    (n + add(& ns)) / (1 + List.length(ns))

| avg(p :: Posn, & ps :~ List.of(Posn)):

    Posn(avg(p.x, & Function.map(Posn.x, ps)),

         avg(p.y, & Function.map(Posn.y, ps)))

> avg(1, 2, 6)

3

> avg(Posn(0, 0), Posn(1, 3), Posn(2, 0))

Posn(1, 1)