Expressions

Expressions form the building blocks of any Type-C program. They are fundamental constructs that evaluate to values, execute operations, or rarely, produce side effects. As you have seen in the previous sections, Type-C comes with a rich set of expression and aims to be a very expressive language, embracing functional constructs. Hence, in this chapter, we take a deep dive into the types of expressions in type-c.

Every expression must return a value. That's why, when reviewing if-else and match expressions, we always had to provide an else branch.

Operators and Precedence

PrecedenceOperatorTypeAssociativityOverloadable
16unreachableObject creationLTRNo
16mutateObject creationLTRNo
16coroutineObject creationLTRNo
16yield, yield!Object creationLTRNo
16newObject creationLTRNo
16spawnProcess spawningLTRNo
16awaitAsynchronous waitLTRNo
15()Function callLTRYes
15[]Array subscriptionLTRYes
15?.Nullable member selectionLTRNo
15.Member selectionLTRNo
14++ (postfix)Unary post-incrementLTRYes
14-- (postfix)Unary post-decrementLTRYes
13++ (prefix)Unary pre-incrementRTLYes
13-- (prefix)Unary pre-decrementRTLYes
13+Unary plusRTLYes
13-Unary minusRTLYes
13!Unary logical negationRTLYes
13!!DenullRTLYes
13~Unary bitwise complementRTLYes
12*MultiplicationLTRYes
12/DivisionLTRYes
12%ModulusLTRYes
11+AdditionLTRYes
11-SubtractionLTRYes
10<<Bitwise left shiftLTRYes
10>>Bitwise right shiftLTRYes
9<Relational less thanLTRYes
9<=Relational less than or equalLTRYes
9>Relational greater thanLTRYes
9>=Relational greater than or equalLTRYes
9isType comparison (objects only)LTRNo
9as, as? as!Type castingLTRNo
8==Relational is equal toLTRNo
8!=Relational is not equal toLTRNo
7&Bitwise ANDLTRYes
6^Bitwise exclusive ORLTRYes
5|Bitwise inclusive ORLTRYes
4&&Logical ANDLTRYes
3||Logical ORLTRYes
2??Nullish coalescing operatorLTRYes
1if,elseConditional ExpressionRTLNo
0=AssignmentRTLNo
0+=Addition assignmentRTLNo
0-=Subtraction assignmentRTLNo
0*=Multiplication assignmentRTLNo
0/=Division assignmentRTLNo
0%=Modulus assignmentRTLNo

Expressions

Now we will review the list of all expressions in Type-C.

Array Construction

This expression constructs a built-in array, syntax:

When no type hint is provided, the type of the array is inferred from the types of the expressions. If the expressions have different types, the type of the array is the common type of the expressions. If there is no common type, the compiler will throw an error.

Binary Expression

Binary expressions are expressions that involve two operands and an operator. The operator can be any of the binary operators listed in the table above. Notice that binary expression can translate to class method call when the LHS of the operation is class that override the operator.

Cast Expression

The cast expression is used to convert a value from one type to another. Casting is safe by default, meaning the compiler will complain when you are casting from one type to another that is unrelated. Casting is explained further in the Type Casting section. Here is a basic example of a cast expression:

Element Expression

An element is the most basic expression in Type-C. It can be a variable, function, etc.

x is an element expression in the assignment statement.

Function Call Expression

A function call expression is used to call a function. The syntax is as follows:

A function call can also be a method call on an object that overloads the __call__ method.

Another example is:

In this example, the function call is MyClass.MyStaticMethod applies to the arguments (1, 2, 3).

If-Else Expressions

In type-c, if-else can be used as expression, similar to python

The values and condition pairs <value> if <condition> must be comma separate, and the else expression is mandatory at the end, since expressions must always have a value.

The if-else expression shares the same limitation as the let .. in construct. It is designed to be used at the root level of an expression for clarity and correct parsing. When used within a larger expression, parentheses should be used to scope the if-else block explicitly.

Index Access Expression

The index access expression is used to access an element in an array. The syntax is as follows:

A class that overloads the [] method can also use any datatype for indexing, even strings and such.

IndexSet Expression

When the compiler encounters an assignment to an array element, it will generate an IndexSet expression. This expression is used to set the value of an element in an array. The syntax is as follows:

from a compiler perspective, x[0] = 4 is translated to IndexSet(target: x, value: 4, indexes: [0]), the reason for this is that an indexing operation can have multiple indexes in theory:

So the built-in array type only allows one index, but a custom class can allow multiple indexes.

Reverse Index Access Expression

Reverse index access allows the access of elements from the end of an array. The syntax is as follows:

It is important to note that unary minus is used to denote reverse indexing, and is part of the operator!

a[-1] translates to: reverse_index(a, 1), where 1 is still a u64.

To overload this operator within a method, you use the name [-]

Reverse Index Set Expression

Reverse index set allows the setting of elements from the end of an array. The syntax is very similar to index set:

Same logic as Reverse Index Access Expression applies here, the - is part of the operator.

To overload this operator within a method, you use the name [-]=

Instance Check Expression

This expression is similar to instanceof in other languages. However in type-c, the keyword is is chosen since it is not just about classes and interfaces, but also variants. Also remember that Type-C matches types by structure and not names. So instead of thinking "is this object an instance of this class", think "does this data match with that".

Here are the acceptable cases:

  1. class is interface
  2. interface is class
  3. interface is interface
  4. Variant is Variant Constructor

Casting Expression

Avoid using as!. It just ignores safety checks.

To safery cast between interfaces/classes, you can use as? which returns a nullable if casting fails, or as if the cast is guaranteed to succeed.

Lambda Expressions and Clojures

To create an anonymous function in Type-C, you can use the fn keyword, followed by parameter declarations and the function's body (notice that it has no name, because it is anonymous). These functions create a contained scope, making them perfect for tasks like mapping, filtering, or reducing data collections. Let's take a look at an example of an anonymous function in Type-C:

In this code snippet, the map function receives an anonymous function that specifies how each element in the numbers array should be transformed. Closures, an extension of anonymous functions, introduce a fascinating concept—they can capture variables from their surrounding lexical scope. These variables become "bound" to the closure's context, even after the enclosing function has completed its execution. Closures are invaluable for creating functions that encapsulate state, resulting in elegant and concise code patterns.

Consider this example of a closure in Type-C:

Let in Expression

The let .. in construct allows you to introduce a new variable and make it available within a specific expression's scope. This construct is particularly useful when you have a temporary value that is only needed within a limited part of your code and you want to avoid polluting the outer scope. Here's the general syntax:

In this construct, expression1 is evaluated, and its value is bound to var. Then, expression2 is evaluated with var available as a variable. Here's an example:

In this example, x is bound to 2, and then x * x is evaluated, yielding 4. Notice that x is not available outside the in block:

Unlike the let statement, let .. in expression evaluates to a value (expression2), which means it can be part of a larger expression.

The let .. in construct is optimized for use at the root level of an expression. However, it can technically appear within other expressions. When doing so, it's important to explicitly scope the let .. in block with parentheses to avoid issues with precedence. Failing to do so will often result in compile-time error about unexpected.

Developers are encouraged to use let .. in at the root level or to always include parentheses when nesting within other expressions to ensure clarity and correct evaluation order.

In the code above, the createCounter function returns a closure that captures the count variable. Each time the closure is invoked, the count increments, and this state is preserved across multiple calls, illustrating the power of closures. Anonymous functions and closures in Type-C elevate the language's capabilities, allowing developers to craft concise and expressive code. These constructs are especially valuable when it comes to encapsulating behavior and state within self-contained units. They enable modular and clean coding practices, making your code more readable and maintainable.

Literal Expression

A literal expression is any literal values, and there few subcategories:

  • Literal strings such as "hello" which gets transformed into standard String objects by the compiler
  • Binary string literals which are treated as byte arrays such as b"hello"
  • Binary integer literal
  • Octal integer literal
  • Decimal integer literal
  • Hexadecimal integer literal
  • Float literal
  • Double literal
  • Boolean literal
  • Null literal

Match Expression

Pattern matching is discussed in its own section at Pattern Matching, but they can be used as expressions as well. A match expression must always return a value (otherwise it is not an expression), hence, a match expression always require a default wildcard branch (even if semantically it is not needed) since current, Type-C doesn't perform exhaustiveness check on match expressions.

In the previous example, the match expression is used as the body of the fib function. The match expression evaluates to a value, which is then returned by the function. This is a common pattern in Type-C, where a function's body is a single expression that evaluates to a value. This is a powerful and concise way to write functions, and it is encouraged in Type-C.

Member Access Expression

A member access expression is used to access a member of an object, such as a field or method. The syntax is as follows:

Member access is not limited to classes, as it is also used for getting the fields of a structure, variant, static class methods/attributes, etc.

In the last line, there are 2 member access expression, one is applied to the Error datatype to access its constructor Ok and the second is on the variant constructor to access its attribute code.

Named Struct Expression

As the name suggests, this expression is used to create structs with named fields. The syntax is as follows:

new Expression

The new expression is used to create a new instance of a class. The syntax is as follows:

Also, new is also used to create lock objects:

Nullable Member Access Expression:

The nullable member access expression is used to access a member of an object that may be null. Its behavior is very similar to the member access expression, but it can only be applied to nullable types. The syntax is as follows:

The Nullable member access expression has two behaviors:

  1. When used without the nullish coalescing operator ??, it returns a nullable type.
  2. When used with the nullish coalescing operator ??, it returns a non-nullable type.

Case 1: Nullable Member Access without Nullish Coalescing Operator

The result of a nullable member access expression, must be nullable. For example:

Since the point is nullable, the result of point?.x has the potential to be null, but since the type of the attribute x is u32, and a u32 cannot be a nullable (numbers can't be null!), the compiler will generate an error.

Since String is a class and can be nullable, the result of point?.desc is a nullable String.

Case 2: Nullable Member Access with Nullish Coalescing Operator

When using ?. with ??, there is a guarantee that the result will be non-nullable. For example:

The compiler will realise that the result of point?.x is nullable, and since the nullish coalescing operator ?? is used, the result of the expression will be a valid data type.

spawn Expression

Spawn expression is used to spawn a thread that runs concurrency with the main thread. The syntax is as follows:

This Expression:

The this expression is used to access the current object's fields and methods. Hence it is only valid within a class.

Unary Expression:

Unary expressions are expressions that involve a single operand and an operator.

Unnamed Struct Expression

This expression is used to create structs with unnamed fields. Notice that such expression requires a type hint, since the compiler can't infer the type of the struct. The syntax is as follows:

When creating an unnamed struct, the order of the fields must match the order of the fields in the struct definition. Also, the fields must be comma-separated.

Do Expressions

Do expressions provide a clear and concise way to execute a block of code and return a value. Unlike standard blocks, which are used for grouping statements, do expressions evaluate the block as an expression and return a result. The syntax is as follows:

In this example, the variable x is assigned the result of the do block, where the local variable y is computed and subsequently returned. The key feature is that the entire block is treated as a single expression, with its return value becoming the result of the assignment.

Purpose and Clarity

By using do expressions, you encapsulate a sequence of computations in a clear, self-contained block, making the intent of your code more obvious. Instead of having to analyze line-by-line to infer the purpose of intermediate steps, the do keyword immediately signals that the block is intended to produce a final result. This approach promotes cleaner, more readable code, particularly in cases where complex logic or multiple statements are needed to compute a value. The do expression helps to separate the “how” of the computation from the “what” of the result, improving both intent and structure.

Example Usage:

Compare it against:

Variables declared within the do block are not visible outside of it. This helps to avoid variable name clashes and to make the code more readable. Each do block has its own local scope, ensuring that the intermediate variables used in computations don’t interfere with the surrounding code.

This approach promotes cleaner, more readable code, particularly in cases where complex logic or multiple statements are needed to compute a value. The do expression helps to separate the “how” of the computation from the “what” of the result, improving both intent and structure.

On top of that, this can potentially be optimized by the compiler, since the entire block can be treated as a single expression, and the compiler can optimize the code accordingly.

Potential Pitfalls

Returns within a do expression must be returned with the return keyword. Meaning if you are within a do expression within a function, you cannot return from the function by returning from the do expression.

Unreachable Expression

unreachable is a keyword and expression, you can use it in part of your code to denote that the code should never be reached. The use of unreachable is circumstantial, but as in type-c, all expression requires a return value, hence, in cases where you are sure that; say you have covered all possible cases in a match expression, you can use unreachable to denote that the code should never be reached.

Under the hood, unreachable has a special data type associated with it, that matches with any existing type.

Mutate Expression

mutate allows you to create a copy of a constant value, but with a different name. The usage of mutate is considered an escape hatch, and should be used with caution.


Kudos! Keep reading!