Tuples

Limbo has several mechanisms for grouping data of different types into a single object. The simplest of these is a tuple, which is just a parenthesized list of values:

	t := (3, "three");

creates a tuple called t that stores an integer and a string. A tuple can be unpacked by assignment:

	i: int;
	s: string;
	(i, s) = t;

sets i to 3 and s to "three". This style of declare-and-unpack is common enough that Limbo provides a shorthand; we could have written

	(i, s) := t;

Sometimes, we want to unpack only part of a tuple, not the whole thing. In this case, we can use the keyword nil as a stand-in for any element to be ignored:

	(i, nil) = t;

will assign 3 to i and ignore the string part of t.

Tuples offer a convenient way to return an error condition from a function. For example the Limbo cast to convert from string to int does not signal incorrectly formatted numbers. If we wish to do the conversion with careful error-reporting, we could use a function that returns the int value and an error string to report any trouble:

	convert(str: string): (int, string)
	{
		# skip white space
		for(i:=0; i<len str; i++)
			if(str[i]!=' ' && str[i]!='\t')
				break;
		if(i == len str)
			return (0, "blank or empty string");
	
		# leading sign
		sign := 1;
		case str[i]{
		'-' =>
			sign = -1;
			i++;
		'+' =>
			i++;
		}
	
		# check digits
		for(j:=i; j<len str; j++)
			if(str[j]<'0' || '9'<str[j])
				break;
		if(j == i)
			return (0, "string does not begin with digits");
		err := "";
	
		# check trailing white space
		for(k:=j; k<len str; k++)
			if(str[i]!=' ' && str[i]!='\t'){
				err = "non-digits after number";
				break;
			}
		return (sign*int str[i:j], err);
	}
	

This function skips any leading white space and checks that it got some characters. Then it looks for a leading sign.

The Limbo case statement compares its expression, which may be an integer or string, against a list of constant values. If the expression matches a value, the statements associated with that value are executed. If it matches none, the statement has no effect except to evaluate the expression. The asterisk (*) is a special `value' that matches all expressions. We could have written:

	case str[i] {
	'-' =>
		sign = -1;
		i++;
	'+' =>
		i++;
	'0' or '1' or '2' or '3' or '4' or
	'5' or '6' or '7' or '8' or '9' =>
		;	# empty statement; range check only
	* =>
		return (0, "non-numeric string");
	}

but this was unnecessary because the next section of convert performs this check on the whole string. This rewrite also uses the ability to group values in the case statement; the list of numbers could be reduced to the more direct

	'0' to '9' =>
		;

Finally, convert makes sure that any text after the number is white space. Along the way, if it finds any problems it returns 0 for a value along with a description of the problem. Note how the check for trailing white space sets an error flag but still returns the value it got. This allows the calling program to retrieve the value but warn that there may be garbage in the string:

	(v, err) := convert(str);
	if(err != "")
		sys->print("warning: error %s converting %s\n", err, str);
	# use v anyway
	compute(v);

Using this style with functions that return reference types, it's possible to distinguish success by returning (value, ""); partial failure (value, error); and complete failure (nil, error).

previous next

© Rob Pike and Howard Trickey 1997. All rights reserved.