Lasso Language Features

The Lasso programming language has a number of great features that make coding in it enjoyable. This tutorial will scratch the surface of some of the best features of Lasso while also giving an introduction to defining methods, types, and traits.

Type Constraints

Lasso allows programmers to specify that a variable they create can only store objects of a specific type or trait. The following example creates a local variable that can only store integer values:

local(myInt::integer) = 5
#myInt = 8
#myInt = '44'
// => // Throws an error since we are trying to assign a string

This syntax also works for type-constraining thread variables.

Methods

Defining your own methods in Lasso is extremely easy. The following example returns the time of day (“morning”, “afternoon”, or “evening”) given a specified hour:

define time_of_day(hour::integer) => {
   // Check to make sure the hour value is valid
   fail_if(#hour < 0 or #hour > 23,
      error_code_invalidParameter,
      error_msg_invalidParameter + ': hour must be between 0 and 23'
   )

   if(#hour >= 5 and #hour < 12) => {
      return 'morning'
   else(#hour >= 12 && #hour < 17)
      return 'afternoon'
   else
      return 'evening'
   }
}

The first line contains the define keyword, followed by the name for the method and its the parameter list in parentheses (the method signature), followed by the association operator (=>) and an open brace. All the code between that open brace and its matching closing brace is the capture associated with the method, which is executed when the method is called.

The method starts by making sure that the hour passed to it is valid. If it is, then the code that determines the time of day will run and return the proper value.

Notice that the type constraint in the method definition’s signature constrains hour to be an integer object. This enables a handy feature in Lasso called “multiple dispatch”. Let’s say we want a similar function that accepts a date object. No need for a different method name; instead we can define that method like this:

define time_of_day(datetime::date=date) => time_of_day(#datetime->hour)

This defines a second method that also has the name “time_of_day”, but accepts a date object and returns the value of calling the time_of_day method that takes an integer, passing it the hour of the date object. This method definition doesn’t have a capture associated with it. If your method is going to just return the value of an expression, you can put that expression to the right of the association operator. It’s equivalent to this code:

define time_of_day(datetime::date=date) => {
   return time_of_day(#datetime->hour)
}

Besides multiple dispatch, methods can also have optional parameters and named parameters. In the time_of_day example method that takes a date object, the datetime parameter is actually optional: the current date and time will be used if no value is passed. See the Methods chapter for more information on parameter definition and use.

Types

Lasso is an object-oriented language that comes with a number of core types already defined, but you can also create your own types. Below is a simple type definition to demonstrate how:

define person => type {
   data public nameFirst::string
   data
      public nameMiddle::string,
      public nameLast::string

   public onCreate(first::string, last::string, middle::string='') => {
      .'nameFirst'     = #first
      .'nameMiddle'    = #middle
      self->'nameLast' = #last
   }

   public nameFirstLast => self->nameFirst + ' ' + .nameLast
}

The type definition starts off with the define keyword followed by the type name, the association operator, the type keyword, and finally the braces for the capture containing the type definition code. The definition starts with two data sections that define three data members for the type. Two member methods are then defined using the access level keyword public instead of the define keyword. The onCreate methods are special for types: they define type creator methods that are automatically called when you create instances of your type. The following code would use the person->onCreate method to create an object of type “person” and then output their first and last name:

local(cool_dude) = person('Bill', 'Doerrfeld')  // "middle" is defined as an optional parameter
#cool_dude->nameFirstLast

// => Bill Doerrfeld

Types in Lasso also have single inheritance and can implement and import traits, described next. For more information on types, see the Types chapter.

Traits

Traits are a great way to package up and make available reusable code for types. If there is functionality that needs to be shared between different types, it can be packaged up as a trait instead of creating a different implementation for each type or forcing a complex inheritance scheme.

Defining traits is similar to defining types. The following example is a slightly modified version of the definition for trait_positionallyKeyed:

define ex_trait_positionallyKeyed => trait {
   import trait_doubleEnded

   require size()::integer, get(key::integer)

   provide
      first()  => (.size > 0 ? .get(1) | null),
      second() => (.size > 1 ? .get(2) | null),
      last()   => (.size > 0 ? .get(.size) | null)
}

The definition starts with the define keyword followed by the name of the trait, the association operator, the trait keyword, and then a set of braces enclosing the trait definition. There are then three sections that start with their own keyword:

import
This section can contain a comma-separated list of traits that the current trait implements. In this case, because our trait implements a first and last method, it can import trait_doubleEnded which allows for types that use this trait to also get the methods that trait_doubleEnded provides. (Alternatively, if trait A imports trait B but doesn’t implement trait B’s required traits, then any type that imports trait A must also meet the requirements for trait B by implementing the missing methods.)
require
This section can contain a comma-separated list of method signatures that must be implemented by any type wanting to import this trait. In this case it requires a size method that returns an integer and a get method that takes a single integer parameter.
provide
This section can contain a comma-separated list of method definitions. This is where the reusable code is defined that types importing this trait will be able to access.

The result of this trait definition is that types defining a size method and a get method can import this trait and have the following methods available as member methods: first, second, last. For more information on defining and using traits, see the Traits chapter.

Query Expressions

Query expressions allow programmers to create highly readable code that can do complex manipulation of data sets. Here is a quick example:

local(data_set) = (: 42, 11, 72, 13, 14, 88, 92, 35)

with number in #data_set
where #number % 2 == 0
skip 1
take 3
sum #number

// => 174

Every query expression starts as with newLocalName in trait_queriable, where newLocalName becomes the name of a local variable only accessible in the query expression, and trait_queriable is an object whose type implements and imports trait_queriable, such as the staticarray in the example.

After this initial with clause, a query expression can have zero or more operation clauses that each start with their own keyword. The example above uses three: where which filters the input using an expression, skip which skips a set number of elements, and take which returns a set number of elements. Order does matter.

Every query expression ends with one action clause that specifies what should be done for each iteration. In this case, we’re using the sum action to add each value in the iteration together. Other actions are min, max, average, and select, which returns a new set of values rather than a single value; and do, which runs a block of code for each value.

The example above iterates over each element in the staticarray and first tests to see if it is an even number. It then skips the first even number it finds and only executes the sum action on the next three. The end result is that it adds 72, 14, and 88 together.

The best part about query expressions is that most of the actions are lazily executed. This means you can store a query expression in a variable, and it will wait to be executed until the value for the variable is expected. For a more thorough description, see the Query Expressions chapter.