Types

Types are the fundamental data abstraction concept in Lasso. Since Lasso is an object-oriented language, every piece of data is an object and every object is of a particular type. A type is a predefined layout of data combined with a particular set of methods. Types provide a means for encapsulating data with the collection of methods designed to modify objects representing that data in predetermined ways.

Defining Types

Before a type can be used, it must first be defined. Defining a type is done in the same manner as other entities (traits, methods). The word define is used, followed by the name for the type, the association operator (=>), and a type expression that provides the description of the type’s methods and data members.

define typeName => type expression

Type Expressions

A type expression consists of the word type followed by a set of curly braces ({ ... }). Between those curly braces reside a series of sections; each describing a different aspect of the type. These sections include: “parent”; “data”; “trait”; and “public”, “protected”, and “private” member methods. Each section begins with one of those words and ends at the beginning of the next section or the end of the type expression (which would be a close curly brace). Each section is optional. Sections can occur in any order. The sections “trait” and “parent” can occur only once.

The most simple type definition is shown below. It defines a type named “person” and contains no sections. Therefore, the person type contains no methods or data members of its own. It is a completely valid, if somewhat useless, type.

define person => type { }

Data Members

Each data section defines one or more data members for the type, which are other objects contained by the type. In a data member section, the word data is followed by one or more data member names. Data member names follow the same rules as variable and method names. They can begin with an underscore or the characters A–Z and then can be followed by zero or more underscores, letters, numbers, or period characters. Character case is irrelevant for data member names.

Like variables, data members store values. Three values are unique to each instance of the type. If a person type was created then it could contain data members for the first and last name of the person, his/her birthdate, social security number, address, etc. Just as every individual has his own values for these items, so would every instantiated object.

The following example type implementation shows several different methods for defining data members. These methods can be mixed and matched in a single type to provide the best readability. Data sections can also be interspersed with the other sections in the type expression if necessary.

define person => type {
   data firstName, lastName
   data age
   data
      birthdate
   data ssn
   data address1, address2, city,
      state, zip, country
}

Type Constraints

Data member values can be constrained to hold only particular types of objects. To do this, follow the data member name with two colons (::) and then a type or trait name. When a data member is constrained, it cannot be assigned any value that does not fit the constraint. The following type constrains “firstName” and “lastName” to be string objects and “age” to be an integer value:

define person => type {
   data firstName::string, lastName::string
   data age::integer
}

Default Values

Data members can be given default values. When a type instance is first created, before it can be otherwise used, its data members are assigned their default values. A default value can be any single expression. The following type definition uses both type constraints and default values for “firstName” and “lastName”, but just a default value for “age”:

define person => type {
   data firstName::string = '', lastName::string = ''
   data age = 0
}

Accessing Data Members

Data members can be accessed from within the methods of a type by targeting the current type instance using the keyword self and the target operator (->) followed by the name of the data member between single quotes. The following expression would set the value of the data member “age” to “36”:

self->'age' = 36

The following expression produces the value of the “age” data member:

self->'age'
// => 36

Equivalently, Lasso supports a shortcut syntax for targeting “self” by using a single period. The examples above could be rewritten using a period in place of self->.

.'age' = 36

.'age'
// => 36

All of the data members in a type are private. This means that a data member can only be directly accessed using either of the above syntaxes; only when “self” is the target object. Optionally, data members can be exposed to the outside world. The following section describes how getters and setters can access data member values from outside of the owning type.

Getters and Setters

A getter is a member method that produces the value of a data member, while a setter is a member method that permits the value of a data member to be assigned. If the value of a data member should be accessible from outside of the owning type, it is necessary to create a getter and/or a setter method for that data member.

If the word public, protected, or private is given in front of a data member name, Lasso will automatically create a getter method and a setter method with the appropriate access level as described in the section on member methods. The following code defines three publicly accessible data members:

define person => type {
   data public firstName, public lastName
   data public age::integer=0
}

The automatically created getter method has the same name as the data member. Parentheses are optional after the getter (as they are with all methods accepting no parameters). The current value for the data member can be returned as follows:

#person->firstName
// => // Produces the value stored in the "firstName" data member

#person->lastName()
// => // Produces the value stored in the "lastName" data member

The automatically created setter permits the assignment (=) or the assign-produce (:=) operators to assign a new value to the data member. As with the getter, parentheses are optional.

// Sets "firstName" to a new value
#person->firstName = 'John'

// Sets "lastName" to a new value
#person->lastName() := 'Doe'
// => Doe

Exposing a data member in this manner always creates both the getter and setter. However, getters and setters can also be added manually without automatically exposing both get and set behaviors. One hypothetical use for this is a type that wants to provide to the outside world read-only access to one of its data members. Additionally, a getter or a setter can be added manually in order to override or replace the automatically provided behavior; perhaps to validate the values in a particular manner.

The following example defines a person type that manually exposes its “firstName” data member by defining two member methods, one for the getter and another for the setter. (See the section on member methods for more information on creating member methods.)

define person => type {

   // The firstName data member
   data firstName

   // The firstName getter
   public firstName() => {
      return .'firstName'
   }

   // The firstName setter
   public firstName=(value) => {
      .'firstName' = #value
   }
}

The type definition above would operate identically if it instead omitted the manual getter and setter methods and made its “firstName” data member public.

Implementing getter and setter methods for a data member allows assignment operators to be used with it. For example, since the +, -, and * operators are implemented for the string type (see the section on Operator Overloading below), they can be used to modify the “firstName” data member:

local(someone = person)
#someone->firstName = "Bob"
#someone->firstName += "by"   // Bobby
#someone->firstName -= "y"    // Bobb
#someone->firstName *= 2      // BobbBobb

Setters can be defined to accept more than one parameter. When called, the additional parameters are given in the method call’s parentheses, just as with a regular method. When defining such a setter method, the first parameter is always the new value for the assignent. All additional parameters follow. For example, with a “firstName” setter that includes an optional nickname:

public firstName=(value, nick) => {
   .'firstName' = `"` + #nick + `" ` + #value
}

it would be called like this:

#someone->firstName("Big Wheels") = "Bob"    // "Big Wheels" Bob

For another multi-parameter setter example, see security_registry->userComment=.

Within a manual getter or setter, it is vital to refer to the data member using the single-quoted name syntax. Otherwise, an infinite recursion situation may arise as the getter/setter continually re-calls itself.

Member Methods

A member method is a method that belongs to a particular type, as opposed to an unbound method which does not, thus acting as a standalone function. A member method can operate on the data members of its owning type in addition to any parameters the method may receive.

Member methods are created in sections of a type expression beginning with the word public, private, or protected, followed by a method signature, the association operator (=>), and the implementation of the method. Each section can define one or more methods separated by commas. The choice of word used to begin a member methods section influences how the methods are permitted to be accessed. There are three such access levels.

public
Public member methods can be called without any restrictions. They represent the public interface of the type. When the type is documented for others to use, only the public methods are described.
private
Private member methods can only be called from methods defined within the owning type. Private methods are to be used for low-level implementation details that shouldn’t be exposed to the end user or to inheriting types.
protected
Protected member methods can be called from within the owning type implementation or any type that inherits from that type. Protected methods represent functionality that is not intended to be exposed to the public, but which may be overridden, modified, or used from within types inheriting from the owning type.

The following type expression defines three data members and three member methods. The method describe returns a description of the person and is intended to be called by users of the type. The methods describeName and describeAge are private and protected methods, not intended to be used by the outside world.

define person => type {
   data
      public firstName,
      public lastName,
      public age

   public describe() => {
      return .describeName + ', ' + .describeAge
   }
   private describeName() => .firstName + ' ' + .lastName
   protected describeAge() => 'age ' + .age
}

Given the definition above, the following example illustrates valid and invalid use of a person object:

local(p) = person

#p->describe
// =>  , age

#p->describeAge
// => // FAILURE: access not permitted

The second usage fails because the describeAge method is protected. A type that inherits from person can access describeAge, but it cannot access describeName because that method is marked as private.

Inheritance

Every type inherits from one or more parent types. To inherit from another type means that every instance of the type will automatically possess all of the data members and methods of the parent type, plus those defined in the type expression itself. The concept of inheritance is used to build more complex types out of more generalized types.

A more general type may have several different more specific types inheriting from it as it provides a basic set of functionality that each inheriting type will also possess. Lasso only supports single-inheritance, that is, each type has only one immediate parent and that parent has only one immediate parent. All types can eventually trace down to a null parent. If a parent is not explicitly specified when a type is defined then the parent of the type is null.

All of the public or protected member methods belonging to a parent type will be made available to the types that inherit from it. Any method defined in a parent type that conflicts with those of an inheriting type will be replaced by the inheriting type’s method, unless the parent’s method was declared as frozen. This permits inheriting types to override or replace functionality provided by a parent.

Parent Section

The parent section names the parent that the type being defined is to inherit from. For example, the person type can inherit from the entity type by naming it in its parent section. Each person object that gets created will then possess all of the data members and methods found in the entity type, whatever those might be.

define person => type {
   parent entity
}

Only one parent type can be listed. The parent section can appear only once in a type expression. While it can be placed anywhere in the type expression, it is recommended that you place it at the top.

The following code defines two simple types: one and two. Type two inherits from type one. Notice that the second method is overridden by the second type, but the first method is not.

define one => type {
   public first() => 'alpha'
   public second() => 'beta'
   public last() => frozen 'omega'
}

define two => type {
   parent one
   public second() => 'gamma'
   public last() => 'zeta'
}

When the first method of a two object is called, the value “alpha” will be returned since it is automatically calling the method from the parent type. The second method returns “gamma” since it is calling the overridden method from type two. The last method always returns “omega” because the parent type defined it with the frozen keyword.

two->first
// => alpha
two->second
// => gamma
two->last
// => omega

Accessing Inherited Methods

Sometimes it is necessary to call “down” to an inherited method. A method inherited from an ancestor (any of the parents down the chain to null) can be accessed by using the inherited keyword followed by the target operator (->) followed by the method call (name and any parameters).

In the following example, the method third is defined to call the inherited method second. The method from type two will be bypassed in favor of the corresponding method from type one.

define one => type {
   public first() => 'alpha'
   public second() => 'beta'
}

define two => type {
   parent one
   public second() => 'gamma'
   public third() => inherited->second
}

two->third
// => beta

Equivalently, Lasso supports a shortcut syntax for targeting “inherited” by using two periods, which can be used to access the methods of a parent type. The example above can be rewritten using .. in place of inherited->.

define two => type {
   parent one
   public second() => 'gamma'
   public third() => ..second
}

Trait Section

Every type has a single trait which may be composed of other subtraits. A type inherits all of the methods that its trait defines, provided that the type implements the requirements for the trait. For example, a type must be serializable for it to be stored in a session, which means importing the trait_serializable trait. (See the Traits chapter for a complete description of how traits are created.)

The trait section of a type expression can import one or more other traits. These traits are combined to form the trait for the type. The following code shows a type definition that imports the trait_array and trait_map traits:

define mytype => type {
   trait {
      import trait_array, trait_map
   }
}

A trait section can appear anywhere within a type expression, but can appear only once.

Type Creators

A type creator is a method that returns a new instance of a type. For example, calling the method named string produces a new string object. By default each type has a creator method that corresponds to the name of the type and requires no parameters.

The example type person would automatically have a creator method person that returns a new instance of the type.

// Assigns a new person object to #myperson
local(myperson) = person()

If a type does not define its own creator method(s), it is provided with a default zero-parameter type creator. Attempting to provide parameters to a type creator that does not accept any parameters will fail.

local(myperson) = person(264)
// => // FAILURE: person() accepts no parameters

onCreate

Many types allow one or more parameters to be provided when a new object is created in order to customize the object before it is used. A type can specify its own type creators by defining one or more methods named onCreate. When a new object is created, the onCreate method corresponding to the given parameters is immediately called before the new object is returned to the user. Each onCreate must be a public member method.

To illustrate, the following type definition defines an onCreate method that requires three parameters: firstName, lastName, and birthdate. These parameters correspond to the data members of the type and allow setting their values when the object is first created. The creator simply assigns the parameter values to the data members.

define person => type {
   data firstName::string, lastName::string
   data birthdate::date

   public onCreate(firstName::string, lastName::string, birthdate::date) => {
      .'firstName' = #firstName
      .'lastName' = #lastName
      .'birthdate' = #birthdate
   }
}

To create an instance of this type, the creator must be called with the required parameters. The following code will create a new instance of the person type:

local(myperson) = person('Cathy', 'Cunningham', date('1/1/1974'))

Note that when a creator has been specified, the default creator, which requires no parameters, is not automatically provided. Lasso will not supply a default type creator when the author has included their own. Also note that if a type overrides its parent’s creator, it needs to include a call to the parent’s creator method, passing on any arguments as required.

public onCreate(...) => ..onCreate(: #rest)

Many type creators can be defined by specifying multiple onCreate methods. The following type defines three type creators. The first permits person objects to be created with no parameters; the second, with first and last names; and the third, with first and last names and a birthdate.

define person => type {
   data firstName::string, lastName::string
   data birthdate::date

   public onCreate() => {}
   public onCreate(firstName, lastName) => {
      .'firstName' = string(#firstName)
      .'lastName' = string(#lastName)
   }
   public onCreate(
            firstName::string,
            lastName::string,
            birthdate::date) => {
      .'firstName' = #firstName
      .'lastName' = #lastName
      .'birthdate' = #birthdate
   }
}

Callback Methods

In addition to the onCreate method, Lasso reserves a number of other method names as callbacks which are automatically used in different situations. Lasso provides default behavior so all callbacks are optional, but by defining a callback a type can customize its behavior.

asString

The asString method is called when an object is expressed as a string. By default, a type instance will simply output the name of the object’s type. Overriding this method allows a type to control how it is output. The following code defines a simple type that outputs a greeting when its asString method is called:

define mytype => type {
   public asString() => 'Hello World!'
}

mytype
// => Hello World!

onCompare

The onCompare method is called whenever an object is compared against another object. This includes when using the equality (==), and inequality (!=) operators, and when objects are compared for ordinality using any of the relative equality operators (<, <=, >, >=). It’s also called via null->onCompareStrict, which first verifies that the two objects are the same type, when using the strict equality (===) and inequality (!==) operators.

An onCompare method must accept one parameter and must return an integer value.

public onCompare(rhs)::integer

If the parameter is equal to the current type instance then a value of “0” should be returned. If the current type instance is less than the parameter then an integer less than zero should be returned, e.g. “-1”. If the current type instance is greater than the parameter then an integer greater than zero should be returned, e.g. “1”.

For example, the following person type has an onCompare method that gives person objects the ability to compare themselves with each other:

define person => type {
   data public firstName::string,
         public lastName::string

   public onCompare(other::person) => {
      .firstName != #other->firstName ?
         return .firstName < #other->firstName ? -1 | 1
      .lastName != #other->lastName ?
         return .lastName < #other->lastName ? -1 | 1
      return 0
   }

   public onCreate(firstName::string, lastName::string) => {
      .firstName = string(#firstName)
      .lastName = string(#lastName)
   }
}

Given the above type definition, the following examples use the onCompare method behind the scenes to provide the ability to compare persons:

person('Bob', 'Barker') == person('Bob', 'Barker')
// => true

person('Bob', 'Barker') == person('Bob', 'Parker')
// => false

Multiple onCompare methods can be provided, each specialized to compare against particular object types. For example, an integer type would want to permit itself to be compared against other integer objects, but it should also want to be comparable to decimal objects. Such an integer type would have one onCompare method for integer objects and another for decimal objects. This example also shows how the onCompare method can be manually called on objects. In this case, the “value” data member is responsible for doing the actual comparisons, so its onCompare method is called and the value returned.

define myint => type {
   data private value

   public onCompare(i::integer) => .value->onCompare(#i)
   public onCompare(d::decimal) => .value->onCompare(integer(#d))
}

contains

The contains method is called whenever an object is compared using the contains (>>) or not contains (!>>) operators. A contains method definition should accept one parameter and must return a boolean value, either “true” or “false”.

public contains(rhs)::boolean

If the parameter is contained within the current type instance (using whatever logic makes sense for the type) then a value of “true” should be returned; otherwise, a value of “false” should be returned.

For example, the type odds below overrides the contains operators so that odds >> 3 returns “true” and odds >> 4 returns “false”.

define odds => type {
   public contains(rhs::integer)::boolean => {
      return #rhs % 2 == 1
   }
}

Other types that implement their own contains methods include array and map, which search their contained objects for a match before returning “true” or “false”.

invoke

The invoke method is called whenever an object is invoked by applying parentheses to it. By default, invoking an object produces a copy of the invoked object. However, objects can add their own invoke methods to alter this behavior. The following code shows how an instance of the person type might be invoked:

define person => type {
   data
      public firstName::string,
      public lastName::string

   public invoke() => .firstName + ' ' + .lastName + ' was invoked!'
   public onCreate(firstName::string, lastName::string) => {
      .firstName = string(#firstName)
      .lastName = string(#lastName)
   }
}

The following shows how a person object would be invoked, by either directly calling the invoke method or by applying parentheses:

local(per) = person('Bob', 'Parker')

#per()
// => Bob Parker was invoked!

#per->invoke
// => Bob Parker was invoked!

_unknowntag

Implementing the _unknowntag method allows a type to handle requests for methods that it does not have. When a search for a member method fails, the system will call the _unknowntag method if it is defined. The originally sought method name is available by calling method_name.

The following example creates a type whose only member method is _unknowntag, which returns the name of the called method:

define echo_method => type {
   public _unknowntag => method_name->asString
}

echo_method->rhino
// => rhino

Operator Overloading

Types can provide their own routines to be called when the standard arithmetical operators (+ - * / %) are used with an instance of the type on the left-hand side of the expression.

If the standard operators are overloaded they should be mapped as closely as possible to the standard arithmetical meanings of the operators. For example, the addition operator (+) is also used for string concatenation.

Overloading Arithmetical Operators

An arithmetical operator is overloaded by defining a member method whose name is the same as the operator symbol. The method must accept one parameter and return an appropriate value. The type instance should not be modified by these operations.

public +(rhs)
public -(rhs)
public *(rhs)
public /(rhs)
public %(rhs)

The following example provides a full set of arithmetical operators for the myint type:

define myint => type {
   data private value

   public onCreate(value = 0) => { .value = #value }
   public asString() => string(.value)
   public +(rhs::integer) => myint(.value + #rhs)
   public -(rhs::integer) => myint(.value - #rhs)
   public *(rhs::integer) => myint(.value * #rhs)
   public /(rhs::integer) => myint(.value / #rhs)
   public %(rhs::integer) => myint(.value % #rhs)
}

myint(9) + 5 * 40
// => 209

Overloading Equality Operators

See the section on the onCompare method for information about how to overload the equality operators (==, !=, <, <=, >, >=, ===, !==).

Overloading Containment Operators

See the section on the contains method for information about how to overload the containment operators (>>, !>>).

Modifying Types

Lasso permits types to have methods added to them outside of the original defining type expression. This is done by defining the method using the word define followed by the name of the type, the target operator (->), and then the rest of the method signature and body. The following example adds the method speak to the person type:

define person->speak() => 'Hello, world!'

Type/Object Introspection Methods

Lasso provides a number of methods that can gain information about a type or object. These methods are summarized below, and can be called by any type.

null->type()

Returns the type name for any type instance. The value is the name that was used when the type was defined.

null->isA(name::tag)

Checks whether an instance of an object is of the given type, returning an integer indicating the result.

0:The given type has no relation to the object.
1:The name parameter matches the type of the instance. (The method call null->isA(::null) will only return “1” for the null type instance itself.
2:The name parameter matches a trait implemented by the type of the instance, or one of its parents.
3:The name parameter matches the parent type of the instance.
null->isNotA(name::tag)

The opposite of null->isA.

null->listMethods()

Returns a staticarray of signature objects for all of the methods that are available for the type.

null->hasMethod(name::tag)

Returns “true” if the type implements a method with the given name.

null->parent()

Returns the name of the parent of the target object. If the method returns “null” then the final parent has been reached.

null->trait()

Returns the trait for the target object. Returns “null” if the object does not have a trait.

See also

setTrait and addTrait methods in the Traits chapter

type tag

An immutable object that represents a unique string of characters. Since Lasso uses tags internally to keep track of names, this type has member methods that can query them.

tag->istype() → boolean

Check if a type with the same name as the given tag exists.

::string->istype
// => true
tag->gettype()

Create an instance of a type matching the given tag. This is useful for calling listMethods on a type that has no literal syntax or simple type creator.

::regexp->gettype->listmethods
// => ... regexp->input(), regexp->replacepattern(), regexp->findpattern(), ...
tag->doccomment()
tag->doccomment=(value::string)

Retrieve and set doc comments for a type matching the given tag. Requires that Lasso be run with the LASSO9_RETAIN_COMMENTS variable enabled.

See also

Tag Literals and Doc Comments in the Literals chapter