Skip to content

Proposal: List packing/unpacking (variadic arguments) #2

@JoeStrout

Description

@JoeStrout

Motivation

Sometimes it's handy to allow a function to accept a variable number of arguments, e.g., a print function that takes a list of things to print. Currently the only way to do that is to wrap all the arguments in a list, which is ugly at the call site.

In addition, it's impossible to make a decent wrapper function (e.g. a function-level decorator) without being able to both pack an unknown set of arguments into some sort of container, and then unpack them when calling through to the wrapped function.

Those are the main motivations; as a secondary consideration, it might be nice to support similar packing/unpacking in assignment statements, to allow for things like swapping values without a temporary, returning multiple values from a function, etc.

Phased Implementation

If we want, we could break implementation into two phases:

  1. Argument packing/unpacking: packing applies only in a parameter list (function definition); unpacking only applies in an argument list (function call). Assignments and return are unaffected.
  2. Assignments and return: assignment statements allow comma-separated items, with optional variadic last LHS identifier, and optional unpacking on the RHS; return statements allow comma-separated values with optional unpacking.

Proposed Syntax

To support both packing and unpacking, we'll use a new ellipsis token type: ... or .

For list packing, the syntax would be ...identifier. This would be valid only on the last parameter of a function definition, or (phase 2) the last left-hand side lvalue. It means that any argument or right-hand side values beyond this point are stored in the given argument/lvalue as a list.

For list unpacking, the syntax would be just like list slicing but with an ellipsis instead of a colon. So someList[...] would unpack the entire list as arguments or right-hand side values. Just as with slicing, start and end indexes are supported but optional; so someList[1...] would skip the first element of someList and unpack the rest, etc.

Phase 1 Example: Min

The mathUtil module defines max and min functions that take two arguments; but when you need to get the min of three or more arguments, you need to switch to a different method. It'd be nice if these could instead take any number of arguments, which they could with this feature:

min = function(...values)
  if values.len < 2 then return err("min requires at least two values"
  result = values[0]
  for i in range(1, values.len-1)
    if values[i] < result then result = values[i]
  end for
  return result
end function

lowestNum = min(5, 2, 3, 7)  // 2

Phase 1 Example: Decorator

A decorator is a function that takes another function as an argument, and returns a wrapped version of that function that does something extra.

This code defines a logging decorator that, when applied to a function, logs every call to a file.

logging = function(wrappedFunc)
  wrapper = function(...args)
    logFile.writeLine "Called " + wrappedFunc.name + "(" + join(args, ", ") + ")"
    return wrappedFunc(args[...])
  end function
  return wrapper
end function

It would be used like this:

susFunction = logging(function(a, b, c)
   // Do something suspicious with our arguments a, b, and c
   foo = range(a, b)
   for i in foo.indexes; foo[i] *= c; end for
   return foo.sum
end function)

print susFunction(5, 100, 3)  // logs and then does the call

Phase 2 Examples

If we also add Phase 2, it allows us to return multiple values:

twoD6 = function
  d1 = floor(rnd*6) + 1
  d2 = floor(rnd*6) + 1
  return d1 + d2, d1, d2
end function

sum, d1, d2 = twoD6

What's happening here: packing on the return statement is automatic when there are multiple return values (it returns a list); and unpacking of a list in the assignment statement is automatic when there are multiple left-hand-side lvalues.

If we want, we could collect some of those return values into a new list:

sum, ...dice = twoD6   // so dice == [d1, d2] in this example

Unpacking would work the same way:

rollD6 = function(n=2)
  dice = []
  for i in range(1, n)
    dice.push floor(rnd*6) + 1
  end for
  return dice.sum, dice[...]
end function

sum, d1, d2, d3 = rollD6(3)

The basic assignment-statement feature of supporting multiple LHS lvalues and RHS values could be used without packing/unpacking:

a, b, c = 0, 10, 256  // initialize several values at once

...and could be used for (among other things) swapping values in place:

x, y = y, x

Quirks and Oddities

Phase 2 includes automatic packing on return statements and the right-hand side of an assignment, because it just seems silly not to. But it does produce some weird cases where there are two ways to do the same thing:

  return d1 + d2, d1, d2
  return [d1 + d2, d1, d2]   // exactly the same!

Or:

  return return dice.sum, dice[...]
  return [dice.sum] + dice   // exactly the same!

On the left-hand side of an assignment, though, there is no alternative to automatic unpacking, without (in general) introducing a temporary and doing a lot of extra work:

sum, d1, d2, d3 = rollD6(3)
// ...would have to be...
temp = rollD6(3)
sum = temp[0]; d1 = temp[1]; d2 = temp[2]; d3 = temp[3]

This is why if we do Phase 2 at all, there is a strong incentive to have automatic unpacking. And if we have that without automatic packing, it leads to weirdly asymmetric cases like having to do:

x, y = [y, x]

So, in the end, it's probably least surprising to the user to just have automatic packing as well. But it does mean that whether you slap [ and ] around your return (or right-hand side) values makes no difference at all.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions