Frugurt is an interpreted language, with focus on functional and OOP.
It is proof-of-concept, showing interesting features in active development still
The main purpose of Frugurt is to present an entirely different approach to OOP, compared to other languages like Python or JavaScript.
Example
let square = fn(x) {
// if the last expression has no semicolon then it is returned
x * x
};
let square_other = fn(x) {
// or you can use return keyword
return x * x;
};
// square_other is equivalent to square
print(square(7)); // 49
My main goal is to make objects strictly typed (not variables!).
All types have fixed schema, that means:
- All fields must be declared at once
- Any other fields can never be declared
Also, there are three flavors of types:
struct
- mutable, passed by valueclass
- mutable, passed by referencedata
- immutable, passed by reference
struct Vector {
x;
y;
} impl {
static new(x, y) {
return Vector:{ x, y };
}
add(other) {
// fields are accessible like in complied languages
// there are static fields too (see docs)
Vector:{x + other.x, y + other.y }
}
}
// you can define operator with any name you want!
operator <+> (v1 : Vector, v2 : Vector) {
v1.add(v2) // no semicolon = return
}
let v1 = Vector.new(1, 2);
let v2 = Vector.new(3, 4);
let v3 = v1 <+> v2;
print(v3); // Vector{x=4, y=6}
Getting started
Let's start! This section contains information on:
- Installing Frugurt on Windows and Linux
- Writing a program that prints
Hello, World!
Installation and Running
Prebuilt executable
Prebuilt executable is only available for Windows.
You can download if from release page.
Build from source code
You can build Frugurt from source code on any platform.
Use Rust Toolchain to build interpreter.
Hello world
Frugurt is an interpreted language, so you do not need any setup for it.
Create new frugurt file ending in .fru
, for example hello-world.fru
Filename: hello-world.fru
print("Hello, World!");
Common concepts
This chapter covers all concepts that exist in Frugurt, some of them appear in almost every programming language, but since Frugurt is an experimental language, it has a big set of distinct features.
Specifically, you’ll learn about variables, basic types, comments, control flow, functions and currying.
Variables and Types
These are primitive types in Frugurt, they can be assigned to variables.
Nah
Number
Bool
String
There are also Function
s and custom types
Nah
let x = nah;
print(x);
Number
let x = 7;
let y = 3;
print(x + y, x * y); // 10 21
Bool
let x = true;
let y = false;
print(x && y, x || y); // false true
String
let x = "hello";
let y = "world";
print(x <> ", " <> y); // hello, world
Function
let f = fn(x, y) {
return x + y;
};
print(f(1, 2)); // 3
We will learn more about functions in the corresponding chapter.
Comments
Comments are used to document the code, they are not executed and are ignored by the compiler.
// This is a comment
let a = 3;
/*
This is multiline comment
I can be an many lines long as you wish
*/
print(a); // 3
Control Flow
The control flow in Frugurt can be implemented using both statements and expressions.
Conditionals
using statements
let age = 16;
if age < 12 {
print("child");
} else if age < 18 {
print("teenager"); // this branch is executed
} else {
print("adult");
}
using expressions
let age = 16;
print(
// this big expression is evaluated to "teenager"
// and then returned to print function
if age < 12 {
"child"
} else if age < 18 {
"teenager"
} else {
"adult"
}
);
Loops
There is only while
loop statement in Frugurt for now
let i = 0;
while i < 10 {
print(i); // 0 1 2 3 4 5 6 7 8 9
i = i + 1;
}
Functions
Functions can be created with the fn
keyword.
let f = fn (x) {
x + 1
};
print(f(5)); // 6
Functions can take other functions as arguments, and they capture their scope.
let f = fn (x) {
fn (y) {
x + y
}
};
print(f(5)(10)); // 15
They can also return other functions. Function that takes function and returns the modified version of it is called decorator.
let decorator = fn (func) {
fn (x) {
func(func(x))
}
};
let f = fn (x) {
x * 2
};
f = decorator(f);
print(f(5)); // 20
Functions can have named parameters. They must go after positional parameters.
let f = fn (x, y=1) {
x + y
};
print(f(5)); // 6
print(f(5, 10)); // 15
print(f(y: 10, x: 5)); // 15
Named parameters can be computed using positional ones and other named parameters that go before them.
let f = fn (x, y=x + 5, z=x + y + 5) {
x + y + z
};
print(f(5)); // 35
Functions can be curried, will talk about in the next chapter.
Currying
You can apply first n arguments to a function to make a new function that accepts the rest of the arguments. This is called currying. Curried can be curried as many times as you want.
let add = fn(a, b) {
a + b
};
let add3 = add$(3);
print(add3(7), add(1, 2)); // 10 3
let five = add3$(2);
print(five()); // 5
You can apply arguments not in order using named arguments.
let combine = fn(a, b, c) {
a + 2 * b + 3 * c
};
let g = combine$(a: 2, c: 3);
print(g(b: 1)); // 13
Object oriented programming
Frugurt's main goal is to present an entirely different approach to OOP, using the best sides of scripting and compiled languages.
This chapter explains my take on OOP.
Basics
Types come in 3 flavors:
struct
- mutable, copied by valueclass
- mutable, copied by referencedata
- immutable, copied by reference
You can label the fields to make code more readable. You can instantiate a class either by labeling all the fields or labeling none. Labeling part of the fields is not allowed.
struct Vector {
// fields can be marked public or/and annotated with type,
// but this does not change semantics(now)
x;
pub y : Number;
}
let v = Vector:{ x: 5, y: 10 };
print(v); // Vector{x=5, pub y: Number=10}
// struct is copied by value, so `a` is not the same object as `v`
let a = v;
a.x = 1;
print(v, a); // Vector{x=5, pub y: Number=10} Vector{x=1, pub y: Number=10}
let v2 = Vector:{ x: 5, y: 10 };
// let v2 = Vector:{ 5, y: 10 }; // would throw an error
Operators
You can define any operator between any two types(even builtin ones).
struct Vector {
x;
y;
}
operator + (a : Vector, b : Vector) {
Vector:{
a.x + b.x,
a.y + b.y
}
}
operator += (a : Vector, b : Vector) {
a.x = a.x + b.x;
a.y = a.y + b.y;
}
commutative operator * (k : Number, b : Vector) {
Vector:{
k * b.x,
k * b.y
}
}
let a = Vector :{ 1, 2 };
let b = Vector :{ 3, 4 };
print(a + b); // Vector{x=4, y=6}
print(a * 2, 2 * a); // Vector{x=2, y=4} Vector{x=2, y=4}
a += b;
print(a); // Vector{x=4, y=6}
Operator precedences from highest to lowest:
- All custom operators
**
<>
*
/
%
+
-
<
>
<=
>=
==
!=
&&
||
All operators are left associative
Methods
You can access fields and methods by name, there is no this
or self
keyword.
struct Vector {
x;
y;
} impl {
rotate90() {
let old_x = x;
x = -1 * y;
y = old_x;
}
rotate180() {
rotate90();
rotate90();
}
}
let v = Vector:{ 4, 5 };
v.rotate90();
print(v); // Vector{x=-5, y=4}
v.rotate180();
print(v); // Vector{x=5, y=-4}
Statics
You can define shared fields and methods that are available to all instances of a type.
struct Vector {
x;
y;
static scaler = 10;
} impl {
static scale(v) {
Vector :{ v.x * scaler, v.y * scaler}
}
static double_scaler() {
scaler = scaler * 2;
}
}
let v = Vector :{ 1, 2 };
print(Vector.scale(v)); // {x: 10, y: 20}
Vector.double_scaler();
print(Vector.scale(v)); // {x: 20, y: 40}
Properties
Properties are type members that externally look like fields, but internally behave like methods.
struct Vec {
x;
y;
Length {
get => (x * x + y * y) ** 0.5;
set(new_length) {
let k = new_length / Length;
x = x * k;
y = y * k;
}
}
}
let v = Vec :{ x: 3, y: 4 };
print(v.Length); // 5
v.Length = 1;
print(v); // Vec { x: 0.6, y: 0.8 }
In this example, Vec has a "property" Length that is easily computable from other fields.
Like methods, properties can access fields, methods and other properties of the object.
(new_length)
can be omitted, in which case the default identifier value
is used.
Properties can be static.
Also, there is no need to implement get
and set
every time.
class Time {
static time = 0;
pub static Now {
get { time } // this is equivalent to `get => time;`
}
}
print(Time.Now);
In this example, imagine game engine.
Static field time is updated by game engine every frame, and public property Now
can be used to get current time
on the user side.
Scope manipulation
Frugurt supports explicit scope capturing and subsequent manipulation.
Scope keyword
Scope keyword can be used in three constructs:
scope()
- captures scope in which it was evaluatedscope s { statements... }
- run statements in specified scopescope s { statements... expression }
- run statements in specified scope and return result of expression
Example:
let f = fn () {
let a = 5;
let b = 3;
scope()
};
let scope_object = f();
print(scope_object.a); // 5
scope scope_object {
// this statement is executed in the same scope as the body of function f ran
// so the variables a and b are available here
print(a * b); // 15
}
scope_object.a = 10; // old variables can be re-assigned
scope_object.c = 20; // new variables can be declared
print(scope scope_object {
let r = a + c;
r * b
}); // 90
Imports
Other files can be imported into your code by using the import
expression.
Import expression returns the same scope object, which was mentioned in the previous chapter.
Example:
main.fru
let foo = import "foo.fru";
print(foo.f(1, 2)); // 3
// this is as badass as extremely stupid
scope foo {
let wow = 5;
print(omg()); // 5
}
foo.fru
let f = fn(x, y) {
x + y
};
let omg = fn() { wow };