Introduction
monkey-rs
is a Rust implementation of the Monkey
programming language from Thorsten Ball's
Writing An Interpreter In Go.
"But why the name? Why is it called "Monkey"? Well, because monkeys are magnificent, elegant, fascinating and funny creatures. Exactly like our interpreter" — Thorsten Ball
What is Monkey?
Monkey is a programming language designed to teach the fundamentals of interpreter construction. It features:
- C-like syntax that's familiar and easy to read
- Variable bindings with
let
statements - Integers and booleans as basic data types
- Arithmetic expressions with standard operators
- Built-in functions for common operations
- First-class and higher-order functions supporting functional programming
- Closures that capture their environment
- String data type with built-in functions
- Array data type with built-in functions
- Hash data type for key-value storage
Getting Started
To start using monkey-rs, you can run the interactive REPL:
cargo run
This will launch the Monkey REPL where you can experiment with the language interactively.
Example Program
Here's a simple example of Monkey code:
let fibonacci = fn(x) {
if (x == 0) {
0
} else {
if (x == 1) {
1
} else {
fibonacci(x - 1) + fibonacci(x - 2);
}
}
};
fibonacci(10); // => 55
This documentation will guide you through all the features and capabilities of the Monkey programming language as implemented in Rust.
Syntax
Monkey has a C-like syntax that should feel familiar to programmers coming from languages like JavaScript, C, or Go. This section covers the fundamental syntax elements of the Monkey programming language.
Identifiers
Identifiers in Monkey must start with a letter, followed by any combination of letters, digits, or underscores:
let myVariable = 10;
let counter1 = 0;
Keywords
Monkey has the following reserved keywords:
let
- Variable bindingfn
- Function definitionif
- Conditional statementelse
- Alternative branch for conditionalsreturn
- Return statementtrue
- Boolean true literalfalse
- Boolean false literal
Operators
Arithmetic Operators
let a = 5 + 3; // Addition
let b = 10 - 4; // Subtraction
let c = 6 * 7; // Multiplication
let d = 15 / 3; // Division
Comparison Operators
let equal = 5 == 5; // Equality
let notEqual = 5 != 3; // Inequality
let less = 3 < 5; // Less than
let greater = 7 > 4; // Greater than
Logical Operators
let negation = !true; // Logical NOT
Statements vs Expressions
Monkey distinguishes between statements and expressions:
Statements
let
statements for variable bindingreturn
statements for returning values- Expression statements (expressions used as statements)
Expressions
- Literals (numbers, strings, booleans)
- Identifiers
- Prefix expressions (
!
,-
) - Infix expressions (
+
,-
,*
,/
,==
,!=
,<
,>
) - Function calls
- If expressions
- Function literals
Semicolons
Semicolons are generally optional in Monkey, but they can be used to explicitly terminate statements:
let x = 5
let y = 10;
Both lines above are valid. Semicolons are required when you want to put multiple statements on the same line:
let x = 5; let y = 10;
Data Types
Monkey supports several built-in data types that cover the most common programming needs. Each data type has its own characteristics and supported operations.
Integers
Integers in Monkey are 64-bit signed integers. They support standard arithmetic operations:
let age = 25;
let negative = -42;
let result = 10 + 5 * 2; // 20
Arithmetic Operations
- Addition:
+
- Subtraction:
-
- Multiplication:
*
- Division:
/
Comparison Operations
- Equal:
==
- Not equal:
!=
- Less than:
<
- Greater than:
>
Booleans
Monkey has two boolean values: true
and false
.
let isReady = true;
let isComplete = false;
let comparison = 5 > 3; // true
Boolean Operations
- Logical NOT:
!true
returnsfalse
- Equality:
true == true
returnstrue
- Inequality:
true != false
returnstrue
Strings
Strings in Monkey are sequences of characters enclosed in double quotes:
let name = "Alice";
let greeting = "Hello, World!";
let empty = "";
String Operations
- Concatenation is not directly supported with
+
, but you can use built-in functions - Strings can be compared for equality:
"hello" == "hello"
returnstrue
- String length can be obtained with the
len()
built-in function
Arrays
Arrays are ordered collections of elements that can contain different data types:
let numbers = [1, 2, 3, 4, 5];
let mixed = [1, "hello", true, [1, 2]];
let empty = [];
Array Operations
- Indexing: Access elements with
array[index]
(0-based indexing) - Length: Get array length with
len(array)
- First element: Get first element with
first(array)
- Last element: Get last element with
last(array)
- Rest: Get all elements except first with
rest(array)
- Push: Add element to end with
push(array, element)
let arr = [1, 2, 3];
let first = arr[0]; // 1
let length = len(arr); // 3
let tail = rest(arr); // [2, 3]
let extended = push(arr, 4); // [1, 2, 3, 4]
Hash Maps
Hash maps (or dictionaries) store key-value pairs. Keys must be hashable types (integers, booleans, or strings):
let person = {
"name": "Alice",
"age": 30,
true: "boolean key",
42: "integer key"
};
Hash Operations
- Access: Get values with
hash[key]
- Keys: Can be strings, integers, or booleans
- Values: Can be any data type
let config = {"debug": true, "port": 8080};
let debugMode = config["debug"]; // true
let port = config["port"]; // 8080
Functions
Functions are first-class values in Monkey, meaning they can be assigned to variables, passed as arguments, and returned from other functions:
let add = fn(a, b) {
a + b;
};
let result = add(5, 3); // 8
Function Characteristics
- Functions are closures (they capture their environment)
- Functions can be anonymous
- Functions can be higher-order (take or return other functions)
- The last expression in a function body is automatically returned
Null
Monkey has a null
value to represent the absence of a value:
let nothing = null;
let result = if (false) { 42 }; // result is null
Features
Monkey is a feature-rich programming language that supports both imperative and functional programming paradigms. This section covers the main features that make Monkey a powerful and expressive language.
Variable Bindings
Variables in Monkey are created using let
statements:
let name = "Alice";
let age = 30;
let isStudent = false;
Variables are immutable once bound - you cannot reassign them:
let x = 5;
// x = 10; // This would cause an error
Functions
Functions are first-class citizens in Monkey, supporting both named and anonymous functions.
Function Definition
let add = fn(a, b) {
a + b;
};
let greet = fn(name) {
"Hello, " + name + "!";
};
Higher-Order Functions
Functions can take other functions as parameters and return functions:
let applyTwice = fn(f, x) {
f(f(x));
};
let double = fn(x) { x * 2; };
let result = applyTwice(double, 5); // 20
Closures
Functions capture their lexical environment, creating closures:
let makeCounter = fn() {
let count = 0;
fn() {
count = count + 1;
count;
};
};
let counter = makeCounter();
counter(); // 1
counter(); // 2
Conditional Expressions
Monkey uses if-else
expressions (not statements) that return values:
let max = fn(a, b) {
if (a > b) {
a;
} else {
b;
};
};
let status = if (age >= 18) { "adult" } else { "minor" };
Return Statements
Functions can use explicit return
statements:
let factorial = fn(n) {
if (n <= 1) {
return 1;
}
n * factorial(n - 1);
};
Recursion
Monkey supports recursive function calls:
let fibonacci = fn(n) {
if (n < 2) {
n;
} else {
fibonacci(n - 1) + fibonacci(n - 2);
}
};
Array Operations
Monkey provides rich array manipulation capabilities:
let numbers = [1, 2, 3, 4, 5];
// Functional array operations
let doubled = map(numbers, fn(x) { x * 2 });
let sum = reduce(numbers, 0, fn(acc, x) { acc + x });
let evens = filter(numbers, fn(x) { x % 2 == 0 });
Hash Map Operations
Hash maps provide key-value storage:
let person = {
"name": "Bob",
"age": 25,
"city": "New York"
};
let name = person["name"];
let hasAge = "age" in person; // Note: 'in' operator may not be implemented
String Manipulation
While Monkey doesn't have built-in string concatenation with +
, it provides
string operations through built-in functions:
let greeting = "Hello";
let name = "World";
// String operations would typically be done through built-in functions
let length = len(greeting); // 5
Error Handling
Monkey handles errors at runtime. Invalid operations will produce error messages:
let result = 5 / 0; // Runtime error
let invalid = {}[42]; // Runtime error for invalid hash access
Scoping
Monkey uses lexical scoping with proper variable shadowing:
let x = 10;
let outer = fn() {
let x = 20; // Shadows outer x
let inner = fn() {
let x = 30; // Shadows both outer x values
x;
};
inner(); // Returns 30
};
Expression-Oriented
Most constructs in Monkey are expressions that return values, making the language very composable:
let result = if (condition) {
fn(x) { x * 2 }(5)
} else {
10
};
Built-in Functions
Monkey provides several built-in functions that are available globally without any imports. These functions provide essential operations for working with the language's data types.
Array Functions
len(array)
Returns the length of an array or string.
let numbers = [1, 2, 3, 4, 5];
let count = len(numbers); // 5
let text = "Hello";
let textLength = len(text); // 5
Parameters:
array
- An array or string
Returns:
- Integer representing the length
Errors:
- Throws an error if the argument is not an array or string
first(array)
Returns the first element of an array.
let numbers = [10, 20, 30];
let firstNum = first(numbers); // 10
let empty = [];
let firstEmpty = first(empty); // null
Parameters:
array
- An array
Returns:
- The first element of the array, or
null
if the array is empty
Errors:
- Throws an error if the argument is not an array
last(array)
Returns the last element of an array.
let numbers = [10, 20, 30];
let lastNum = last(numbers); // 30
let empty = [];
let lastEmpty = last(empty); // null
Parameters:
array
- An array
Returns:
- The last element of the array, or
null
if the array is empty
Errors:
- Throws an error if the argument is not an array
rest(array)
Returns a new array containing all elements except the first one.
let numbers = [1, 2, 3, 4, 5];
let tail = rest(numbers); // [2, 3, 4, 5]
let single = [42];
let restSingle = rest(single); // []
let empty = [];
let restEmpty = rest(empty); // null
Parameters:
array
- An array
Returns:
- A new array with all elements except the first, or
null
if the array is empty
Errors:
- Throws an error if the argument is not an array
push(array, element)
Returns a new array with the element added to the end. The original array is not modified.
let numbers = [1, 2, 3];
let extended = push(numbers, 4); // [1, 2, 3, 4]
// numbers is still [1, 2, 3]
let mixed = push([1, "hello"], true); // [1, "hello", true]
Parameters:
array
- An arrayelement
- Any value to add to the array
Returns:
- A new array with the element appended
Errors:
- Throws an error if the first argument is not an array
Output Functions
puts(...args)
Prints the given arguments to standard output, each on a new line.
puts("Hello, World!");
puts(42);
puts(true, "multiple", "arguments");
let name = "Alice";
puts("Hello,", name);
Parameters:
...args
- Any number of arguments of any type
Returns:
null
Notes:
- Each argument is printed on a separate line
- Objects are converted to their string representation
- Always returns
null
Usage Examples
Here are some practical examples of using built-in functions:
Working with Arrays
// Create and manipulate arrays
let numbers = [1, 2, 3, 4, 5];
puts("Array length:", len(numbers));
puts("First element:", first(numbers));
puts("Last element:", last(numbers));
puts("All but first:", rest(numbers));
// Build arrays incrementally
let empty = [];
let withOne = push(empty, 1);
let withTwo = push(withOne, 2);
puts("Built array:", withTwo);
Implementing Higher-Order Functions
// Map function using built-ins
let map = fn(arr, f) {
let iter = fn(arr, accumulated) {
if (len(arr) == 0) {
accumulated;
} else {
iter(rest(arr), push(accumulated, f(first(arr))));
}
};
iter(arr, []);
};
// Filter function using built-ins
let filter = fn(arr, predicate) {
let iter = fn(arr, accumulated) {
if (len(arr) == 0) {
accumulated;
} else {
let head = first(arr);
let tail = rest(arr);
if (predicate(head)) {
iter(tail, push(accumulated, head));
} else {
iter(tail, accumulated);
}
}
};
iter(arr, []);
};
// Usage
let numbers = [1, 2, 3, 4, 5];
let doubled = map(numbers, fn(x) { x * 2 });
let evens = filter(numbers, fn(x) { x % 2 == 0 });
puts("Original:", numbers);
puts("Doubled:", doubled);
puts("Evens:", evens);
String Processing
let processText = fn(text) {
puts("Text:", text);
puts("Length:", len(text));
if (len(text) > 10) {
puts("This is a long text");
} else {
puts("This is a short text");
}
};
processText("Hello");
processText("This is a longer string");
REPL
The Monkey REPL (Read-Eval-Print Loop) provides an interactive environment for experimenting with the Monkey programming language. It's perfect for learning, testing code snippets, and exploring language features.
Starting the REPL
To start the Monkey REPL, run:
cargo run
You'll be greeted with the Monkey ASCII art and a prompt:
__ ___ __
/ |/ /__ ___ / /_____ __ __
/ /|_/ / _ \/ _ \/ '_/ -_) // /
/_/ /_/\___/_//_/_/\_\\__/\_, /
/___/
Welcome to the Monkey programming language!
Feel free to type in commands
>>
Basic Usage
The REPL evaluates Monkey expressions and statements as you type them:
>> 5 + 3
8
>> let x = 10
>> x * 2
20
>> "Hello, " + "World!"
Hello, World!
Features
Persistent Environment
Variables and functions defined in the REPL persist throughout your session:
>> let name = "Alice"
>> let greet = fn(n) { "Hello, " + n + "!" }
>> greet(name)
Hello, Alice!
Command History
The REPL maintains a command history that persists between sessions. You can:
- Use Up/Down arrow keys to navigate through previous commands
- History is saved to
/tmp/.monkey-history.txt
- History is automatically loaded when you start a new REPL session
Line Editing
The REPL supports basic line editing features:
- Left/Right arrows: Move cursor within the current line
- Home/End: Jump to beginning/end of line
- Backspace/Delete: Remove characters
- Ctrl+C: Exit the REPL
- Ctrl+D: Exit the REPL (EOF)
Multi-line Input
You can enter multi-line expressions and function definitions by using the "\" character and pressing Enter. You will then be re-prompted with ".. " to continue entering your input program:
>> let factorial = fn(n) { \
.. if (n < 2) { \
.. 1 \
.. } else { \
.. n * factorial(n - 1) \
.. } \
.. } \
fn(n) {
if (n < 2) { 1 } else { (n * factorial((n - 1))) }
}
>> factorial(5)
120
Example Session
Here's a complete example session showing various Monkey features:
// Define some variables
>> let numbers = [1, 2, 3, 4, 5]
>> let double = fn(x) { x * 2 }
// Use built-in functions
>> len(numbers)
5
>> first(numbers)
1
>> last(numbers)
5
// Higher-order function example
>> let map = fn(arr, f) { \
.. let iter = fn(arr, accumulated) { \
.. if (len(arr) == 0) { \
.. accumulated \
.. } else { \
.. iter(rest(arr), push(accumulated, f(first(arr)))) \
.. } \
.. } \
.. iter(arr, []) \
.. }
fn(arr, f) {
let iter = fn(arr, accumulated) { if (len(arr) == 0) { accumulated } else { iter(rest(arr), push(accumulated, f(first(arr)))) } };iter(arr, [])
}
>> map(numbers, double)
[2, 4, 6, 8, 10]
// Hash map example
>> let person = {"name": "Bob", "age": 30}
>> person["name"]
Bob
// Conditional expressions
>> let status = if (person["age"] >= 18) { "adult" } else { "minor" }
>> status
adult
// Recursive function
>> let fibonacci = fn(n) { \
.. if (n < 2) { \
.. n \
.. } else { \
.. fibonacci(n - 1) + fibonacci(n - 2) \
.. } \
.. }
fn(n) {
if (n < 2) { n } else { (fibonacci((n - 1)) + fibonacci((n - 2))) }
}
>> fibonacci(10)
55
>> puts("Session complete!")
Session complete!
null
Error Handling
The REPL gracefully handles errors and continues running:
>> 5 / 0
division by zero
>> let x = [1, 2, 3]
>> x[10]
null
>> unknownFunction()
identifier not found: unknownFunction
// Can continue running commands
>> 2 + 2
4
Tips for Using the REPL
- Experiment freely: The REPL is perfect for trying out language features
- Build incrementally: Define helper functions and test them step by step
- Use puts() for debugging: Print intermediate values to understand your code
- Save important code: Copy useful functions to files for later use
- Use history: Navigate through previous commands with arrow keys
Exiting the REPL
To exit the REPL, you can:
- Press Ctrl+C
- Press Ctrl+D
- Type the EOF character
The REPL will save your command history and display:
Exiting...
Technical Details
The REPL is implemented using the rustyline
crate, which provides:
- Line editing capabilities
- Command history management
- Cross-platform terminal handling
The REPL maintains a shared environment across all evaluations, allowing for persistent state throughout your session. Each input is parsed into an AST and evaluated using the same interpreter engine that would process Monkey source files.
Functional Programming in Monkey
Monkey is designed with functional programming as a first-class paradigm. Functions are values that can be passed around, stored in variables, and used to build powerful abstractions. This chapter explores the functional programming capabilities of Monkey.
First-Class Functions
In Monkey, functions are first-class citizens, meaning they can be:
- Assigned to variables
- Passed as arguments to other functions
- Returned from functions
- Stored in data structures
// Functions as values
let add = fn(a, b) { a + b };
let subtract = fn(a, b) { a - b };
// Functions in arrays
let operations = [add, subtract];
// Using functions from arrays
let result = operations[0](10, 5); // 15
Higher-Order Functions
Higher-order functions are functions that either take other functions as parameters or return functions as results.
Functions that Take Functions
let applyTwice = fn(f, x) {
f(f(x));
};
let double = fn(x) { x * 2 };
let result = applyTwice(double, 5); // 20 (5 * 2 * 2)
Functions that Return Functions
let makeAdder = fn(x) {
fn(y) { x + y };
};
let add5 = makeAdder(5);
let result = add5(10); // 15
Closures
Monkey functions are closures, meaning they capture and remember the environment in which they were created:
// `newAdder` returns a closure that makes use of the free variables `a` and `b`:
let newAdder = fn(a, b) {
fn(c) { a + b + c };
};
// This constructs a new `adder` function:
let adder = newAdder(1, 2);
adder(8); // => 11
Common Functional Patterns
Map
The map
function applies a transformation to every element in an array:
let map = fn(arr, f) {
let iter = fn(arr, accumulated) {
if (len(arr) == 0) {
accumulated;
} else {
iter(rest(arr), push(accumulated, f(first(arr))));
}
};
iter(arr, []);
};
let numbers = [1, 2, 3, 4];
let double = fn(x) { x * 2 };
let doubled = map(numbers, double); // [2, 4, 6, 8]
Filter
The filter
function selects elements that satisfy a predicate:
let filter = fn(arr, predicate) {
let iter = fn(arr, accumulated) {
if (len(arr) == 0) {
accumulated;
} else {
let head = first(arr);
let tail = rest(arr);
if (predicate(head)) {
iter(tail, push(accumulated, head));
} else {
iter(tail, accumulated);
}
}
};
iter(arr, []);
};
let numbers = [1, 2, 3, 4, 5, 6];
let isMoreThanTwo = fn(x) { x > 2 };
let evens = filter(numbers, isMoreThanTwo); // [3, 4, 5, 6]
Reduce
The reduce
function combines all elements in an array into a single value:
let reduce = fn(arr, initial, f) {
let iter = fn(arr, result) {
if (len(arr) == 0) {
result;
} else {
iter(rest(arr), f(result, first(arr)));
}
};
iter(arr, initial);
};
let sum = fn(arr) {
reduce(arr, 0, fn(acc, x) { acc + x });
};
let product = fn(arr) {
reduce(arr, 1, fn(acc, x) { acc * x });
};
sum([1, 2, 3, 4, 5]); // 15
product([1, 2, 3, 4, 5]); // 120
Function Composition
You can compose functions to create more complex operations:
let compose = fn(f, g) {
fn(x) { f(g(x)) };
};
let add1 = fn(x) { x + 1 };
let multiply2 = fn(x) { x * 2 };
let add1ThenMultiply2 = compose(multiply2, add1);
let result = add1ThenMultiply2(5); // (5 + 1) * 2 = 12
Currying
Currying transforms a function that takes multiple arguments into a series of functions that each take a single argument:
let curry = fn(f) {
fn(a) {
fn(b) {
f(a, b);
};
};
};
let add = fn(a, b) { a + b };
let curriedAdd = curry(add);
let add5 = curriedAdd(5);
let result = add5(10); // 15
Practical Examples
Finding Maximum Element
let max = fn(arr) {
if (len(arr) == 0) {
null;
} else {
reduce(rest(arr), first(arr), fn(acc, x) {
if (x > acc) { x } else { acc }
});
}
};
max([3, 1, 4, 1, 5, 9, 2, 6]); // 9
Functional Fibonacci
let fibonacci = fn(n) {
let fib = fn(a, b, count) {
if (count == 0) {
a;
} else {
fib(b, a + b, count - 1);
}
};
fib(0, 1, n);
};
fibonacci(10); // 55
Pipeline Processing
let pipe = fn(value, functions) {
reduce(functions, value, fn(acc, f) { f(acc) });
};
let numbers = [1, 2, 3, 4, 5];
let pipeline = [
fn(arr) { map(arr, fn(x) { x * 2 }) }, // double each
fn(arr) { filter(arr, fn(x) { x > 5 }) }, // keep > 5
fn(arr) { reduce(arr, 0, fn(a, b) { a + b }) } // sum
];
let result = pipe(numbers, pipeline); // 18
Advantages of Functional Programming in Monkey
- Immutability: Functions don't modify their inputs, reducing side effects
- Composability: Small functions can be combined to create complex behavior
- Reusability: Generic functions like
map
andfilter
work with any data - Testability: Pure functions are easier to test and reason about
- Expressiveness: Functional code often reads like a description of what you want
Recursive Patterns
Monkey's functional style naturally leads to recursive solutions:
// Recursive list processing
let length = fn(arr) {
if (len(arr) == 0) {
0;
} else {
1 + length(rest(arr));
}
};
// Recursive tree traversal (conceptual)
let traverse = fn(tree, visitor) {
if (tree == null) {
null;
} else {
visitor(tree);
traverse(tree["left"], visitor);
traverse(tree["right"], visitor);
}
};
Functional programming in Monkey provides a powerful and elegant way to solve problems by composing simple, reusable functions. The language's support for closures, higher-order functions, and immutable data structures makes it well-suited for functional programming patterns.