d

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.

15 St Margarets, NY 10033
(+381) 11 123 4567
ouroffice@aware.com

 

KMF

Binding Patterns in Ballerina – DZone Web Dev

The term “binding pattern” in Ballerina denotes a way of allowing different parts of a single structured value to have each of its parts assigned to separate variables at the same time given that all variables in the binding pattern are distinct. The concept of binding patterns makes it easier to extract information, especially from structured values in a much cleaner way. 

Shown below is a typical example of a binding pattern where fields name and age of the Person type record named tom are being matched and assigned to variables named firstName and personAge.

import ballerina/io;

type Person record {
    string name;
    int age;
};

public function main() {
    Person tom = {
        name: "Tom",
        age: 25,
    };

    // Binding pattern    
    Person {name: firstName, age: personAge} = tom; 

    io:println("Name: ", firstName);
    io:println("Age: ", personAge);
}

The above code will produce the below output in which we can see that the newly created variables can now be utilized independently and that they carry the expected information derived from the Person type record named tom

In properly understanding binding patterns, we must first understand typed binding patterns. 

Typed Binding Patterns

A typed binding pattern refers to the combination of a type and a binding pattern. It is used to create the variables that occur in the binding pattern. 

Shown below is an example of the simplest form of a binding pattern. It consists of just the type and the variable name, in this case, int and a.

In the below example var is used instead of the type. In such a case, the type will be inferred from the expression on the right-hand side, in this case from 2.

The below example denotes a binding pattern involving a structured type where the structured type is a tuple and the types of members in the tuple are defined as int and string. As opposed to the above examples, we can see that there are two separate variable names here: x and y. These variable names correspond to the types of the members defined in the tuple type, x as having int type and y as having string type. This creates two separate variables: x of type int and y of type string and the members of the tuple type value in the right-hand side expression will be assigned to x and y in their order of occurrence. 

  [int, string] [x, y] = [10, " World"]; 

There are 2 ways in which typed binding patterns can be used:

  1. Single use approach
  2. Iterative approach

Single Use Approach

Single use approach is usually followed by an = operator. All the examples that we discussed previously can be categorized as examples of the single use approach. The only exception where the single use approach is not followed by an = operator is when it is used with an on fail clause. This will be discussed further in the Error Binding Pattern section. 

Iterative Approach

The iterative approach is always followed by the in keyword. In this approach, an iterator gets created as a result of evaluating an expression, and the typed binding pattern is matched against each value returned by the iterator. The in keyword can be used with foreach statement, join clause, from clause, and with let expression. 

The below example depicts an iterative approach using the foreach statement. In this case, the typed binding pattern is the [int, string] [i, v] portion where the type is [int, string] and the variable names are i and v

public function main() {
  string tempString = "";
  [int, string][] arr = [[1, "A"], [2, "B"], [0, "C"]];

  foreach [int, string] [i, v] in arr {
    tempString = tempString + i.toString() + ":" + v + " ";
  }
}

Since now we have a good understanding of typed binding patterns, let us explore more about binding patterns. 

Binding Patterns

Binding patterns in Ballerina can be mainly classified into 2 separate groups:

  1. Simple binding patterns
  2. Structured binding patterns

The below image illustrates a high-level classification of binding patterns in Ballerina.

          

Simple Binding Pattern

A simple binding pattern usually matches any value. However, there can be an exception, and it will be discussed in the Wildcard Binding Pattern section. Given that the simple binding pattern consists of a variable name, it then causes the matched value to be assigned to the named variable. There are 2 types of binding patterns that classify as simple binding patterns:

  1.  Capture binding pattern
  2.  Wildcard binding pattern

Capture Binding Pattern

A capture binding pattern always assigns a matched value to a named variable, also known as an identifier. The capture binding pattern will contain a value if the binding pattern successfully matches and the type of that value will be the type of that variable.

Discussed below are a few examples of the different ways in which the capture binding pattern can be used.

Shown below is a typical example of a capture binding pattern where the value text, which is of string type, gets matched to the type of the binding pattern string a, resulting in a successful match and causing the value text to be assigned to the variable named a.

Unlike in the previous example, since var is used here instead of the type, the type of the capture binding pattern will be inferred from the expression on the right side — in this case from true.

In this example, the inferred type of the capture binding pattern will be int|error, not just int. This is because int:fromString("abc") is bound to result in an error. In such a case where the intersection of error and the inferred type, which in this case is int is nonempty, f gets assigned an error. 

var f = int:fromString("abc");

In the below example since the value text of string type cannot be matched with the type of the capture binding pattern which is int, the match fails and it results in a compilation error. 

int c = "text"; // ERROR: incompatible types: expected 'int', found 'string'

Wildcard Binding Pattern

A wildcard binding pattern matches a value if the value belongs to type any. The wildcard binding pattern is denoted by the _ symbol. What it does is that as long as the basic type of the value is not an error, it does not cause any assignments to be made to the variable. In other words, it does not create a match with error types, thus, if the basic type of the value is an error, it will result in a compilation error. Furthermore, the any type will be assigned as the type of the wildcard binding pattern if it successfully matches a value.

Discussed below are a few examples of the different ways in which the wildcard binding pattern can be used.

In the below example, since the basic type of value true is boolean and it also belongs to the any type, this will not cause an assignment and the value will be ignored.

In this case, as in the previous example since the basic type of the value returned by testFunction() is of int type and it also belongs to any type, this will not cause an assignment and the value will be ignored.

public function main() {
    int _ = testFunction(); 
}

function testFunction() returns int {
    return 3;
}

The type of the wildcard binding pattern in these examples will be derived as any as they result in a successful match.

public function main() {
    _ = 3;
    _ = testFunction(); 
}

function testFunction() returns int {
    return 3;
}

Since the value we are trying to ignore here is of type string and it belongs to any type, this will not result in a compilation error.

public function main() {
    var [a, b, _] = testFunction();
}

function testFunction() returns [int, error, string]{
    return [1, error("EROR"), "text"];
}

These examples will result in compilation errors since the basic type of the value attempted to be matched is error and error type does not belong to any type.

public function main() {  
    var _ = int:fromString("xyz"); // ERROR : a wildcard binding pattern can be used only with a value that belong to type 'any'
    var [d, _, c] = testFunction(); // ERROR : a wildcard binding pattern can be used only with a value that belong to type 'any'    
}

function testFunction() returns [int, error, string]{
    return [1, error("EROR"), "text"];
}

Structured Binding Pattern

A structured binding pattern matches structural values. Structural values are containers for other values that are called their members. A structured binding pattern assigns each part of the matched structural value to separate variables. There are 3 types of binding patterns that classify as structured binding patterns:

  1. List binding pattern
  2. Mapping binding pattern
  3. Error binding pattern

List Binding Pattern

A list binding pattern binds members of a list. Unlike in the capture and wildcard binding patterns, these binding patterns have the ability to contain more than one binding pattern in their container-like structure. 

A list binding pattern with m number of binding patterns will match a list with n members given that:

  • m is less than or equal to n
  • ith binding pattern matches the ith member of the list for each i in 1 to m.
  • If m is less than n the list binding pattern includes a rest binding pattern (...v).

If these points are satisfied, a successful match would occur. In the case of having a rest binding pattern, a successful match creates a new list value consisting of all members of the matched list except for the first m number of values and it is assigned to v. If a successful match occurs with the list binding pattern, the type of the list binding pattern will be a tuple type. Furthermore, the list binding pattern can be employed with both tuples and arrays.

Discussed below are a few examples of the different ways in which the list binding pattern can be used.

Shown below is an example of an array binding pattern. The 3 resulting variables score1, score2 and score3 can be used independently given that it results in a successful match. 

import ballerina/io;

public function main() {
   float[3] scores = [6.2, 4.5, 7.5];

   [float, float, float] [score1, score2, score3] = scores;

   io:println("score1: ", score1);
   io:println("score2: ", score2);
   io:println("score3: ", score3);
}

This example will result in a compilation error as destructuring the value to separate variables is not possible with open arrays without having a rest binding pattern (...v).

float[] scores = [6.2, 4.5, 7.5];
[float, float, float] [score1, score2, score3] = scores; // ERROR: incompatible types: expected '[float,float,float]', found 'float[]' 

Unlike the previous example, this is possible because of the “rest binding pattern” ...allScores. Thus, the allScores variable will be of type float[].

float[] scores = [6.2, 4.5, 7.5];
[float...] [...allScores] = scores;

In this example the first two values 9.8 and 9.6 will be bound to score1 and score2, which will be of float type, and the rest of the values 9.5 and 4.6 will be bound to score3, which will be of float[] type.

float[4] scores = [9.8, 9.6, 9.5, 4.6];
[float, float, float...] [score1, score2, ...score3] = scores;

Shown below is an array typed binding pattern used in an iterative approach. The int val portion is the array binding pattern, and in each iteration, values in the array arr will be bound to val.

int[] arr = [1, 2, 3];
int total = 0;

foreach int val in arr {
    total += val;
}

This is another example of an array binding pattern used in an iterative approach. However, unlike the previous example, this creates a tuple type variable as a result of the array binding pattern since the inferred type from arr is [int, string].

[int, string][] arr = [[1, "A"], [2, "B"], [3, "C"]];
string output = "";

foreach var [i, v] in arr {
    output = output + i.toString() + ":" + v + " ";
}

Shown below is an example of a tuple binding pattern. As discussed with the array binding pattern, the variables a1, a2 and a3 can be used independently. 

[[string, int], float] [[a1, a2], a3] = [["text", 4], 89.9];

This example will not result in an error since the number of members in the value [["text", 4]] is less than the number of binding patterns, and since the float type has a filler value (0.0), the value for a3 will be 0.0

[[string, int], float] [[a1, a2], a3] = [["text", 4]];

The below example will result in a compilation error since the number of member types in the binding pattern does not match the number of variables and a rest binding pattern does not exist.

[[string, int], float] [[a1, a2], a3, a4] = [["text", 4], 43.2]; ERROR: invalid list binding pattern; member variable count mismatch with member type count

In the below example, the value in the variable reference b will be matched against the tuple binding pattern. Since var is used, the type will be inferred from b. What happens when matching with the tuple binding pattern is that 44 will get matched with n and the rest of the values will get matched with the rest binding pattern ...path creating a collection resulting in the type of path to be string[].

type Address [int, string...];

public function main() {                                  
	Address b = [44, "Tomk Road", "Tennessee"];
	var [n, ...path] = b;
}

This is an example of a tuple binding pattern being used in an iterative approach. In each iteration, values in the tuple tempTuple will be bound to val.

[int, int, string, int, string] tempTuple = [10, 5, "string", 3, "string"];
int i = 0;

foreach int|string val in tempTuple {
    if val is int {
        i += val;
    }
}

For more examples on list binding patterns refer to [1].

Mapping Binding Pattern

The mapping binding pattern boils down to the binding patterns of its fields. A mapping binding pattern  { f1: p1, f2: p2, …, fn: pn, r} matches a mapping value m only if it satisfies the below requirements:

  • The mapping value m includes fields f1, f2, …, fn.

  • If pi matches the value of field fi for each i in 1 to n.

  • If r exists it is ...v (rest binding pattern).

If these points are satisfied, then a successful match occurs and a new mapping value consisting of all the fields f1...fn will be created and the additional fields will be assigned to v. A field binding pattern consisting of just a variable name x is equivalent to a field binding pattern x: x. The record type will be the type that will be assigned for the mapping binding pattern if it successfully matches the value. These binding patterns are especially helpful when dealing with queries. 

Discussed below are a few examples of the different ways in which the mapping binding pattern can be used. The map type and the record type both can be employed in mapping binding patterns.

In Case 1 the mapping binding pattern binds the fields named id, fname and lname with the variable names personId, personFname and personLname, and causes the record value {id: 123, fname: "Jhon", lname: "Doe"} to be matched to it.  

The example in Case 2 is the same as in Case 1 with the exception that {id, fname, lname} does not have explicitly defined variable names. It is actually a simpler way of doing the same thing as the previous example. {id, fname, lname} is the same as saying {id: id, fname: fname, lname: lname} because  when a variable name is not given for a field, a variable will be created with the same name as the field ({x} is short for {x: x}).

type Person record {
   int id;
   string fname;
   string lname;
};

public function main() {
    Person person = {id: 123, fname: "Jhon", lname: "Doe"};
// Case 1                            
    Person {id: personId, fname: personFname, lname: personLname} = person;
// Case 2
    Person {id, fname, lname} = person;
}

In this example, the fields in person will be matched with the variables personId, personFname and personLname. The remaining ...otherDetails is a rest parameter and since the Person type record is an open record, a new variable called otherDetails will be created having type map<anydata|error>. The remaining fields that have not been matched will be matched with this.

type Person record {
   int id;
   string fname;
   string lname;
};

public function main() {
    Person person = {id: 123, fname: "Jhon", lname: "Doe", "age": 23};
    Person {id: personId, fname: personFname, lname: personLname, ...otherDetails} = person;
}

The below example shows the recursive destructuring nature of mapping binding patterns. In this example, person of record type Person is assigned to field personalInfo of Person type in student record of Student type. In such a case, upon a successful match, the mapping binding pattern binds the studentId field with variable stuId and person will also be destructured further, thus the recursive nature. In destructuring person, its fields id, fname, and lname will be bound with variables personId, personFname and personLname.

type Person record {
   int id;
   string fname;
   string lname;
};

type Student record {
   int studentId;
   Person personalInfo;
};

public function main() {
    Person person = {id: 123, fname: "Jhon", lname: "Doe"};
    Student student = {studentId: 234, personalInfo: person};

    Student {studentId: stuId, personalInfo: {id: personId, fname: personFname, lname: personLname}} = student;
}

The below example depicts a few scenarios with invalid mapping binding patterns. Notice that in this example the Person record is a closed record.

Case 1 example will result in a compilation error (ERROR: unknown field ‘age’ in record type ‘Person’) because it contains a field named age which is not in the Person record type. However, this error would not have occurred if Person was an open record.

Case 2 example will result in a compilation error (ERROR: redeclared symbol ‘personLname1’) because  personLname1 has already been declared in Case 1.

type Person record {|
   int id;
   string fname;
   string lname;
|};

public function main() {
  Person person = {id: 123, fname: "Jhon", lname: "Doe"};
// Case 1
  Person {id: personId1, fname: personFname1, lname: personLname1, age: personAge1} = person;

// Case 2
  Person {id: personId2, fname: personFname2, lname: personLname1} = person;
 }

                    

The below example depicts a scenario of using query expressions with a mapping binding pattern. It selects fname, lname, and dept by getting the fname and lname fields from the record list named personList and dept from the name field in the record list named deptList recursively and assigns the values to a record list named deptPersonList. This is made possible by using query expressions from clause and join clause. 

In this example, the var {id: deptId, name: deptName} portion is the mapping binding pattern. It is being used in an iterative approach with query expressions. The mapping binding pattern used here binds the field names id and name with the variable names deptId and deptName for every value in deptList. As a result, we are able to utilize variables deptId  and deptName independently in places like on person.id equals deptId and dept : deptName.

type DeptPerson record {
   string fname;
   string lname;
   string? dept;
};

type Department record {
   int id;
   string name;
};

type Person record {
   int id;
   string fname;
   string lname;
};

public function main() {
    Person[] personList = [{id: 1, fname: "Alex", lname: "George"}, 
                          {id: 2, fname: "Ranjan", lname: "Fonseka"}];

    Department[] deptList = [{id: 1, name:"HR"}, 
                            {id: 2, name:"Operations"}];
                      
    DeptPerson[] deptPersonList =
       from var person in personList
       join var {id: deptId, name: deptName} in deptList
       on person.id equals deptId
       select {
           fname : person.fname,
           lname : person.lname,
           dept : deptName
        };
 }

The below example shows a mapping binding pattern that involves map type in an iterative approach. It binds every field in m with the variable named val.

string output = "";
map<anydata> m = { a: "A", b: "B", c: "C" };

foreach anydata val in m {
        output = output + val.toString() + " ";   
}

This example depicts a mapping binding pattern that involves a record type in an iterative approach. It binds every field in person with the variable named val. Since var is used here, the type will be inferred from person.

type Person record {
   int id;
   string fname;
   string lname;
};

public function main() {
    string output = "";
    Person person = {id: 123, fname: "Jhon", lname: "Doe"};

    foreach var val in person {
        output = output + val.toString() + " ";
    }
}

For more examples on mapping binding patterns refer [2].

Error Binding Pattern

An error binding pattern requires the different elements of an error value to be matched. Considering an error binding pattern error ET(pm, pc, f1=p1, f2=p2, ..., fn=pn, r) — this matches an error value e if:

  • e has a message that matches pm (reason).
  • If pc (cause) is present, e has a cause that matches it.
  • e has a detail record that has fields f1,f2,...,fn such that pi matches the value of field fi for each i in 1 to n.
  • If ET (user-defined error type) is present, the shape of e is in ET.
  • If r is present it is ...v (rest binding pattern).

If these points are satisfied then a successful match causes a new mapping value consisting of all fields of the detail record other than f1,f2,...,fn to be assigned to v . In the case of a successful match, the type assigned for the error binding pattern will be the error type.

Shown below is an error binding pattern where it is destructuring an error of type TestError. Furthermore, named arguments are not needed for the reason part of the error to be assigned to a variable of type string. However, the members of the detail mapping need named arguments and they will be assigned to variables errorInfo and errorCode.

type ErrorDetail1 record {
    string message?;
    error cause?;
    string info;
    int code;
};

type TestError error<ErrorDetail1>;

public function main() {
    TestError err1 = error("ERROR", info = "Text", code = 123);
    var error(reason, info = errorInfo, code = errorCode) = err1;    
}

As mentioned in the Single Use Approach section, shown below is an example of an error binding pattern being used with an on fail clause. In this case, when the do block fails with the error that we defined as error err = error("ERROR"), that error gets assigned to e. Basically, the error binding pattern binds the error that causes the on fail clause to trigger, to the error type variable e.

import ballerina/io;

public function main() {
  do {
      error err = error("ERROR");
      fail err;
    } on fail error e {
      io:println(e.toString()); // error("ERROR");
    }
}

You can also refer [3] for more examples of error binding patterns.

Conclusion

We have now discussed the basic concepts of binding patterns. In the next article, we will discuss the different ways in which binding patterns can be used with other language concepts in Ballerina. See you in the next article. 

Credit: Source link

Previous Next
Close
Test Caption
Test Description goes like this