Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 binding
  • fn - Function definition
  • if - Conditional statement
  • else - Alternative branch for conditionals
  • return - Return statement
  • true - Boolean true literal
  • false - 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 binding
  • return 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 returns false
  • Equality: true == true returns true
  • Inequality: true != false returns true

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" returns true
  • 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 array
  • element - 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

  1. Experiment freely: The REPL is perfect for trying out language features
  2. Build incrementally: Define helper functions and test them step by step
  3. Use puts() for debugging: Print intermediate values to understand your code
  4. Save important code: Copy useful functions to files for later use
  5. 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

  1. Immutability: Functions don't modify their inputs, reducing side effects
  2. Composability: Small functions can be combined to create complex behavior
  3. Reusability: Generic functions like map and filter work with any data
  4. Testability: Pure functions are easier to test and reason about
  5. 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.