The Great Lambda

Introduction

This tutorial shows how to use the Lambda class. There are two examples below. The first example manipulates an Array of numbers using the Lambda class. The second example manipulates a List of words using the List and Lambda classes. Both examples perform similar operations on the data.

Lambda Functions

The Lambda class allows us to operate on an entire Iterable at once. This is often preferable to looping routines since it is less error prone and easier to read. For convenience, the List class contains some of the frequently used methods from the Lambda class.

The Lambda class gives us the following capabilities:

  • count the number of elements (count)
  • determine if the Iterable is empty (empty)
  • determine if the specified element is in the Iterable (has)
  • determine if any element satisfies a criteria (exists)
  • find out the index of the specified element (indexOf)
  • determine if every element satisfies a criteria (foreach)
  • call a function for each element (iter)
  • find the elements that satisfy a criteria, returning a new List (filter)
  • apply a conversion to each element, returning a new List (map)
  • functional fold, which is also known as reduce, accumulate, compress or inject. For more info, see wikipedia (fold)

Arrays, Lists, and Iterables

The Lambda class operates on Iterables. Iterables are typedefs, so any class that has an iterator method is by definition an Iterable. The Array and List classes are both Iterable since they have this method. User defined classes can be Iterables as well by simply defining an appropriate iterator method inside the class. This allows a great deal of flexibility in where Lambda methods can be utilized.

The Lambda.array and Lambda.list functions in the Lambda class convert any Iterable into an Array or List, respectively. Both functions allocate memory for a new data structure and copy each element. The Lambda.map and Lambda.filter functions return Lists even if the input was an Array.

The first example stores the data in an Array and uses Lambda functions directly. The second example stores the data in a List and uses the Lambda class or, where possible, makes calls through the List class's interface.

Here you can find more information about using Lambda with arrays and lists: Using Lambda .map() and .filter().

Count

Lambda.count returns the number of elements in an Iterable. If the Iterable is a Array or List it is faster to use the length property, but the example uses count anyway to show its usage.

Empty

There are several ways to check if an Iterable is empty. For Arrays and Lists, it is best to use the isEmpty method. For all other Iterables it is best to use the Lambda.empty function. It is possible to compare the length (or result of Lambda.count) to zero, but this is slower.

Calling Lambda Functions

Most Lambda functions are called in similar ways. The first argument for all of the Lambda functions is the Iterable on which to operate. Many also take a function as an argument.

It is helpful to look at an example. The exists function is specified as:

static function exists<A>( it : Iterable<A>, f : A -> Bool ) : Bool

The argument it is the Iterable that Lambda will traverse. The f argument is a function that will be called on each element in it. The definition for exists defines f as taking a single argument with the same type as the it elements, and returning a Bool.

f can be an anonymous function specified in the function call itself (as in the calls to exists in the examples below) or can be a named function (as in the calls to filter in the examples below).

Output

HaXe has built-in routines for converting Arrays to Strings. These routines are automatically called when Arrays are printed using neko.Lib.println. The elements are concatenated together, separated with a comma and space, and surrounded by square brackets. The conversion routine for Lists is similar but it uses curly brackets instead of square brackets.

Number Example


class LambdaNumberTest
{
  public static function main()
  {
    // create and output an array of numbers
    var nums = [1, 3, 5, 6, 7, 8];
    trace("nums: " + nums);

    // count the numbers in the array, see if the array is empty
    trace("count: " + Lambda.count(nums));
    trace("is empty: " + Lambda.empty(nums));

    // check for individual elements in the array
    trace("contains 2: " + Lambda.has(nums, 2));
    trace("contains 3: " + Lambda.has(nums, 3));

    // check if any element fits a criteria
    trace("contains an element less than 10: "
             + Lambda.exists(nums, function(ii) { return ii<10; }));
    trace("contains an element greater than 10: "
             + Lambda.exists(nums, function(ii) { return ii>10; }));

    // check if all elements fit a criteria
    trace("all elements less than 10: "
             + Lambda.foreach(nums, function(ii) { return ii<10; }));
    trace("all elements greater than 10: "
             + Lambda.foreach(nums, function(ii) { return ii>10; }));

    // find even elements from the array
    var isEven = function(num) { return Math.floor(num/2) == num/2; }
    trace("even: " + Lambda.filter(nums, isEven));

    // multiply each element by 2
    var timesTwo = function(num) { return num*2; }
    trace("times two: " + Lambda.map(nums, timesTwo));

    // get the sum of all elements
    var sum = function(num, total) { return total += num; }
    trace("sum: " + Lambda.fold(nums, sum, 0));
  }
}

Compile and run with:

haxe -neko lambda.n -main LambdaNumberTest.hx 
neko lambda.n

The output should be:

LambdaNumberTest.hx:7: nums: [1, 3, 5, 6, 7, 8]
LambdaNumberTest.hx:10: count: 6
LambdaNumberTest.hx:11: is empty: false
LambdaNumberTest.hx:14: contains 2: false
LambdaNumberTest.hx:15: contains 3: true
LambdaNumberTest.hx:18: contains an element less than 10: true
LambdaNumberTest.hx:20: contains an element greater than 10: false
LambdaNumberTest.hx:24: all elements less than 10: true
LambdaNumberTest.hx:26: all elements greater than 10: false
LambdaNumberTest.hx:31: even: {6, 8}
LambdaNumberTest.hx:35: times two: {2, 6, 10, 12, 14, 16}
LambdaNumberTest.hx:39: sum: 30

Word Example


Note that since there is no way to create and populate a list in a single statement we create words as an Array and convert it to a list with the Lambda.list function.
class ListWordTest
{
  public static function main()
  {
    // create a list of words
    var words = Lambda.list(['car', 'boat', 'cat', 'frog']);
    trace("words: " + words);

    // join the words into a string
    trace("join: " + words.join('_'));

    // count the words, see if the list is empty
    trace("count: " + Lambda.count(words));
    trace("is empty: " + words.isEmpty());

    // check for individual elements in the list
    trace("contains cat: " + Lambda.has(words, 'cat'));
    trace("contains tree: " + Lambda.has(words, 'tree'));

    // check if any element fits a criteria
    trace("contains an element less than 5 letters: "
             + Lambda.exists(words, function(ii) { return ii.length<5; })); 
    trace("contains an element greater than 5 letters: "
             + Lambda.exists(words, function(ii) { return ii.length>5; }));

    // check if all elements fit a criteria
    trace("all elements are less than 5 letters: "
             + Lambda.foreach(words, function(ii) { return ii.length<5; }));
    trace("all elements are greater than 5 letters: "
             + Lambda.foreach(words, function(ii) { return ii.length>5; }));

    // find three letter elements from the list
    var isThreeLetters = function(word) { return word.length==3; }
    trace("three letter words: " + words.filter(isThreeLetters));

    // capitalze each word
    var capitalize = function(word) { return word.toUpperCase(); }
    trace("capitalized: " + words.map(capitalize));

    // count the letters in all words
    var countLetters = function(word, total) { return total+=word.length; }
    trace("total letters: " + Lambda.fold(words, countLetters, 0));
  }
}

Compile and run with:

haxe -neko list.n -main ListWordTest.hx 
neko list.n

The output should be:

ListWordTest.hx:7: words: {car, boat, cat, frog}
ListWordTest.hx:10: join: car_boat_cat_frog
ListWordTest.hx:13: count: 4
ListWordTest.hx:14: is empty: false
ListWordTest.hx:17: contains cat: true
ListWordTest.hx:18: contains tree: false
ListWordTest.hx:21: contains an element less than 5 letters: true
ListWordTest.hx:23: contains an element greater than 5 letters: false
ListWordTest.hx:27: all elements are less than 5 letters: true
ListWordTest.hx:29: all elements are greater than 5 letters: false
ListWordTest.hx:34: three letter words: {car, cat}
ListWordTest.hx:38: capitalized: {CAR, BOAT, CAT, FROG}
ListWordTest.hx:42: total letters: 14

Type Inference


We use type inference to improve code readability, but we could have declared each variable's type explicitly. If doing so nums would be defined as:
    var nums : Array<Int> = [1, 3, 5, 6, 7, 8];

words could be defined as:
    var words : List<String> = Lambda.list(['car', 'boat', 'cat', 'frog']);

isEven could be defined as:
    var isEven : Int->Bool = function(num:Int) 
    { return Math.floor(num/2) == num/2; }

countLetters could be defined as:
    var countLetters : String->Int->Int = function(word:String, total:Int) 
    { return total+=word.length; }

version #15990, modified 2013-02-20 12:13:29 by shohei909