We are currently working on new rules for what content should and shouldn't be allowed on this website, and are looking for feedback! See Esolang:2026 topicality proposal to view and give feedback on the current draft.

Legato

From Esolang
Jump to navigation Jump to search
This entry is not about the playing technique 'legato'.

Legato is a programming language designed by PSTF.

Overview

Type System
Statically strong-typed, with full type inference support (Hindley-Milner style).
Paradigm
A mix of imperative, functional (Lambda calculus), and object-oriented programming.
Core Features
First-class functions, closures, tail call optimization, safe pointers, structs, and classes.
Design Philosophy
Explicit over implicit, combining low-level control with high-level abstractions.

Architecture

Variable and Control Flow

Legato supports both advanced structured control flow and explicit conditional jumps.

Jump Statement

label labelname:
    # Blahblahblahblahblah

jump -> labelname;
jump_if condition -> labelname;

If-Else Statement

if condition {
    # code
} elif condition {
    # code
} else {
    # code
}

While Statement

while condition {
    # code
}

Iterative Loop

for item in container {
    # code
}

Data Types

Literal Types

let name <- "Legato";
let greeting <- f"Hello, {name}!"; # "Hello, Legato!"
let character <- name[0] # Uppercase L

let x <- 4; # Integer
let y <- 3.14159; # IEEE Float
let pi <- 3.1415926535897932384626433832795...D; # Scientific Float, Decimal
let rational <- 0.`3`D; # Absolute rational like what in Polynomix or SletScript
let oneseventh <- 1/7; # Unless converted to IEEE Float, otherwise the result should be exactly fractional.

let legato <- true; # Boolean
let arpeggio <- nil; # Null value

Composite Types

let P <- (1, 5); # Ordered pair
let mut users [str] <- ["Administrator", "Adam", "Henry", "Catherine", "Sophie", "Mike", "Raymond"];
# Array
let newUser <- "Kelvin";
users.push(newUser); # Append new element. Lists can also be joined together.
let user2 <- users[2]; # Should return Henry.

let mut miscallenous [any] <- [114514, 1919810, "Hello, World!", "明月几时有", nil, [1, 1, 4, 5, 1, 4]];
# Any is also a type which means the universal type.

Functions, Lambda Calculus, Closures, and Recursion

Legato treats functions as first-class citizens, fully supporting lambda calculus (anonymous functions), environment capture (closures), and tail call optimization.

Normal Functions

fn add(a: int, b: int) -> int {
    ret a + b;  # Explicitly return
}
fn greeting(name: str) -> nil {
    print(f"Hello, {name}!");
}

Lambda Calculus

fn apply_twice(f: lambda(int -> int), x: int) -> int {
    ret f(f(x));
}
let output <- apply_twice(lambda(x -> x * 2), 3); # 12

Closures

fn make_multiplier(factor: int) -> lambda(int -> int) {
    # Inner anonymous function captured "factor"
    ret lambda(x -> x * factor);
}
let double <- make_multiplier(2);
print(double(10)); # Return 20 since the closure kept factor as 2

let triple <- make_multiplier(3);
print(triple(10)); # 30, because we generated another function by closure

Recursion & Regression

fn factorial_tail(n: int, accumulator: int) -> int {
    if n <= 1 {
        ret accumulator;
    }
    # Compiler will optimize the tail-call recursion
    ret factorial_tail(n - 1, n * accumulator);
}

# Fibonacci function: Tree-like recursion
fn fib(n: int) -> int {
    if n <= 1 { ret n; }
    ret fib(n - 1) + fib(n - 2);
}

OOP Example

# --- Base class ---
class Animal {
    # Attributes(Private things, publicize by getter/setter when needed)
    priv name: str;

    # Initialization (With name constantly as new)
    fn new(name: str) -> Animal {
        this.name <- name;
        ret this;
    }

    # Methods
    fn speak(self) -> str {
        ret "{self.name} makes a sound.";
    }

    # Getter
    fn get_name(self) -> str {
        ret self.name;
    }
}

# --- Derive Class (Inherit) ---
class Dog extends Animal {
    priv breed: str;

    fn new(name: str, breed: str) -> Dog {
        # Call the initialization of parent-class
        super(name);
        this.breed <- breed;
        ret this;
    }

    # Method Overriding (Polymorphism)
    override fn speak(self) -> str {
        ret "{super.speak()} Actually, it barks!";
    }

    fn fetch(self) {
        print("{self.get_name()} fetches the stick.");
    }
}

# --- Usage of OOP ---
let animal <- Animal("Generic");
let dog <- Dog("Buddy", "Labrador");

print(animal.speak()); # "Generic makes a sound."
print(dog.speak());    # "Buddy makes a sound. Actually, it barks!"
dog.fetch();           # "Buddy fetches the stick."

Example Programs

Calculator

class Calculator {
    #* A simple calculator. *#
    
    priv history: [str];

    fn new() -> Calculator {
        this.history <- [];
        ret this;
    }

    fn operate(self, a: decimal, b: decimal, op: lambda(decimal, decimal -> decimal) -> decimal {
        let result <- op(a, b);
        self.history.push(f"{a} {op} {b} <- {result}");
        ret result;
    }

    fn show_history(self) {
        for entry in self.history {
            print(entry);
        }
    }
}
# Main function is the entry of program.
fn main() {
    let calc <- Calculator();
    let add_lambda <- lambda(a: decimal, b: decimal -> a + b);
    let result <- calc.operate(0.1d, 0.2d, add_lambda);
    print("0.1 + 0.2 <- {result}"); # 0.3
    # Why not 0.30000000000000004? Because decimal is not IEEE float.
}

Output First 100 square numbers

let numbers <- interval(1, 100, 1, true, true)
fn main() {
    let squared <- map(numbers, lambda(x: int -> x ^ 2));
    print(squared)
}

See Also

Guidelines

Writing a program is essentially a process of communication between humans and computers.  

Maybe for a computer, the format of a program doesn't really matter, as long as it doesn't produce errors. But for humans, if the program is too messy or the language used to write it is too obscure, it becomes difficult to understand.  

That's why coding standards are especially important.  

First of all, even though we have the concept of "code blocks," indentation is still very important—if the indentation within the same block is off, it just means that this code is either decorative or unreadable. The indentation levels should always keep same in one program, and 4 spaces for best.  

Secondly, readability matters. The design of this language is intended to make programs understandable to humans, so a block of code formatted properly is much better than a jumble of symbols.  

Of course, beyond just making code readable, sometimes you should add some comments—so its functionality is fully explained within the same file, unless you are really working on a project made up of several, or dozens of, or even a big number of, files.  

Sometimes flat code alone isn't enough to achieve certain functions, but flat code is at least easier to extend than deeply nested code. Nesting is a double-edged sword—it can be a useful tool if used well, or a mountain of mess if used poorly.  

Simplicity doesn't just come from how powerful its functions are or how many commands it has—it's also about whether you can easily build some fancy projects. While unificated idea is an important part of achieving simplicity, it's ultimately just one part, not the whole picture.  

A problem should have—ideally only—one obvious way to solve it. But that doesn't mean programming has to be monotonous and formulaic! Sometimes individuality is allowed, as long as the final result is correct. Even if the process is flashy and esoteric, it doesn't matter—sometimes known as "all wrong in process, all right in result."  

Dense code can sometimes achieve special effects, but a piece of code shouldn't be a lump of symbols squeezed together; it should be an organism made up of commands and symbols, and sometimes comments.  

For an object-oriented programming language like this, each instance of a class is a living individual, not just a bunch of lifeless zeros and ones.  

A program is like a building—without a solid foundation, no matter how fancy the construction on top is, it's just a castle in the air and will collapse eventually.  

A programming language itself is like a baby—when it's created, it needs time to grow, and our maintenance, expansion, and refinement are the best ways to care for that baby.  

Programming languages may change like flowing water over time, but algorithms are eternal and unchanging. The same algorithm can always be implemented in ordinary languages or peculiar ones, and Legato is no exception. We can't create the perfect programming language all at once, but we can continuously improve one with room for growth. Making information technology more human-friendly is the ultimate goal of Legato, and it should also be the ultimate goal of all programming languages—whether it's my Don Giovanni or MØSS, or any language created by others like Sclipting, Polynomix, and LOLCODE, or even the computational models themselves.

Categories