Gradual Typing. #
Dynamic typing reduces the amount of friction when writing code. However, the additional freedom can result in more runtime errors. Cyber also supports static typing which provides compile-time guarantees to prevent runtime errors. Static typing also makes it easier to maintain and refactor your code.
Dynamic typing. #
A variable with the any
type can hold any value. It can only be copied to destinations that also accept the any
type. An any
value can be used as the callee for a function call or the receiver for a method call. It can be used with any operators.
Compile-time dynamic typing. #
Cyber introduces the concept of compile-time dynamic typing. This allows a local variable to gain additional compile-time features while using it as a dynamic value. It can prevent inevitable runtime errors and avoid unnecessary type conversions.
Local variables declared without a type specifier start off with the type of their initializer. In the following, a
is implicity declared as a number
at compile-time because number literals default to the number
type.
a = 123
The type can change at compile-time from another assignment.
If a
is then assigned to a string literal, a
from that point on becomes the string
type at compile-time.
a = 123
foo(a) -- Valid call expression.
a = 'hello'
foo(a) -- CompileError. Expected `number` argument, got `string`.
func foo(n number):
pass
The type of a
can also change in branches. However, after the branch block, a
will have a merged type determined by the types assigned to a
from the two branched code paths. Currently, the any
type is used if the types from the two branches differ. At the end of the following if
block, a
assumes the any
type after merging the number
and string
types.
a = 123
if a > 20:
a = 'hello'
foo(a) -- Valid call expression. `foo` can be called without type casting.
foo(a) -- CompileError. Expected `string` argument, got `any`.
func foo(s string):
pass
Default types. #
Static variables without a type specifier will always default to the any
type. In the following, a
is compiled with the any
type despite being initialized to a number literal.
var a: 123
a = 'hello'
Function parameters without a type specifier will default to the any
type. The return type also defaults to any
. In the following, both a
and b
have the any
type despite being only used for arithmetic.
func add(a, b):
return a + b
print add(3, 4)
Static typing. #
In Cyber, types can be optionally declared with variables, parameters, and return values.
The following builtin types are available in every namespace: bool
, number
, int
, string
, list
, map
, error
, fiber
, any
.
An object declaration creates a new type.
object Student: -- Creates a new type named `Student`
name string
age int
gpa number
When a type specifier follows a variable name, it declares the variable with the type. Any operation afterwards that violates the type constraint will result in a compile error.
a number = 123
a = 'hello' -- CompileError. Type mismatch.
Parameter and return type specifiers in a function signature follows the same syntax.
func mul(a number, b number) number:
return a * b
print mul(3, 4)
print mul(3, '4') -- CompileError. Function signature mismatch.
Type specifiers must be resolved at compile-time.
object Foo:
a number
b string
c Bar -- CompileError. Bar is not declared.
Circular type references are allowed.
object Node:
val any
next Node -- Valid type specifier.
Type aliases. #
The atype
keyword can be used to create a type alias. This creates a new type symbol for an existing data type.
import util './util.cy'
atype Vec3 util.Vec3
v = Vec3{ x: 3, y: 4, z: 5 }
Runtime type checking. #
Since Cyber allows invoking any
function values, the callee’s function signature is not always known at compile-time. To ensure type safety in this situation, type checking is done at runtime and with no additional overhead compared to calling an untyped function.
op any = add
print op(1, 2) -- '3'
print op(1, '2') -- Panic. Function signature mismatch.
func add(a number, b number) number:
return a + b