Introduction
Rock is an experimental native language.
Its main goal is to mix some parts of popular functionnal languages like haskell or livescript with the rigor of Rust while staying elegant and fast with minimal runtime
Rock is at an early development stage. Don't expect everything to work smoothly. (you have been warned)
struct Player
level: Int64
name: String
impl Player
new: x ->
Player
level: x
name: "MyName"
impl Show Player
@show: -> @name + "(" + @level.show! + ")"
use std::print::printl
impl Print Player
@print: -> printl @
main: ->
let player = Player::new 42
player.print!
Rock syntax is entierly based on indentation, like Livescript.
Each whitespace count, and tabulations \t
are prohibited.
The number of whitespace to make one level of indentation is taken from the first indent level.
If your first indentation has two whitespaces, the rest of the file must have the same number of whitespace per level (here two)
We generally use two as a default, but you can use any number you want. Here is the same example with four whitespaces:
struct Player
level: Int64
name: String
impl Player
new: x ->
Player
level: x
name: "MyName"
Quick start
The actual best way to start is to build and install Rock via cargo:
cargo install --git https://github.com/Champii/Rock --locked
rock -V
Then to create a new empty project folder
rock new my_project
cd my_project
The project folder should contain a src
folder with a src/main.rk
file that should look like this:
main: -> "Hello World !".print!
You can immediately build and run this default snippet with
rock run
This should output
Hello World !
The compiler has created a build
folder containing your compiled executable build/a.out
Language reference
Primitives
Rock actually support some essential primitives.
Boolean
The Bool
type
main: ->
true.print!
false.print!
Integer
The most stable integer is Int64
and should be used until the rest follows.
main: -> 0
Rock has an inference mechanism that automatically assign types to variables and primitives
Here the main
function is special and has a fixed signature (Int64)
so 0
is a Int64
You can learn more about Function signature
Float
main: ->
let x = 2.2
x.print!
Char
main: -> '*'.print!
String
Strings are immutable (like in most language)
main: -> ("Hello" + " World!").print!
You can index them to get a Char
second_char: -> "Hello"[1]
Strings are 0-indexed
Both Strings and Chars accept some escaped characters:
\\ \' \" \n \r \0
In the future there will be a distinction between native Str
and on-the-heap String
like in Rust
Array
There is very limited support for arrays, they are still in development.
main: ->
let a = [1, 2, 3]
a[2] = 4
a[2]
Function
Syntax
The function declaration format is:
function_name: arg1, arg2, arg3 -> return_value
The function call format is:
function_name arg1, arg2, arg3
But you can add parenthesis
function_name(arg1, arg2, arg3)
Every Rock package must have a ./src/main.rk
file containing a main
function
main: -> "Hello World!".print!
You can call functions with no args with a bang !
like the .print!
above or with
explicit parenthesis like .print()
But the idiomatic way is to avoid parenthesis as much as possible
add: x, y -> x + y
main: -> add 2, 3
Rock does not allow for uppercase identifiers, so you should embrace the snake case. Uppercase names are reserved for custom types like Struct or Trait
Polymorphism
Every function in Rock is polymorphic by default, and only infer the types based on the caller and the return value of its body. Multiple calls with different types will generate each corresponding function, just like the templates in C++ or in Rust, except the generic parameter is always implicit if no constraints have been made.
For example, lets declare the most polymorphic function of all, id
:
id: x -> x
This function takes a x
argument and returns it. Here x
can be of any type
main: ->
id 42
id 6.66
id "Hello"
id my_custom_struct
The infered signature of the function is id: a => a
, with a
being any type
If we had changed the body of id
to be:
id: x -> x + x
the previous main would still work if all of the types implemented the Num
trait from the stdlib, that provide implementation of +
for the basic types
Function signature
Syntax
foo: a => Int64 => String
foo: x, y -> "bar"
Here, the foo
function takes 2 arguments, of type a
(generic) and Int64
, and returns a String
The implementation ignore the two arguments and returns "bar"
A signature is always formed of at least one type.
The last (or only) type is the return type
Functions can take functions as parameter, and must be representable in a signature:
my_func: a => (a => b) => b
my_func: x, f -> f x
# A function of type (a => b) that resolves to (Int64 => String)
handler: x -> x.show!
main: -> my_func 42, handler .print!
Here the second argument of the function my_func
is a function that take a generic type a
and returns a type b
, and the whole method returns a type b
This outputs:
42
Structure
Declaration:
Here is the idiomatic way to declare a structure
struct Counter
value: Int64
name: String
This structure has two fields value
and name
with their types respectively Int64
and String
Implementation
You can attach some methods to the structure, for example here a class-method new
that takes a x
and is used as a constructor
The increment
method is an instance-method that takes nothing, increments the value
field by 1
and returns @
impl Counter
new: x ->
Counter
value: x
name: "Counter"
@increment: @->
@value = @value + 1
You can learn more about the @
parameter here: Self
main: ->
Counter::new(41)
.increment!
.value
.print!
This prints 42
*Note: We could have written something more compact, but less readable
impl Counter
new: x -> Counter value: x, name: "Counter"
@increment: @-> @value = @value + 1
main: -> Counter::new 41 .increment!.value.print!
Trait
Declaration
The trait feature is very similar to what Rust have
You can define some 'interfaces' that has some methods, you can then implement them for your types.
For example, we will implement a trait that double itself with the +
operator
trait CanDouble
@double_me: @
We define a trait CanDouble
and declare a method double_me
that returns the generic self type @
.
Implementation
We can now implement the trait for any type we want, like Int64
impl CanDouble Int64
@double_me: -> @ + @
We can then call this method:
main: -> (2).double_me!.print!
This output
4
Default method
You can define trait methods that have a default implementation.
That means you don't have to reimplement it for each type, but you can override the default implementation with your own if you need it.
trait CanDouble
@double_me: -> @ + @
impl CanDouble Int64
main: -> (2).double_me!.print!
Overriding
You can override a default implementation
trait CanDouble
@double_me: -> @ + @
impl CanDouble Int64
@double_me: -> @ * 2
main: -> (2).double_me!.print!
Self
The self
parameter
The self
parameter is represented by the @
symbol.
impl CanDouble Int64
@double_me: -> @ + @
This method desugars to:
impl CanDouble Int64
double_me: self -> self + self
Those are strictly equivalent, as @
desugar to self
We can see that we also have a @
at the start of the name of the method. This allows for auto-injection of the self-parameter:
Auto-inject self parameter
The standard way of defining a self-method is to auto-inject the self parameter:
impl CanDouble Int64
@double_me: -> @ + @
Auto-return self
And if you also wanted to return self
for chainable capababilities:
impl CanDouble Int64
@double_me: @->
@ = @ + @
main: ->
let x = 2
x.double_me!.double_me!.double_me!
x.print!
Outputs
16
The @->
automatically inject the @
as last returned statement of the function's body.
This operator is only available when using the @method_name:
notation