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 callnull->isA(::null)
will only return “1” for thenull
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->
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.
-
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