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 value
  • class - mutable, passed by reference
  • data - 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}

See docs for more details

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 Functions 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 value
  • class - mutable, copied by reference
  • data - 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 evaluated
  • scope s { statements... } - run statements in specified scope
  • scope 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 };