Software is hard
Software is hard

First steps with Bosque – Part 2

9 minutes read

Welcome to the 2nd part of the series on Bosque programming language. As Bosque is still a moving target and I am a beginner myself, I will mostly follow its main documents and use the current source code. However, expect some rough edges and swift changes while we’re experimenting with it. So far, we have learned how to use a few of Bosque’s main features that are the building blocks of the Regularized Programming paradigm: 

  • it is free from side-effects
  • all variables are immutable (unless declared as mutable, within a block scope only)
  • has no loops and instead relies on functors like filter, map, reduce etc.
  • offers OO-like concepts & entities
  • typed strings
  • flexible function invocations with named parameters and spread & rest concepts

In this part we will be talking about:

  • equality comparison and identifiers
  • if-then-else and switch-case statements and expressions
  • structured declarations and assignments

Equality Comparison

First things first: Bosque doesn’t allow user-visible reference equality. This might sound alien to you as many other languages offer that kind of equality “by default”, but Bosque is different in this case. And for a good reason. Reference Equality is problematic, because it ties the execution semantics of languages to hardware models. In such cases references become dependent on hardware-specific memory locations. This means that no matter how abstract your objects and other structures might be, the referential equality will keep them tied your machine’s memory model. Therefore, in Bosque we can either explicitly compare primitive types like Int, Bool, String etc. or define our own identifier keys that provide the notion of equality tied to our particular domain. 

The operators used in the first group are not different from many other languages: == and != and they can be used for KeyType values like Int, Bool, String, GUID, CryptoHash and others. There is also a special KeyType named None that represents a single value “none” which can be used to compare against any kind of value, regardless if KeyType or not. KeyType itself is defined as concept which indicates that a value can be used as a key in a collection (Map for example).

The KeyType concept declares two methods, equal and less, which are needed for equality operations.

A few of the KeyTypes demand a stricter equality comparison as they expect the type and value to be the same. This is the case with Enum for example, where both the Enum type and its value must be the same to be recognized as equal.

Here is an example on equality comparison with Enums.

namespace NSMain;

enum Colors {
    red,
    green,
    blue
}

entrypoint function main(): Bool {
    let red = Colors::red;
    let green = Colors::green;
    return red == green;
}
 
./enums
false

Typical comparison operations with values of KeyType type would look like this:

1 == 1                   //true
"value" != "other value" //false
{} == none               //false
false == none            //false

For comparison of more complex types we have to define identifiers. They can be simple, for example single integer values, or compound identifiers that can contain several fields.

We declare a simple identifier by using the identifier keyword. Here we define one which maps to Int type. An identifier gets instantiated by using the static create method which takes the identifier’s value or values (in case of compound identifiers) as its parameters.

namespace NSMain;

identifier SimpleId = Int;

entity User {
    field id: SimpleId;
    field category: String;
    field name: String;
}

entrypoint function main(): Bool {
    let user = User@{id=SimpleId::create(1), category="staff", name="john"};
    let user2 = User@{id=SimpleId::create(2), category="staff", name="jakob"};
    return user.id == user2.id;
}
 
./simpleident
false
 

A compound identifier comprises of multiple parts which are KeyType types (or: primitive types) like Int, String etc. Again, we use the static create method to assign values to our identifier’s fields.

 
namespace NSMain;

identifier UserKey = { id: Int, category: String };

entity User {
    field id: UserKey;
    field name: String;
}

entrypoint function main(): Bool {
    let user = User@{id=UserKey::create(1, "staff"), "john"};
    let user2 = User@{id=UserKey::create(2, "staff"), "jakob"};
    return user.id == user2.id;
}
 
./compound
false
 

We see that instead of comparing elements via reference equality we explicitly define a type that represents equality for our particular domain. 

If-Then-Else

Although Bosque removes loops completely from its syntax some parts from the “old” structured programming languages are still with us. Like if-else and switch-case. And not only in form of conditional control flow structures, but also as expressions. But before we come to this, let’s see how they work as statements.

namespace NSMain;

entrypoint function main(): Int {
    let a = 10;
    if (a < 5) {
       return a;
    } else {
       return 1;
    }
}
 
./ifelse
1
 

So far, no big surprises here. The statement validates the expression and executes the part of the code associated with it. It is worth noting that every block in an if-then-else statement introduces a new lexical scope. Therefore, if you introduce a new variable within such a scope it’ll only exist within it.

A more interesting variant of if-then-else is the statement expression.

namespace NSMain;

entrypoint function main(): Int {
    let a = 10;
    return if (a < 5) a else1;
}
 
./ifelseexpr
1
 

Our code is now more compact and more functional.

Switch

Switch is another statement that Bosque shares with traditional programming languages. However, there are some subtle differences and novel ways to deal with values and expressions. A typical switch statement in Bosque looks like this:

 
namespace NSMain;

entrypoint function main(): String {
    let a = 10;
    switch(a) {
      case 1 => { return "one"; }
      case 0 => { return "zero"; }
      case _ => { return "out of range"; }
    }
}

./switchstmt 
"out of range"
 

We define a variable and then check its value via switch-case statement that returns a string depending on the numerical value. The third case in the above switch statement is a “catch-all” or wildcard case that will be used if none of the other options match. The rule is that a switch statement must be exhaustive so we use the wildcard statement stop the compiler from yelling at us.

But this is not everything as Bosque also supports switch expressions as shown in the example below.

namespace NSMain;
 

entrypoint function main(): String {
    let a = 10;
    let result = switch(a) {
                   case 1 => "one"
                   case 0 => "two"
                   case _ => "out of range"
                 };
    return result;
}

./switchexpr
"out of range"
 

Instead of using the keyword return to return values we instead define a variable that will receive it from our switch expression. Also, the switch is now closed with a semicolon to indicate that this is an expression and not a statement. But this too is not everything Bosque’s switch statements can do. Let’s look at this example:

namespace NSMain;

entrypoint function main(): Int {
    let x = {a = 2, b = 1};
    let result = switch(x) {
                   case {a=2, b=2} => 99
                   case let {a=2, b=1} => x.b
                   case _ => -1
                 };
     return result;
}


./switchcomplex
2
 

Here we have a switch expression that deals with structured assignments and pattern matching. Its first case declares a pattern the variable x must fit into, that is its fields must be a=2 and b=1. The second case defines a new structure that is valid only inside this scope and can be used as a return value as is the case with x.b.  The last case is the usual wildcard case.

We can also check for types as shown in this example:

switch(x) {
    type Bool => { return false; }
    type Int => { return 0; }
    type String => {return ""; }
    type {f: Int, g?: Bool} => { return x.f; }
    case _ => { return none; }
}

Mixing of case and type checks is allowed as shown above with the wildcard case.

And we can also introduce additional conditions by using the when clause:

namespace NSMain;

entrypoint function main(): Any {
    let x = 10;
    switch(x) {
      case Int when x >= 0 => { return x; }
            case Int when x < 0 => { return -x; }
      case {f=_: Int, g=let y: Int} when y != 0 => { return y; }
      case _ => { return -1; }
    }
}
 

Structured Declaration and Assignment

This feature can be found in various other languages. It makes direct structuring and assignment of variables possible. In the below example we define “on the fly” two variables in a temporary array structure and assign them values from another temporary array. 

namespace NSMain;

entrypoint function main(): Int {
       [let x: Int, let y: Int] = [ 10, 15 ];
       return y;
}


./declarations
15

The same can be done with maps as well. And it can even contain another declaration and assignment inside as shown in this example.

namespace NSMain;

entrypoint function main(): Int {
    {a=let c, b=let d} = {a=1, b=2};
    return d;
}
 
./declarations
2

In Bosque’s language documentation you can find further examples, but as the language is still in development, some of them don’t compile correctly. This, however, is only temporary as many bugfixes are on their way to be included in the upcoming version.

Conclusion

I hope that you liked this 2nd part of our Bosque tutorial series. As the language is still evolving I had to exclude a few areas that I planned to talk about. Instead, I have opened a few PRs and tried to provide fixes for some smaller bugs (I still don’t know much about the internals of the language). Bosque is fascinating, because it shows how a new programming paradigm could look like and what advantages it offers when compared with structural and object-oriented programming. As my experience with Bosque will (hopefully) expand over time I hope that one day I’ll be able to show you more complex (more useful?) examples.

Have fun!

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.