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
Precedence | Operator | Type | Associativity | Overloadable |
---|---|---|---|---|
16 | unreachable | Object creation | LTR | No |
16 | mutate | Object creation | LTR | No |
16 | coroutine | Object creation | LTR | No |
16 | yield, yield! | Object creation | LTR | No |
16 | new | Object creation | LTR | No |
16 | spawn | Process spawning | LTR | No |
16 | await | Asynchronous wait | LTR | No |
15 | () | Function call | LTR | Yes |
15 | [] | Array subscription | LTR | Yes |
15 | ?. | Nullable member selection | LTR | No |
15 | . | Member selection | LTR | No |
14 | ++ (postfix) | Unary post-increment | LTR | Yes |
14 | -- (postfix) | Unary post-decrement | LTR | Yes |
13 | ++ (prefix) | Unary pre-increment | RTL | Yes |
13 | -- (prefix) | Unary pre-decrement | RTL | Yes |
13 | + | Unary plus | RTL | Yes |
13 | - | Unary minus | RTL | Yes |
13 | ! | Unary logical negation | RTL | Yes |
13 | !! | Denull | RTL | Yes |
13 | ~ | Unary bitwise complement | RTL | Yes |
12 | * | Multiplication | LTR | Yes |
12 | / | Division | LTR | Yes |
12 | % | Modulus | LTR | Yes |
11 | + | Addition | LTR | Yes |
11 | - | Subtraction | LTR | Yes |
10 | << | Bitwise left shift | LTR | Yes |
10 | >> | Bitwise right shift | LTR | Yes |
9 | < | Relational less than | LTR | Yes |
9 | <= | Relational less than or equal | LTR | Yes |
9 | > | Relational greater than | LTR | Yes |
9 | >= | Relational greater than or equal | LTR | Yes |
9 | is | Type comparison (objects only) | LTR | No |
9 | as, as? as! | Type casting | LTR | No |
8 | == | Relational is equal to | LTR | No |
8 | != | Relational is not equal to | LTR | No |
7 | & | Bitwise AND | LTR | Yes |
6 | ^ | Bitwise exclusive OR | LTR | Yes |
5 | | | Bitwise inclusive OR | LTR | Yes |
4 | && | Logical AND | LTR | Yes |
3 | || | Logical OR | LTR | Yes |
2 | ?? | Nullish coalescing operator | LTR | Yes |
1 | if,else | Conditional Expression | RTL | No |
0 | = | Assignment | RTL | No |
0 | += | Addition assignment | RTL | No |
0 | -= | Subtraction assignment | RTL | No |
0 | *= | Multiplication assignment | RTL | No |
0 | /= | Division assignment | RTL | No |
0 | %= | Modulus assignment | RTL | No |
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 __index__ 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.
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:
- class is interface
- interface is class
- interface is interface
- 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:
- When used without the nullish coalescing operator ??, it returns a nullable type.
- 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.