Types are blueprints which are used to create values for bindings.
Types can be basic (integer number, character, ...) or compound (sequence, map, struct, union).
Syntax: int, float, byte, char, string, bool, nothing
Notes:
int
type is a signed 8-byte integer data type.float
is double-precision 8-byte floating point number.byte
is an unsigned 8-bit number.char
is a single unicode character.
- Character literals should be enclosed in single-quote (e.g.
'a'
).
string
is a sequence of characters.
- String literals should be enclosed in double quotes.
- To represent double quote itself inside a string, you can use
\"
.
bool
type is same as int but with only two valid values.true
is 1 andfalse
is 0.nothing
is a special type which is used to denote empty/invalid/missing data. This type has only one value which is the same identifier.
Examples
int_val = 12
float_val = 1.918
char_val = 'c'
bool_val = true
str1 = "Hello world!"
str2 = "Hello" + "World!"
n: nothing = nothing
byte_val: byte = 119 #note that it is optional to mention type of a binding after its name
- Sequence is similar to array in other languages. It represents a fixed-size block of memory with elements of the same type,
T
, and is shows with[T]
notation. - You can initialize a sequence with a sequence literal (First example).
- You refer to elements inside sequence using
x[i]
notation wherei
is index number. []
represents an empty sequence.- Referring to an index outside sequence will throw a runtime error.
- Core defines built-in functions for sequence for common operations:
slice, map, reduce, filter, anyMatch, allMatch, ...
Examples
x = [1, 2, 3, 4]
#a 2D matrix of integer numbers
x: [[int]] = [ [1, 2], [3, 4], [5, 6] ]
#merging multiple sequences
x = [1, 2]+[3, 4]+[5, 6]
int_var = x[10]
#this is definition of string type
string = [char]
- HashMap is a hash table of key values.
- You can use
[KeyType:ValueType]
to define a map type. - When reading from a map, you will get runtime error if value does not exist in the map.
- An empty map can be denoted using
[:]
notation. - Core defines built-in functions for maps for common operations:
slice, map, reduce, filter, anyMatch, allMatch, ...
Examples
pop = ["A":1, "B":2, "C":3]
data = pop["A"]
- You can prefix any sequence literal with
enum
keyword and it will be an enum type. - Example:
MyEnumType = enum [sequence of literals]
- Variables of enum type must accept values of exactly what is specified inside the sequence, nothing else, even if they have equivalent value.
- You can combine enum with a map to implement execution control.
- In case of 4, Compiler will make sure you have covered all possible types, and if not, will issue a warning.
- Also core will have functions to implement
switch
on enums which make sure all cases are covered.
Examples
saturday=1
sunday=2
...
DayOfWeek = enum [saturday, sunday, ...]
x = [saturday: "A", sunday: "B", ...][my_day_of_week]
#definition of type bool
true=1
false=0
bool = enum [true, false]
- Bindings of union type, can store any of multiple pre-defined types.
- Union type are shown as
T1|T2|T3|...
. - You can use casting to check what's inside a union binding or cast it to a type.
Examples
int_or_float: int|float = 11
int_or_float: int|float = "ABCD"
my_int = int(int_or_float) #this will fail if input binding does not have an int
maybe_int = int|nothing(int_or_float) #if binding has a float, you will get a nothing as a result of this cast
- A struct, similar to C, represents a set of related named types.
- To create a binding based on a struct, you should use a struct literal (e.g.
Type{field1:value1, field2:value2, ...}
. - Type name in struct litearl and field names in struct type are mandatory.
- Optional fields: When creating a value of struct type and don't specify value for fields which can be
nothing
, they will be set tonothing
. - Edit: You can create a new struct value based an existing value. This will merge them all. (Example A).
- If struct literal type can be inferred from context, you can omit type and use
&{...}
notation (Example B).
Examples
#defining a struct type
Point = struct {x:int, y:int}
#create a binding of type Point, defined above
point2 = Point{x:100, y:200}
point2new = Point{100, 200}
#update an existing struct binding and save as a new binding
point4 = Point{x:point3.x, y : 101}
process = fn(x: struct {id:int, age:int} -> int) { x.age }
#A
another_point = Point{my_point, x:10}
third_point = Point{point1, z: 10, delta: 9}
#B
switchOnValue(my_number, &{value: 10, handler: AAA}, &{12, BBB}, &{13, CCC})
- You can name a type so you will be able to refer to that type later in the code.
- Type names must start with a capital letter to be distinguished from bindings.
- You define a named type similar to a binding:
NewType = UnderlyingType
. - The new type has same binary representation as the underlying type but it will be treated as a completely different type.
- You have seen examples of named types in previous sections (Union, enum, ...).
Examples
MyInt = int
IntArray = [int]
Point = struct (x: int, y: int)
- You can use
T : X
notation to defineT
as another spelling for typeX
. - In this case,
T
andX
will be exactly the same thing. - You can use a type alias to prevent name conflict when importing modules.
X
on the right must be a type name. It cannot be definition of a type.
Examples
MyInt : int
process = fn(x:MyInt -> int) { x }
- Order of search to resolve a type name:
- Current function
- Closure
- Module level
- At any level, if there are multiple candidates there will be a compiler error.
- Two named types are never equal.
- Two types T1 and T2 are identical/assignable/exchangeable if they have the same structure (e.g.
int|string
vsint|string
).
- We use
T(value)
notation to cast value to a specific type. - Casting can be done for primitive types, named types or unions.
- For union, you can use casting to get underlying type. If you cast a union binding to a wrong type, there will be a runtime error.
- You can cast a union to
T|nothing
to do a safe cast. You will getnothing
it type T is not inside the union binding. - Literals (e.g.
1
or"Hello world"
) will get value of the most primitive type inferred by the compiler (int
,string
, ...). - Based on 4, you cannot assign an untyped literal to a named type without casting. Because for example
1
literal is anint
literal not a named type that maps toint
.
Examples
MyInt = int
myint_var = MyInt(12)
Functions (or lambdas) are a type of binding which can accept a set of inputs and gives an output.
For example fn(int,int -> int)
is a function type (which accepts two integer numbers and gives an integer number) and fn(x:int, y:int -> int) { x+y }
is a function literal.
For generics (types and functions) see Advanced section.
functionName = fn(name1: type1, name2: type2... -> OutputType1, OutputTyp2, ...) { code block }
- Function names are camelCased.
- Functions contain a set of bindings and the last expression in the code block determines function output.
- If calling a function that returns multiple bindings, you can use
_
to ignore one of them. - There is no function overloading. Functions should have unique names in their context.
- You can alias a function by defining another binding pointing to it (example A).
- If a function has no input, you can can eliminate input/output type declaration part (Example B). In this case, compiler will infer output type.
- Optional arguments: When calling a function, you can ommit arguments that are at the end and accept
nothing
(Example C). - If a function is being called with literals (compile time known values), compiler will try to evaluate it during compilation (e.g. generics).
- Module level functions that start with
_test
and have no input/output are considered unit test functions. You can later instruct compiler to run them (Example D). - There is
assert
core function that can be used for checking assertions. You can disable assertions with a compiler flag. - You can chain multiple nested function calls in reverse order via
::
operator (Example E).
Examples
myFunc = fn(x:int, y:int -> int) { 6+y+x }
tester = fn(x:int -> int, string) {x+1, "a"}
int1, str1 = tester(100)
int1, _ = tester(100)
_, str1 = tester(100)
log = fn(s: string -> nothing) { print(s) } #this function returns nothing, pun not intended
process2 = fn(pt: Point -> int,int) { pt.x, pt.y } #this function returns a struct
process = fn(x: int|Point -> int) { ... } #this function can accept either int or Point or int|Point as input
process = fn(x:int -> int)
{
#if x<10 return 100, otherwise return 200
[x<10: 100, x>=10: 200][true]
}
#A
process = fn(x:int -> int) { x+1 }
process2 = process
sorted = sort(my_sequence, fn(x,y -> int) { x-y })
Adder = fn(int,int->int) #defining a named type based on a function type
sort = fn(x: [int], comparer: fn(int,int -> bool) -> [int]) {...} #this function accepts a function
map = fn(input: [int], mapper: fn(int -> string) -> [string]) ...
#B
process = fn{ 100 }
#C
seq = fn(start_or_length:int, end:int|nothing -> ...)
...
x = seq(10)
y = seq(1,10)
add = fn(a:int, b:int ->int) { a+b }
g = add(1,2)
#D
_testProcessWithInvalidInput = fn{...}
#E
resut = f(g(x))
result = x :: g :: f
# calculate average score for new good students
student :: filter(isGoodStudent, _) :: map(createNewStudent, _) :: calculateAverage
- We use a static dispatch for function calls.
- Also because you cannot have two functions with the same name, it is easier to find what happens with a function call.
- If
MyInt = int
, you cannot call a function which needs anint
with aMyInt
binding. - Fucntion resolution is done similar to type name resolution.
- Parameter types must be "identical/compatible" to function argument types, or else there will be a compiler error.
- For example if function argument type is
int | nothing
and parameter is anint
it is a valid function call (But not the other way around). - When you need a function of type
fn(T->U)
any function that can accept T (or more) and returns U (or less) works fine.
- All functions are lambdas.
- Functions are closure. So they have access to bindings in parent contexts (Module or parent function).
- You can use
_
to define a lambda based on an existing function. Just make a normal call and replace the lambda inputs with_
(Example A). - If lambda is assigned to a variable, it can invoke itself from the inside (Example B). This can be used to implement recursive calls.
Examples
rr = fn(nothing -> int) { x + y } #here x and y are captures from parent function/struct
test = fn(x:int -> PlusFunc) { fn(y:int -> int) { y + x } } #this function returns a lambda
fn(x:int -> int) { x+1} (10) #you can invoke a lambda at the point of declaration
#A
lambda1 = process(10, _, _) #defining a lambda based on existing function
#B
ff = fn(x:int -> int) { ff(x+1) }
- A binding assigns an identifier to a typed immutable memory location.
- A binding's value can be a literal value, an expression or another binding.
- The literal value can be of any valid type (integer number, function literal, struct literal, ...).
- Binding names must start with a lowercase letter (except bindings that define a generic type, more in Advanced section).
- You can define bindings at module-level or inside a function.
- Module-level bindings can only have literals as their value.
- Type of a binding can be inferred without ambiguity from right side value, but you also have the option to specify the type (Example A).
- If the right side of an assignment is a struct, you can destruct it into its elements by using comma separated values on the left side of
=
(Example B). - In destruction, you can also use underscore to indicate you are not interested in one or more of those elements (Example C).
- Binding name resolution is similar to type/function name resolution.
Syntax:
identifier = expression
identifier : type = expression
Examples
#A
x : int = 12
#type is inferred
g = 19.8
#B
a,b = Tuple{1, 100}
#C
a,_ = point
a,_ = single_element_struct
There are different types of resources which needs handling but we can categorize them into two main groups: Memory and others (sockets, network connections, DB connections, files, threads, ...).
The first type is handled via GC and the second type is automatically freed by runtime when the corresponding binding goes out of scope. These are similar to C++ destructors, but implemented only for scarce system resources and handle deallocation of those resources when they are no longer needed.
Any other mechanism to free/cleanup a resource, exposes mutation to the language which causes a lot of confusion.