Traits¶
Traits provide a way to define type functionality in a modular fashion. Each trait includes a set of reusable method implementations along with a set of requirements that must be satisfied in order for the included methods to function properly.
Trait Logic¶
Traits allow creating a hierarchy of types that share common functionality without relying on single or multiple inheritance. Traits are similar to mixins and abstract classes found in other languages.
Each trait encapsulates a set of requirements and provides a set of member methods. When a trait is applied to a type, the requirements are checked. If they are satisfied, the provided member methods are added to the type as if they had been implemented directly in the type. Traits can only define public member methods.
Lasso includes many types that have common member methods. For example, the
pair
, array
, string
, and other types implement
first
, second
, and last
methods which return the named element.
array(1, 2, 3, 4)->last
// => 4
'Quick brown fox'->second
// => u
pair('name'='John')->first
// => name
The first
method can be implemented by calling the get(x)
member method
of each type with a parameter of “1”. The second
method calls it with a
parameter of “2”. The last
method calls the get(x)
method with a
parameter defined by the size of the type (usually found by calling the size
member method).
The requirements for implementing the first
, second
, and last
methods are that the type has to have get(x)
and size
member methods. In
a trait this requirement would be specified as follows:
require get(x::integer)
require size()::integer
The requirements take the form of a list of member method signatures. If the type that the trait is applied to defines all of the trait’s required member method signatures, the methods provided by the trait will work.
The methods provided by the trait are specified similar to how methods are
defined in custom types. (However, instead of using the public
keyword, the
method definition starts with the provide
keyword.) The implementation for
the first
, second
, and last
methods would appear as follows:
provide first() => .get(1)
provide second() => .get(2)
provide last() => .get(.size)
Note that the period notation is used to call the member methods of the current
object; the same as it would be used within a custom type implementation. The
implementation of the provided methods can make use of the get
and size
member methods because the requirements ensure that they will be available.
The full trait definition for trait_firstLast
would be as follows:
define trait_firstLast => trait {
require get(x::integer)
require size()::integer
provide first() => .get(1)
provide second() => .get(2)
provide last() => .get(.size)
}
If we define a new type (e.g. month
) that supports get
and size
,
we can import this trait to automatically get an implementation of first
,
second
, and last
.
define month => type {
trait {
import trait_firstlast
}
data y, m
public onCreate(year::integer, month::integer) => {
.'y' = #year
.'m' = #month
}
public get(x::integer) => {
return date(-year=.'y', -month=.'m', -day=#x)
}
public size()::integer => {
local(temp) = date(-year=.'y', -month=.'m'+1, -day=1)
#temp->subtract(-day=1)
return #temp->dayofmonth
}
}
Defining Traits¶
A trait is defined using a trait expression consisting of the define
keyword followed by the trait name, the association operator (=>
), the
keyword trait
, and a code block containing the definition of the trait.
define myTrait => trait {
// ...
}
The code block contains one or more sections which are each identified by a label. Method implementations that are provided by the trait are specified in a provide section. Requirements for the trait are specified in a require section. Other traits can be imported in an import section.
provide¶
The member methods that a trait provides are specified similarly to the public
section of a type definition. The provide section begins with the keyword
provide
, which is followed by a comma-separated list of member method
definitions. The member list has the same form as custom method definitions.
Each method is defined using a signature, the association operator (=>
), and
an expression or code block that defines the implementation of the method.
The following trait would provide two member methods for getting and setting a data member:
define myTrait => trait {
provide getFirstName() => {
return .firstName
}
provide setFirstName(value::string) => {
.firstName = #value
}
}
require¶
The require section allows specifying a list of method signatures that are required for the trait to operate properly. The signatures may be simple method names, or they may be complete signatures with parameter specifications. As many require sections as are necessary can be specified.
The section begins with the keyword require
followed by a comma-separated
list of method signatures. The following trait requires a getter and setter for
the “firstName” data member:
define myTrait => trait {
require firstName, firstName=
provide getFirstName() => {
return .firstName
}
provide setFirstName(value::string) => {
.firstName = #value
}
}
import¶
The import section allows the characteristics of other traits to be imported into this trait definition, thus allowing a hierarchy of traits to be defined. As many import sections as are necessary can be specified.
The section begins with the keyword import
followed by a comma-separated
list of trait names. The following trait simply imports the characteristics of
the built-in trait_array
trait:
define myTrait => trait {
import trait_array
}
All of the requirements and provided member methods of the imported trait will be added to the trait being defined. The requirements of one of the traits may be satisfied by the methods provided by another trait.
However, if two traits provide the same member method, there will be a conflict. The conflict is resolved by eliminating both implementations of that member method and adding a requirement for it to the trait. The type that the trait is ultimately applied to must implement that member method in order for the trait to be applied.
Trait Composition¶
Traits can be combined together into new traits using the +
operator. This
is called “composing” a new trait. The result of this expression will be a trait
that has all the requirements and provides all the member methods of the traits
that have been combined.
The same rules that are used for importing traits apply to composed traits, such as traits satisfying each others’ requirements and resolving conflicting method names.
An alternate method of defining the trait example from the start of this chapter
would be to define three subtraits and then use the composition operator (+
)
to compose them into a single trait.
define trait_first => trait {
require get
provide first() => .get(1)
}
define trait_second => trait {
require get
provide second() => .get(2)
}
define trait_last => trait {
require get, size
provide last() => .get(.size)
}
define trait_firstLast => trait_first + trait_second + trait_last
Replacing the last line with the trait definition below would produce exactly the same result. In general, the latter method is preferred for trait definitions, while the trait composition is preferred for runtime changes.
define trait_firstlast => trait {
import trait_first
import trait_second
import trait_last
}
Checking Traits¶
Since traits provide member methods for a type it is often useful to check
whether a given type instance has a trait applied. The isA
method can
be used for this check. This member method can be used on any type instance, and
will return a positive integer if the instance is the provided type or has the
provided trait name applied to it.
In this code the isA
method returns “2” since the month
type has
the trait_firstLast
trait applied to it:
local(mymonth) = month(2008, 12)
#mymonth->isA(::trait_firstlast)
// => 2
Applying Traits¶
Traits can be applied to types as part of the type definition. This makes the trait an integral part of the type definition. The provided member methods are indistinguishable to the user of the type from member methods that are implemented directly in the type.
Each type definition can include a single trait section. The trait can import as many traits as are needed.
define myType => type {
trait {
import ...
}
data ...
public ...
}
When an instance of the type is created, the instance has the specified trait applied to it automatically.
The trait of any object in Lasso can be programmatically manipulated using the
trait
, setTrait
, and addTrait
methods described
in the next section.
Trait Manipulation Methods¶
-
null->
trait
(t::trait) Returns the trait for the target object. Returns “null” if the object does not have a trait.
-
null->
setTrait
(t::trait)¶ Sets the trait of the target object to the parameter, replacing the existing trait.
-
null->
addTrait
(t::trait)¶ Combines the target object’s trait with the parameter.
In general, traits will be added to a type instance to provide additional functionality rather than resetting the entire trait for a given object. The two examples below are equivalent:
#myinstance->addtrait(trait_firstlast)
#myinstance->settrait(#myinstance->trait + trait_firstlast)
Caution
The setTrait
method should be used with care since resetting the
trait of a type instance may result in many of its member methods becoming
unavailable or ceasing to function.