Know Google’s Carbon Programming Language’s syntax in this Tutorial.
According to Google developer Chandler Carruth, Carbon could serve as a successor language to C++. The documentation declares that Carbon “is designed around interoperability with C++ as well as large-scale adoption and migration for existing C++ codebases and developers”.
The documentation states that Carbon “is designed around interoperability with C++ as well as large-scale adoption and migration for existing C++ codebases and developers”.
This article will provide a practical coding introduction to the Carbon programming language but it won’t discuss why someone might need it.
Must read:
Introduction to Standard C++ : In a Nutshell #1
Algorithms Analysis and why it is important? : DSA
Getting Started
This Getting Started part has two semi-parts:
- Tooling up
- Language Basics
If you have already installed Carbon, feel free to skip to Language Basics, otherwise start with Tooling.
In the Tooling section, we will go over the following to set up our environment:
- Homebrew
- Bazel
- LLVM
- Clone Carbon language
- Run the Explorer
You can find the identical approach in Getting Started.
Here, I will add some context and a short description for the ones who approached the language and the tooling for the first time.
After the installation, move forward with some practical examples in the Language Basics section.
Carbon Language: Tooling
1. Homebrew
Homebrew is a package manager that might be already installed on your computer. Run brew --version
to check if you have it already.
If this is not the case, you can install Homebrew on macOS, Linux, and Windows (through WSL).
2. Bazel
Bazel is an open-source build and test tool that scalably supports multi-language and multi-platform projects.
According to the documentation, “Bazel is Carbon’s standard build system. Bazelisk is recommended for installing Bazel”.
Run the below command to install Bazel:
brew install bazelisk
3. LLVM
You can see LLVM as a low-level virtual machine, but “LLVM has little to do with traditional virtual machines”. LLVM is used to compile and link Carbon as part of its build.
Run the following command to install LLVM:
brew install llvm
Be mindful that on macOS (not Linux), you should run the following command to update the PATH
:
export PATH="$(brew --prefix llvm)/bin:${PATH}"
4. Clone Carbon Language
Run the following commands from an suitable folder.
They will clone and download the Carbon language code locally.
$ git clone https://github.com/carbon-language/carbon-lang
$ cd carbon-lang
5. Run the Explorer
Finally, we are ready to build and run the explorer.
The following code runs Bazel build tool so that it triggers the explorer code:
$ bazel run //explorer -- ./explorer/testdata/print/format_only.carbon
The explorer runs the actual code in the file at the designated location: ./explorer/testdata/print/format_only.carbon
The extension .carbon
is necessary to let the interpreter know that the file contains carbon language.
Eventually, you should get a nice “Hello World” like the following.
Carbon Language: Language Basics
If you are familiar with C, C++, and similar languages you’ll be familiar with Carbon syntax as well.
Carbon is still testing and some things might change. The documentation itself states “Note that Carbon is not ready for use”. However, some design principles are less likely to change.
Let’s start by looking at the code in format_only.carbon
package ExplorerTest api;fn Main() -> i32 {
var s: auto = "Hello world!";
Print(s);
return 0;
}
- The
package
keyword declares packages.
This file contributes to the default library. - The
fn
introducer keyword declares functions.
The codefn Main() -> i32 {...}
declares a function named Main. The type of the return ofMain()
isi32
i.e. integer. Main returns zero. - The
var
keyword introduces a variable declaration.
In the code above,s
is the name of the variable, followed by a colon:
, and the type.auto
is used to infer the variable type automatically. - It is possible to declare constants by using the keyword
let
.
While this might be confusing for developers coming from JavaScript, it will be smooth for devs coming from Swift. - Finally, you can add comments by adding two slashes
//
This simple file gives us a good overview of Carbon syntax.
Let’s count some other pieces.
Primitives Types and Values
In Carbon, we can have the following primitives:
- Boolean. It has two possible values: true and false.
- Integer. Carbon contemplates signed and unsigned integers.
Signed-integers can be i8, i16, i32, i64, i128, or i256. Unsigned integers can be u8, u16, u32, u64, u128, and u256. - Float. It is possible to use f16, f32, f64, and f128.
- String. A byte sequence. String literals can be written on a single line using a double quotation mark (“) at the beginning and end of the string. Like “example”. Multi-line (or block string ) literals begin and end with three double quotation marks (“””). It is possible to use an escape sequence by prepending a backslash (\).
Functions Params and Return Types
Let’s extend the code to include another function:
package ExplorerTest api;// Return type is empty/void
fn AgeLogger(var age: i32) {
Print("Carbon is {0} years old", age);
}fn Main() -> i32 {
var s: auto = "Hello world!";
Print(s);
AgeLogger(0);
return 0;
}
The AgeLogger
function takes one variable parameter named age
of type i32.
However, since AgeLogger
doesn’t return anything, we can simply skip the return type.
Thus, generally speaking, the syntax for a Carbon function is:
fn FunctionName(var param: type, ...) -> return type { ... }
The return type can be ignored when the return is empty or void.
Must read:
Introduction to Standard C++ : In a Nutshell #1
Algorithms Analysis and why it is important? : DSA
Control Flow: If/Else
If you are coming from C++ or many other languages, this is nothing new.
As reported, “if and else provide conditional execution of statements”.
Let’s add this part to our example:
fn AgeLogger(var age: i32) {
if(age == 0){
Print("Carbon is {0} years old", age);
} else {
Print("Carbon is not {0} years old", age);
}
}
By adding some simple control flow, blocks of statements are executed conditionally. Otherwise, blocks of statements are executed sequentially.
Control Flow: While Loop
While the expression is true the loops continue.
I will include the following piece of code just after the if/else statements in the AgeLogger
function.
var x: i32 = 0;
while (not (x == 3)) {
Print("I am number {0}", x);
x = x + 1;
}
Print("Done!");
I should say that in my case I couldn’t use while(x < 3)
nor ++x
. In both cases, I would get an error despite that syntax being shown in the documentation.
I didn’t understand why yet! So, if you figure it out, feel free to add the reason to the comments for everyone to know.
In any case, the outcome is:
Control Flow: For Loop
According to the documentation, for
statements support range-based looping. The example provided is pretty generic:
for (var name: String in names) {
Console.Print(name);
}
But I couldn’t get it to work:
var names: [String;3] = ("str1", "str2", "str3");
for (var name: String in names) {
Print(name);
}
While trying the above code, I kept getting “syntax error, unexpected VAR”, which I couldn’t understand as the keyword var
is supposed to be there!
Unfortunately, I couldn’t find a working example anywhere.
Control Flow: Match
Once again, if you are coming from C or C+, this will be familiar.
match
checks the value of an expression against the case declarations. In the following example, the value 2 is matched against the case declarations, and the string “Matching two” is returned.
fn TheMatcher() -> String {
Print("I am here");
var x: i32 = 2;
match (x) {
case (0) => {
return "Matching zero";
}
case (1) => {
return "Matching one";
}
case (2) => {
return "Matching two";
}
default => {
return "Matching none";
}
}
}
The last block is an optional default
block. The default
block can be added after the case declarations and it will be executed if none of the case declarations match.
Within the scope of control flow, it is worth mentioning break
and continue
. Briefly speaking:
break
immediately ends a loop and Carbon “will continue starting from the end of the loop’s scope”.continue
immediately goes to the next loop.
Composite types
Before checking out composite types, I created a new file called composite_types.carbon
that contains all the code from now onward.
Here is the new code:
package ExplorerTest api;fn Main() -> i32 {
var s: auto = "Hello composite types!";
Print(s);
return 0;
}
I will use it as a blank sheet to try out composite types.
Tuples: A tuple is a fixed-size collection of values, sometimes called coordinates, that can have different types. It is possible to access values by using their position.
The following code assigns the tuple (0, 1, 2, "omega")
to x
before printing the value ad index two and three.
fn UseTuples() {
var x: (i32, i32, i32, String) = (0, 1, 2, "omega");
Print("At index 2: {0}", x[2]);
Print(x[3]);
}// output:
At index 2: 2
omega
Struct types: Structural types allow to identify members by name instead of using their index or position.
- Struct type example:
{.name: String, .count: i32}
- Struct value example:
{.name = "John", .count = 4}
fn UseStruct() {
var data: auto = {.x_var = 3, .b_var = 2, .m_slope = 7};
var y: i32 = data.m_slope * data.x_var + data.b_var;
Print("y: {0}", y);
}// output:
y: 23
Pointer types: As in C and C++, a pointer is a variable that stores the memory address of an object. Carbon offers two pointer operations:
- Dereference: given a pointer p, *p gives the value p points to as an l-value.
- Address-of: given an l-value x,
&x
returns a pointer to x. There are no null pointers in Carbon.
fn Pointers() {
var x: i32 = 0;
Print("Initial x: {0}", x);
x = 10; // changes x to 10
Print("x is now {0}", x);
var y: i32* = &x; // returns pointer to x
*y = 5; // changes x to 5
Print("x is now {0}", x);
Print("y = {0}", *y);}// output:
Initial x: 0
x is now 10
x is now 5
y = 5
- Arrays and slices: The generic syntax for the type of a Carbon array is
[type, number of values]
.
The following code assigns the type and values to an array that contains four integers.
var array: [i32; 4] = (0, 1, 2, 3);// It is possible to omit the number of values
// when the size of the array can be inferred
var array: [i32;] = (0, 1, 2, 3);// access elements using square brakets
Print(array[0]);
Classes
It is possible to use classes to define data structures.
Use the class
keyword before the name of the class to define a class.
The example class is named Widget
and contains three fields declared by using the var
keyword before the name of the field.
You also need to declare the type of each field.
class Widget {
var x: i32;
var y: i32;
var payload: String;
}
Classes can also contain functions, methods, alias, constants, and nested classes.
The following example expands to add a function that returns the sum of the two integer fields.
class Widget {
var x: i32;
var y: i32;
var payload: String;
fn Sum[me: Self]() -> i32 {
var total: i32 = me.x + me.y;
return total;
}
}
First, I expanded the class to include a sum function. The function sums the value of the field x
to the value of the field y
and returns the total.
Then, I updated Main()
as follows:
fn Main() -> i32 {
var coolWidget: Widget = {.x = 6, .y = 7, .payload = "load"};
var total: i32 = coolWidget.Sum();
Print("Total sum {0}" , total);
return 0;
}// output:
Total sum 13
Generics
As in other languages, generics “allow Carbon constructs like functions and classes to be written with compile-time parameters”.
Usually, T
is used to define a generic that can be any type.
The official documentation provides a nice example.
First, we have a Min
function. The function has a type parameter T
that can be any type that implements the Ordered
interface.
fn Min[T:! Ordered](x: T, y: T) -> T {
return if x <= y then x else y;
}
Then, they assign values to the variables a
and b
. Given that both variables are of type i32
, T
is deduced to be i32
.
var a: i32 = 1;
var b: i32 = 2;
Assert(Min(a, b) == 1);
In the same way, assigning String values to a
and b
renders T
a type String.
var a: String = "abc";
var b: String = "xyz";
Assert(Min(a , b) == "abc");
Be aware that in my case I had issues parsing characters like <=
. However, a more generic test where we only pass values to print works fine.
fn TestGenerics[T:! Type](x: T) -> T {
return x;
}// output:
i32: 0
String
Must read:
Introduction to Standard C++ : In a Nutshell #1
Algorithms Analysis and why it is important? : DSA
Conclusion
Given the several hiccups I uncovered, it is safe to say “Note that Carbon is not ready for use”. This is in line with what is declared in the documentation.
Despite these things, it was fun to play around and test it a bit. It was particularly fun because it is new and still untried.
I hope you will feel the same!
For the sake of keeping this article an introduction, I had to overlook parts.