Software is hard
Software is hard

First steps with Bosque

21 minutes read

A few days ago I stumbled upon an interesting new programming language called Bosque. Being a project at Microsoft Research, Bosque is still a moving target and subject to many changes, but this should not hinder us to explore a few of its most interesting bits.

By its own definition, Bosque introduces a new programming paradigm called Regularized Programming. If we look back to the old, bad days of goto programming and spaghetti-code we immediately appreciate the existence of various loops that make our today’s programming much easier to write and reason about. Could you imagine writing a complex application without any loops? However, everything comes at a cost. You pay by constantly having to think about all the invariants that loops introduce in your programs. Also, using loops doesn’t mean having to deal with semantics only. Every loop, be it a for-each, while, or do-while, uses variables for counting, often combined together with iterators, which you have to keep in mind while coding or debugging. This too increases complexity, because you have to be certain at every point in time that every of those temporaries is valid and not interfering with some other part of your code. In short, although being better than gotos, loops come at a cost. Although Structured Programming freed us from dealing with gotos and type-less variables, because its easier to reason about integers, strings and floats than arrays of raw bytes, it also introduced its own problems we have to deal with since the 70es.  We call these problems accidental complexities. And Bosque is removing them altogether. Yes, Bosque has no loops!

Another key aspect of Bosque is that its variables are immutable by default. Being strongly influenced by functional languages, it borrows many things from them. Immutability in particular is very useful to avoid problems introduced by shared mutable state. I don’t think I need many words to convince you that managing variables which can be modified by different parts of the code is a frustrating experience. 

I could continue writing down many other useful aspects of Bosque, like elimination of the reference equality and aliasing with a more safe and abstract concept of composite equality, but I think we don’t need 1000 words to understand the beauty of something. Le’t try to install Bosque and write some code to show some of its advantages.

Installing Bosque

Like any good coder, we go straight to GitHub and clone Bosque’s code. Being an open source project, anyone can participate in there, but beware: this project is new, so the code & docs there are in constant flux. You can be pretty sure that parts of this tutorial will become obsolete or at least incomplete soon.   

git clone https://github.com/microsoft/BosqueLanguage

As I am working on a Mac, the following examples will be compatible to most *nix systems. If you are on Windows, please adapt to DOS/PowerShel accordingly, or use Windows Subsystem for Linux. 

After we have cloned the code, we need to download NodeJS and install TypeScript globally, because Bosque’s toolchain relies upon them for generation of C++ code. Now, if you are asking yourself: but why?, the answer is that Bosque’s toolchain is written in TypeScript which runs on top of NodeJS and takes your code to generate C++ code which then will be compiled by one of the supported C++ compilers.

Your Bosque Code -> NodeJS/TS toolchain -> transpiles into C++ code -> C++ compiler compiles -> Your App

Installing NodeJS is basically a matter of clicking a few buttons. Just let it be installed in the default directory. To install TypeScript globally you’ll have to issue this npm command.

npm install -g typescript

As npm comes together with NodeJS, it should be available right after you have installed it. In case you did the installation via console, restart it to update your local PATHs.

Now go to your cloned Bosque directory and enter the subdirectory impl. This is the place where Bosque’s toolchain is located. We will have to compile it first. For this to succeed, we have to install the needed npm packages first.

npm install

Our next command executes the build script (check package.json in impl dir for more info). 

npm run build

You will see an output similar to this:

bosque-reference-implementation@0.5.0-rc-1 build /Users/brakmic/projects/BosqueLanguage/impl
> tsc -p tsconfig.json && node ./build/resource_copy.js

Copying resources...
Copying /Users/brakmic/projects/BosqueLanguage/impl/src/core to /Users/brakmic/projects/BosqueLanguage/impl/bin/core
Copying /Users/brakmic/projects/BosqueLanguage/impl/src/tooling/aot/runtime to /Users/brakmic/projects/BosqueLanguage/impl/bin/tooling/aot/runtime
Copying /Users/brakmic/projects/BosqueLanguage/impl/src/tooling/aot/runtime/bsqcustom to /Users/brakmic/projects/BosqueLanguage/impl/bin/tooling/aot/runtime/bsqcustom
Copying /Users/brakmic/projects/BosqueLanguage/impl/src/tooling/bmc/runtime to /Users/brakmic/projects/BosqueLanguage/impl/bin/tooling/bmc/runtime
Copying /Users/brakmic/projects/BosqueLanguage/impl/src/test/tests to /Users/brakmic/projects/BosqueLanguage/impl/bin/test/tests
done!

You have successfully installed Bosque. Great!

Usually, my next step would be making the exegen.js script from impl/bin/runtimes/exegen.js executable, so that I can avoid prefixing node each time I have to compile something. However, as I only have a script that’s useful for *nix systems I’ll avoid recommending it here. In case you want to try it out, go to this branch of my Bosque repository and clone it. 

What you need to know is that the tool that’s parsing, transforming and compiling your program is located under impl/bin/runtimes/exegen. It’s written in JavaScript that you run with node:

node impl/bin/runtimes/exegen/exegen.js -c clang++ -o your_app your_app_source.bsq

Bosque toolchain supports various C++ compilers, so you can try different ones. I use clang++ and g++ 9. The difference between generated binary sizes is not that big and you can also use the --level flag to switch between debug and release.

Hello World in Bosque

Like in most other programming languages, Bosque too needs a starting point which we declare with the keyword entrypoint

namespace NSMain;

entrypoint function main(arg: String): String {
     return String::concat("Hello", ", ", arg, ".");
}

We define a function with a single parameter that does something with a string. If you are an experienced OO-language developer or at least have experience with scripting, you should have no problems with identifying the key elements. First, the language is curly-based, just like C, C++, C#, Java etc. Then there is some notion of a namespace which could lead to assumption that certain elements, maybe data types, get included by naming certain namespaces. In this case we want to define an entrypoint function that must be defined in NSMain namespace.

All in all, this doesn’t seem much different from other programming languages. Apart from the entrypoint keyword we haven’t seen anything exciting yet. And the concat method call looks very much like those typical C++ static methods. Double colons are nothing new to C++ programmers. Indeed, in Bosque, static methods must be used with double colons. In our case we wanted to concatenate a few strings before returning it back.

The function’s return value type is defined after the function name preceded by a single colon. In our case it’s a String. Bosque defines several types which can be found in almost every “modern” programming language. We have Ints, Strings, Bools, Lists, Maps and so on. But here’s the catch: although we can create and play with them, there is no way to print them out on the console (or any other kind of output) within a function. Yes, Bosque functions might look like ordinary C or C# functions, but they behave more like Haskell’s. In Bosque, functions are free from side-effects. And writing to a console is provoking side-effects. Therefore, it’s not possible.

No side-effects and Immutability

Bosque combines productive elements from OO/Structural programming languages with more robust functional aspects. This is why we can have side-effect free curly-braced functions.  And not only that. We also have immutable variables by default which can be changed within curly braces. Let’s look at this code:

namespace NSMain;

function increase(arg: Int): Int {
    arg = arg + 1;
    return arg;
}

entrypoint function main(num: Int): Int {
    return increase(num);
}

Would it compile? It seems so. We are merely taking an argument and hoping to modify it by calling the increase function. Looks pretty harmless, doesn’t it? Not so in Bosque as we can read from this compiler error:
 
exegen.js --c clang++ -o immutable immutable.bsq
Compiling Bosque sources in files:
immutable.bsq
...
Parse error -- ["immutable.bsq",4,"Variable defined as const"]
 

The variable arg which is just the num from our main function can not be changed inside the function. It’s declared const by default. What we should do instead is modifying the value without trying to save it back into the original variable. This is the modified code that would compile:

function increase(arg: Int): Int {
    return arg + 1;
}

When we compile and execute our program, it would return the expected value:

./immutable 10 
11

Immutability by default is a powerful feature as it relieves the programmer from thinking about the shared mutable state. However, unlike the more prominent functional programming languages like Haskell, Bosque allows mutability inside curly braces. You are free to define as many variables as you like and mutate them. What is sure is that they’ll never leave the scope defined by those curly braces. Here an example:

namespace NSMain;

function change_inside(arg: Int): Int {
    var temporary = 10;
    temporary = temporary + arg;
    return temporary;
}

entrypoint function main(num: Int): Int {
    return change_inside(num);
}
This code compiles and executes without any problems:
 
./mutable 10 
20
 
 

Flexible function invocations

Until now, we have only passed single parameters to our functions. But Bosque can do more, much much more. Obviously, we can pass multiple parameters which is nothing exceptional. What’s really nice is the ability to use rest and spread concepts together with named parameter passing. You might already know the first two from other programming languages. In JavaScript the rest concept would look like this (we execute this in a browser console):

We have a function that takes four parameters, but we enter only the first two of them individually. The remaining parameters come from an array that gets spread over those two. The three dots preceding the array indicate that JavaScript will spread these elements over the remaining parameters. 

The rest concept is similar in form but its used for solving different kind problems: when there’s an unknown number of parameters a function will receive.

Let’s execute this example:

As the function could receive any number of parameters, we have to make sure that each of them will be processed accordingly. Here we use the same operator (the three dots) to indicated that every given parameter will be treated as “next in line” and processed by the reduce function located inside the function process_rest_params.

Both of these concepts are also supported by Bosque:

namespace NSMain;

function getmin(...args: List<Int>): Int {
   return args.min();
}

entrypoint function main(): Int {
   return getmin(10,6,22,8,50,24);
}
 
./spread
6
 

The above code returns the smallest element of an array that’s constructed by taking all parameters no matter how many of them are present. The function getmin could take any number of individual parameters, because all of them will be converted into a List<Int> first.

Similar to JavaScript example, we can implement the spread concept too:

 
namespace NSMain;

function calculate(a: Int, b: Int, c: Int, d: Int): Int {
    return a + b + c + d;
}

entrypoint function main(): Int {
    let array = [22,33];
    return calculate(10, 6, ...array);
}
./spread 
71
 

In the example above we need four variables to complete our calculation. But the last two of them don’t come as separate arguments but instead as an array that’ll be “spread” over variables c and d. As we see, these two concepts, spread and rest, are very useful, but this is not all. Bosque can do more. Let’s expand the above example by using named parameters and spread arguments.

entrypoint function main(): Int {
    let array = [22,33];
    return calculate(b=10, a=6, ...array);
}

Ok, this is a bit of a contrived example. You might also have noticed that we have been using another keyword: let. This is for variables that wont’t change inside a block scope. Apart from “variable” variables declared with var, we also have constant ones declared with let keyword.

No Loops

As already announced at the beginning, there are no loops in Bosque. No for-each, while-do, do-while and the whole rest. The more loops we have the harder it gets to reason about the execution paths a program might take. Although helpful to avoid the even worse goto-constructs loops have their own shortcomings. Have you ever had problems with indexing variables, or null-pointer iterators? However, we still need to loop somehow, to repeat some parts in our programs, to iterate over lists, sets and maps. There are seldom any programs that simply go straight from A to B to C and then finish. So, how do we create looping constructs in Bosque when there are no loop statements at all? As you might have already thought about it: the answer is to be found in functional programming. Almost any loop can be converted into one of the higher level functors like filter, find, map or reduce. Also, we can apply list comprehensions over groups of elements so that there is seldom any reason to manually build loops. Instead of thinking about loops, shared mutable state, and various “ceremonial” elements,  software developers should focus on solving real problems. Let’s try out this example to see how Bosque implements looping:

namespace NSMain;

entrypoint function main(): List<Int> {
   var list = List<Int>@{1,2,3,4};
   return list.map<Int>(fn(e) => e * 2);
}
 
After a successful compilation we execute the code and get the following result:
 
./loops
{2, 4, 6, 8}
 

In this small example we see several interesting things. First, there is the instantiation of a List<Int> by using its default constructor indicated with @ sign and {} braces. Later, we’ll see how all this works, but for now we’ll stay focused on loops. What happened to our array and where did it happen? The answer is in the map member function call that’s taking a function as its parameter. This is indicated by the keyword fn and its own parameter list that follows it. Bosque treats functions like any other type. In our case, the function needs a single parameter that represents an element that’ll come from the very array that’s calling the map method. Inside the function, the current element will be multiplied by 2. The end result is a new array containing the products. So, instead of maybe creating a for-loop that iterates over the array and keeps track of its current position via index variable and is also accessing elements with it, we solve the problem elegantly by feeding the map method the appropriate multiplication function. Those of you who have already programmed in Haskell or any other functional language (JavaScript included btw.) know instinctively what the advantages of this approach are. Instead of telling the computer what to do we rather let it know what the result should look like. The rest comes from the machine. No shared state, no index variables, no direct element access.

Concepts and Entities

As already mentioned, Bosque supports constructs we know from object-oriented languages. We have constructors and user-defined data types that we call entities in Bosque. There are also interface-like constructs called concepts that are being used as blueprints for entities. However, there are some subtle differences that we will talk about. Here is a simple example of an entity that provides a concept (in OO-languages we’d say “it implements an interface”).

namespace NSMain;

concept Animal {
    field name: String;
    abstract method say(): String;
}

entity Dog provides Animal {
    method say(): String {
       return String::concat(this.name, " says ", "woof!");
    }
}

entrypoint function main(dog_name: String): String {
   let dog = Dog@{name=dog_name};
   return dog.say();
}
 
We let our dog Max speak to us! 🙂
 
./entities Max
"Max says woof!"
 

Our program begins with an Animal concept definition that describes what properties (fields) and methods the entities that provide it will have. Then an entity definition follows, in this case a Dog, which defines a method that represents an implementation of the abstract method say(): String. This is, roughly, similar to abstract methods and implementations from various other OO-languages. We also notice that our Dog entity doesn’t reference the name field as it already has access to it being a provider of Animal concept. We then instantiate the entity by calling its constructor. As previously noted we use the @ sign and {} braces to call the constructor. However, Bosque constructors behave differently from those you’ll find in C++, C# etc. Instead of creating complex hierarchies with lots of super() or base() invocations, Bosque goes the way of direct field initializations. We also say that Bosque constructors are atomic. And similar to other OO-languages, Bosque too has the this “pointer” variable that represents the entity instance. We can also introduce static methods that belong to entity constructs only. That is, they can only be called by using the entity name and not by its instances.

entity Dog provides Animal {
   method say(): String {
      return String::concat(this.name, " says ", "woof!");
   }
   static play(): String {
      return "All dogs like to play!";
   }
}
 

The static method play(): String can only be called like this: Dog::play(). The invocation of static methods resembles C++. Further, we can override method implementations inside our entities. Here we have updated our Animal concept to contain a definition of a method eat(): String. This btw. is also a good example that concepts are more powerful than interfaces found in other OO-languages, because they can contain method definitions and not only declarations.

concept Animal {
   field name: String;
   abstract method say(): String;
   method eat(): String {
     return "tasty!";
   }
}
 
Our Dog entity also contains a method eat(): String that overrides the one from the concept.
 
entity Dog provides Animal {
    method say(): String {
       return String::concat(this.name, " says ", "woof!");
    }
    static play(): String {
       return "All dogs like to play!";
    }
    override method eat(): String {
       return"very tasty!";
    } 
}
 
If we execute this variant the result will be this:
 
./entities Max 
"very tasty!" 
 

We also have the option to introduce hidden methods that resemble private methods found in other OO-languages. Here we have modified our Dog to hunt cats [Disclaimer: No animals were harmed in the making of this article.] 🙂

 
entity Dog provides Animal {
 
   override method eat(): String {
     return this.hunting_cats();
   }
   hidden method hunting_cats(): String {
     return "it's so much fun!";
   }
}
 

The modifiers static, override and hidden can also be combined. So it’s possible to override a static method or make it hidden. But not only programmers deal with concepts. Bosque’s core library itself defines many of them. For example there is a concept called Parsable that describes what an entity must implement to be recognized as as parsable.

//<summary>Special type for indicating this type supports typed string use</summary>
concept Parsable provides Any {
    abstract static tryParse(str: String): Result<Any, String>;
}
 
You can find this and many other concepts in the core.bsq file located here.
 

Typed Strings

Another powerful concept coming from Bosque are typed strings. Such strings contain a “rule set” that’s used to determine if a string can be processed or not. We also say that such strings contain a Validator. Let’s see it in action:

namespace NSMain;

typedef TimeFormat = /\b(0?\d|1[0-2]):[0-5]\d (AM|PM)/;

function get_time_string(str: String): SafeString<TimeFormat> {
   return SafeString<TimeFormat>::from(str);
}

entrypoint function main(data: String): SafeString<TimeFormat> {
   return get_time_string(data);
}

Here we have defined a regular expression for parsing time with AM|PM suffix. You also might recognize some resemblance with C/C++’s typedef specifiers. Our function get_time_string takes a string and converts it to a SafeString that relies on typedef‘d validator for its construction. If everything’s OK we get a proper SafeString<TimeFormat> returned. If not, the program will crash. 

./typed_strings "00:15 AM" │
NSCore::SafeString<T=NSMain::TimeFormat>'00:15 AM'

And this is how it looks like when invalid data is given:

./typed_strings "12345"
"Fail pre-condition" in /Users/brakmic/projects/BosqueLanguage/impl/bin/core/cpp/core.bsq on line 232

A pretty scary response, isn’t it?
 

But we don’t have to live with that and SafeString offers more convenient methods like tryFrom. We will now update our function to deal with non-parsable values:.

 
function get_time_string(str: String): SafeString<TimeFormat>|None {
   let result = SafeString<TimeFormat>::tryFrom(str);
   if (result == none) {
       return none;
   } else {
       return result;
   }
}
 
Here we introduce a few new elements:
 
  • if-then-else statement (else-if with elif keyword is also possible)
  • union of possible return values indicated with |
  • None concept and none value 

In the above code we now try to parse valid time and in case we succeed we return a SafeString<TimeFormat>. If we fail the value returned will be none. The rest of the code is pretty much similar to other programming languages. We use if-then-else to check for none and return values accordingly. In case of a failure we’d get something less worse than a crash:

./typed_strings "12345"
none

Conclusion

I hope that I could show you some of the many interesting features of Bosque. There are much much more, but as this document should have an end, I think I’ll leave it for the next one. I am planning an article series on Bosque Programming.

However, as I am a beginner myself and the language is still evolving, everything written here is subject to change and/or based on my assumptions, (wrong?) conclusions and mistakes. Therefore, I recommend you to read available documents and go through the code (TypeScript and C++). And most importantly: experiment, experiment, experiment

Have fun with Bosque!

Leave a comment

Your email address will not be published.

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