[Contents]
[Prev] [Next] [Limbo Basics] [Limbo Programming] [Language Definition]

Abstract Data Types (ADT)

In Limbo an abstract data type, ADT, is a user-defined type that encapsulates data and functions into a named type. It is similar to a C struct, but can include functions, like a C++ class. Like Limbo modules, however, an ADT is final and does not have inheritence or polymorphism.

Declaring an ADT

An ADT declaration has the general form:

	identifier: adt {
		adt-member-list
	};
	
ADT member declarations are much the same as data and function declarations elsewhere in a Limbo program.

For example, the following is a declaration of a simple Inventory ADT:

	Inventory: adt {
		id: string;
		onhand: int;
		cost: real;
		value: fn(item: Inventory): real;
	};

This declaration contains three data members and one member function. Following the declaration, Inventory is a type, like int or real. Typically, ADT declarations are in a module file (.m).

ADT member function definitions typically are in the implemenation file (.b). ADT member function definitions are the same as regular Limbo function defintions, with the addition of the ADT qualifier in the name using the dot operator (.).

For example:

	Inventory.value(item: Inventory): real {
		return (real(item.onhand) * item.cost);
	}

Instantiating an ADT

Instantiating an ADT is declaring a variable to be of the ADT type. For example:

	part1: Inventory;

This declares part1 to be of type Inventory.

Accessing ADT Members

The members of the ADT are accessed using the dot notation (.) and can be treated like any other variable or function.

For example:

	part1.id = "Widget";
	part1.onhand = 250;
	part1.cost = 4.23;

This assigns values to the id, onhand and cost members of part1.

Calls to ADT member functions follow a similar pattern, for example:

	sys->print("Value On Hand: $%5.2f\n", 
part1.value(part1));

Using the values assigned above, this statement prints:

	Value On Hand: $1057.50

In this example, in order for the member function to operate on the member data, the instance of the ADT must be explicitly passed as a argument to the function. Passing of the ADT instance as an implicit argument can be done using the keyword self.

Implicit Argument: self

Changing the declaration of the Inventory member function to use the keyword self passes an instance of the ADT as an implicit argument. For example, note the addition of the keyword self to the value function declaration and definition:

	Inventory: adt {
		id: string;
		onhand: int;
		cost: real;
		value: fn(item: self Inventory): real;
	};

	Inventory.value(item: self Inventory): real {
		return (real(item.onhand) * item.cost);
	}

The addition of self to the member function declarations and definitions enables the calling of the member functions without explicitly passing the ADT instance. The following reflects the change to the statement that calls this function:

	sys->print("Value On Hand: $%5.2f\n", part1.value());

Using the values assigned earlier, this statement prints:

	Value On Hand: $1057.50

The keyword self enables similar implicit argument functionality as the this keyword used in C++ and Java.

Ref ADT

As stated earlier, an ADT can be instantiated as a value type or a reference type. The ADTs that we have seen thus far have been instantiated as value ADTs. If an ADT is to be a reference type, called a ref ADT, it must be instantiated with the ref keyword.

For example, to instantiate the Inventory ADT as a reference type:

	part2:= ref Inventory;

This creates a new Inventory and places a reference to it in part2. The part2 instance can be used like a regular ADT. For example, assigning values to the member data is exactly the same:

	part2.id = "Whatsit";
	part2.onhand = 1000;
	part2.cost = 0.17;

It is important to note that part2 has a different type than part. This means that part2 cannot be assigned to an Inventory variable or passed directly to member functions of the current Inventory ADT. For example, the following statements would generate compile-time errors:

	part2 = part1;					# type clash error, won't compile
	part2.value();					# argument type mismatch, won't compile

To access a value from a ref ADT, use the asterisk (*) prefix operator:

	part1 = *part2;					 # correct
	(*part2).value(); # correct

ADT member functions can accept ref ADT types. This requires the addition of the ref keyword to the member function declaration:

	clear: fn(item: self ref Inventory);
	
The member function definition looks like this:

	Inventory.clear(item: self ref Inventory) {
		item.onhand = 0;
	}

Ref ADT vs. Value ADT

The primary advantage of ref ADT is that it can be passed between functions without copying, making it more efficient, especially for a large ADT. Member functions that accept a ref ADT can overwrite their argument's contents. This cannot be done by member functions receiving a value ADT.

However, the interface to a ref ADT can become more obscure, primarily with the issue of being able to overwrite values. The interface to a value ADT is more straightfoward, making it more natural for most circumstances.

Pick ADT

A Limbo pick ADT is similar to a union in C. It enables separate instances of an ADT to hold data elements of different types. In this way, a single ADT declaration can instantiate ADTs of different types. Pick ADTs can have common data and function members and data members that are specific to the ADT instance.

There are two parts to a pick ADT: the pick ADT declaration, and the pick statement where you access the elements in the pick ADT.

The pick Declaration

The general form of the pick ADT declaration is:

	adtidentifier: adt {
		pick {
			element => variable: type;
			...
		}
	};

The element is the identifier to the pick element, which specifies a particular variable to be of some type. For example:

	Apick: adt {
		str: string;
		pick {
			String		=> val: string;						# string element
			Int  		=> val: int;						# int element
			Real 		=> val: real;						# real element
		}
	};

The common member of this ADT is str of type string. The pick elements are String, Int, and Real. These specify three variations of the Apick ADT.

If a pick ADT has common data members, the pick block must be the final data segement. Member functions can follow the pick block, however.

A pick ADT must be instantiated as a ref ADT. The specific element of the pick must be specified. For example:

	r := ref Apick.Real;

When you assign values to the data members of the ADT, use the variables declared via the pick structure:

	r.str = "Pi";
	r.val = 3.141593;

You can also declare and initialize an instance of the ADT. For example:

	s := ref Apick.String("Greeting", "Hello, World!");

The tagof Operator

The tagof operator returns the index of the pick ADT element in a particular instance of the ADT. The elements are indexed starting at 0. For example:

	tagof r

This returns the index of the r instance of the Apick ADT created above, which is 2 (the third element).

The pick Statement

The pick statement enables statement selection based on the variant type of the pick ADT. The general form of the pick statement is:

	pick localinst := refadtinst {
		element => statements
		...
	}

The pick statement uses a local copy, localinst, of a reference to the ADT, refadtinst. The element(s) can be one or more of the elements declared in the pick ADT.

For example:

	t : ref Apick = s;
	pick u := t {
		String => sys->print("%s is %s\n", u.str, u.val);
		Int => sys->print("%s is %d\n", u.str, u.val);
		Real => sys->print("%s is %f\n", u.str, u.val);
	}

First, prior to the pick statement, a ref ADT of the s instance is initialized. Then, the pick statement selects the statement to execute based on the variant type of the ADT t.

Another way typically used to accomplish this is to place the pick statement in a function that accepts as an argument the ref ADT. For example:

	printval(a: ref Apick) {
		pick t := a {
			String => sys->print("%d: %s\n", tagof t, t.val);
			Int => sys->print("%d: %d\n", tagof t, t.val);
			Real => sys->print("%d: %f\n", tagof t, t.val);
		}
	}

To call this function, the instance of the ADT that specifies the element of the pick is given as the argument:

	printval(s);

The function can also be a member of the ADT. In this way, the ADT can be passed as an implicit argument. For example:

	Apick.printval(this: self ref Apick) {
		pick t := this {
			String => sys->print("%d: %s\n", tagof t, t.val);
			Int => sys->print("%d: %d\n", tagof t, t.val);
			Real => sys->print("%d: %f\n", tagof t, t.val);
		}
	}
Then, the call to this function would look like this:

	s.printval();

Program Listing 2-2 is an simple program that uses a pick ADT.

Program Listing 2-2 pick.b

  1. implement Command;
  2. include "sys.m";
  3. include "draw.m";
  4. sys: Sys;
  5. Apick: adt {
  6. pick {
  7. String => val: string;
  8. Int => val: int;
  9. Real => val: real;
  10. }
  11. printval: fn(this: self ref Apick);
  12. };
  13. Apick.printval(this: self ref Apick) {
  14. pick t := this {
  15. String => sys->print ("%s: %s\n", tag(t), t.val);
  16. Int => sys->print ("%s: %d\n", tag(t), t.val);
  17. Real => sys->print ("%s: %f\n", tag(t), t.val);
  18. }
  19. }
  20. tag(t: ref Apick): string {
  21. r: string;
  22. case tagof t {
  23. tagof Apick.String => r = "String";
  24. tagof Apick.Int => r = "Int";
  25. tagof Apick.Real => r = "Real";
  26. }
  27. return r;
  28. }
  29. Command: module {
  30. init: fn(ctxt: ref Draw->Context, argv: list of string);
  31. };
  32. init(ctxt: ref Draw->Context, argv: list of string) {
  33. sys = load Sys Sys->PATH;
  34. r := ref Apick.Real;
  35. r.val = 3.141593;
  36. sys->print("%d\n", tagof r);
  37. s := ref Apick.String("Hello, World!");
  38. s.printval();
  39. r.printval();
  40. }

This program produces the following output:

	2
	String: Hello, World!
	Real: 3.141593



[Contents]
[Prev] [Next] [Limbo Basics] [Limbo Programming] [Language Definition]

Copyright © 1998, Lucent Technologies, Inc. All rights reserved.