Variadic functions

Up till now all functions have had a specified number of named arguments. We will now introduce a syntax for defining variadic functions, which may take a fixed number of named arguments and a variable number of additional arguments which are collected into a named list.

The argument declarations for variadic functions are improper lists:

λ-syntax Combined DEFINE
3 args (LAMBDA (arg1 arg2 arg3) body...) (DEFINE (name arg1 arg2 arg3) body...)
≥2 args (LAMBDA (arg1 arg2 . rest) body...) (DEFINE (name arg1 arg2 . rest) body...)
≥1 args (LAMBDA (arg1 . rest) body...) (DEFINE (name arg1 . rest) body...)
≥0 args (LAMBDA args body...) (DEFINE (name . args) body...)

In the definitions above, the parameters are bound as follows:

Definition (f 1 2 3)
Value of a Value of b Value of c
(DEFINE (f a b c) body...) 1 2 3
(DEFINE (f a b . c) body...) 1 2 (3)
(DEFINE (f a . b) body...) 1 (2 3)
(DEFINE (f . a) body...) (1 2 3)

Implementation

All that is required is a small modification to make_closure to accept the declaration:

// make_closure returns an Atom on the stack.
// a closure is a list that binds the environment and arguments.
// note that result may not be updated if there are errors.
func make_closure(env, args, body Atom, result *Atom) error {
    // verify number and type of arguments
    if !listp(body) {
        return Error_Syntax
    }
    // verify arguments.
    // if we find a symbol, stop checking.
    // if we find something that is not a pair with a symbol in the cdr, return an error.
    for p := args; !nilp(p) && p._type != AtomType_Symbol; p = cdr(p) {
        if p._type != AtomType_Pair || car(p)._type != AtomType_Symbol {
            return Error_Type
        }
    }

    // bind the environment and arguments to the closure
    *result = Atom{
        _type: AtomType_Closure,
        value: AtomValue{
            pair: &Pair{
                car: env,
                cdr: cons(args, body),
            },
        },
    }
    return nil
}

And another to apply to bind the additional arguments into a list:

// apply calls a native function with a list of arguments and updates the result.
// note that the result may not be updated if we find errors.
func apply(fn, args Atom, result *Atom) error {
    .
    .
    .
    // handle closure
    if fn._type == AtomType_Closure {
        // create a new environment for the closure
        env := env_create(car(fn))

        // bind the arguments
        for arg_names := car(cdr(fn)); !nilp(arg_names); arg_names = cdr(arg_names) {
            // if arg name is a symbol, apply it as rest of arguments
            if arg_names._type == AtomType_Symbol {
                _ = env_set(env, arg_names, args)
                args = _nil
                break
            }
            .
            .
            .
        }
        .
        .
        .
    }
    .
    .
    .
}

Testing

A boring example:

ID Input Output
1 ((lambda (a . b) a) 1 2 3) 1
2 ((lambda (a . b) b) 1 2 3) (2 3)
3 ((lambda args args) 1 2 3) (1 2 3)

We can also create a variadic adder:

ID Input Output
4 (define (sum-list xs)(if xs(+ (car xs) (sum-list (cdr xs)))0)) SUM-LIST
5 (sum-list '(1 2 3)) 6
6 (define (add . xs) (sum-list xs)) ADD
7 (add 1 2 3) 6
8 (add 1 (- 4 2) (/ 9 3)) 6

Since you can always pass a list to a regular function, this is really just another kind of syntactic sugar.

func TestChapter10(t *testing.T) {
    env := env_create_default()

    for _, tc := range []struct {
        id     int
        input  string
        expect string
        err    error
    }{
        {id: 1, input: "((lambda (a . b) a) 1 2 3)", expect: "1"},
        {id: 2, input: "((lambda (a . b) b) 1 2 3)", expect: "(2 3)"},
        {id: 3, input: "((lambda args args) 1 2 3)", expect: "(1 2 3)"},
        {id: 4, input: "(define (sum-list xs) (if xs (+ (car xs) (sum-list (cdr xs))) 0))", expect: "SUM-LIST"},
        {id: 5, input: "(sum-list '(1 2 3))", expect: "6"},
        {id: 6, input: "(define (add . xs) (sum-list xs))", expect: "ADD"},
        {id: 7, input: "(add 1 2 3)", expect: "6"},
        {id: 8, input: "(add 1 (- 4 2) (/ 9 3))", expect: "6"},
    } {
        var expr Atom
        _, err := read_expr([]byte(tc.input), &expr)
        if err != nil {
            t.Errorf("%d: read error: want nil: got %v\n", tc.id, err)
            continue
        }

        var result Atom
        err = eval_expr(expr, env, &result)

        if tc.err == nil && err == nil {
            // yay
        } else if tc.err == nil && err != nil {
            t.Errorf("%d: error: want nil: got %v\n", tc.id, err)
        } else if !errors.Is(err, tc.err) {
            t.Errorf("%d: error: want %v: got %v\n", tc.id, tc.err, err)
        }
        if got := result.String(); tc.expect != got {
            t.Errorf("%d: eval: want %q: got %q\n", tc.id, tc.expect, got)
        }
    }
}