Friday, September 04, 2026

BUILDING A MINIMAL FUNCTIONAL PROGRAMMING LANGUAGE

 



Here comes my unexpected fourth part - even I didn’t expect it - of the article series on programming language design. In this article we‘ll develop a functional language.


INTRODUCTION TO FUNCTIONAL PROGRAMMING LANGUAGES


Functional programming represents a paradigm where computation is treated as the evaluation of mathematical functions, avoiding changing state and mutable data. Building a minimal functional programming language provides deep insights into how programming languages work at their core, from lexical analysis through evaluation. This tutorial guides you through creating a complete, working functional language called "PureFunc" designed specifically for teaching functional programming concepts to students.


The language we will build supports immutable data structures, first-class functions, higher-order functions, closures, pattern matching, recursion, and lazy evaluation. Every value in PureFunc is immutable by design, forcing students to think in terms of transformations rather than mutations. The syntax is deliberately simple and clean, removing unnecessary complexity that might distract from learning core functional concepts.


Our implementation will be written in Python for accessibility, but the concepts translate to any host language. We will build the complete pipeline: lexer, parser, abstract syntax tree, type checker, and evaluator. Each component will be explained thoroughly with running examples that build upon each other.


ARCHITECTURAL OVERVIEW


Before diving into implementation details, we need to understand the architecture of our language interpreter. The process of executing a PureFunc program follows these stages:


First, the lexer (also called tokenizer or scanner) reads the raw source code as a string and breaks it into meaningful tokens. Tokens are the smallest units of meaning in the language, such as keywords, identifiers, numbers, operators, and punctuation. The lexer removes whitespace and comments, producing a stream of tokens.


Second, the parser consumes the token stream and builds an Abstract Syntax Tree (AST). The AST represents the hierarchical structure of the program according to the language's grammar rules. Each node in the tree represents a construct in the language, such as a function definition, function application, or literal value.


Third, the type checker (optional but recommended) traverses the AST to verify that the program is type-safe. It infers types for expressions and ensures that functions are called with arguments of the correct types. This catches many errors before execution.


Fourth, the evaluator walks the AST and computes the result. In our case, we will implement an environment-based interpreter that maintains a mapping from variable names to their values. The evaluator handles function calls, variable lookups, and all other runtime behavior.


DESIGNING THE LANGUAGE SYNTAX


PureFunc uses a clean, minimal syntax inspired by ML-family languages and Lisp. The syntax is designed to be unambiguous and easy to parse while remaining readable for students. Let us examine the core syntactic elements.


Function definitions use the "fn" keyword followed by parameter names and an arrow pointing to the function body. For example, a function that adds two numbers looks like this:


fn x y -> x + y


This defines an anonymous function taking two parameters. To bind this function to a name, we use the "let" keyword:


let add = fn x y -> x + y


Function application uses simple juxtaposition, where the function name is followed by its arguments:


add 3 5


This applies the add function to arguments 3 and 5, producing 8.

Conditional expressions use "if", "then", and "else" keywords:


if x > 0 then x else 0


Lists are fundamental in functional programming. We represent lists using square brackets with elements separated by commas:


[1, 2, 3, 4, 5]


The empty list is simply:


[]


Pattern matching allows destructuring data and is essential for working with lists and other structures. We use the "match" keyword:


match mylist with

  [] -> 0

  [head | tail] -> head + sum tail


This matches an empty list returning 0, or a non-empty list where we bind the first element to "head" and the rest to "tail".


Let bindings create local scopes and can be used for intermediate calculations:


let x = 10 in

let y = 20 in

x + y


Comments begin with a hash symbol and continue to the end of the line:


# This is a comment


IMPLEMENTING THE LEXER

The lexer transforms source code into tokens. Each token has a type and a value. We need to recognize keywords, identifiers, numbers, operators, and punctuation. Let us implement a complete lexer for PureFunc.


We start by defining token types. Each token type represents a category of lexical element:


class TokenType:

    # Keywords

    LET = 'LET'

    IN = 'IN'

    FN = 'FN'

    IF = 'IF'

    THEN = 'THEN'

    ELSE = 'ELSE'

    MATCH = 'MATCH'

    WITH = 'WITH'

    

    # Literals

    NUMBER = 'NUMBER'

    IDENTIFIER = 'IDENTIFIER'

    

    # Operators

    PLUS = 'PLUS'

    MINUS = 'MINUS'

    MULTIPLY = 'MULTIPLY'

    DIVIDE = 'DIVIDE'

    EQUALS = 'EQUALS'

    NOT_EQUALS = 'NOT_EQUALS'

    LESS_THAN = 'LESS_THAN'

    GREATER_THAN = 'GREATER_THAN'

    LESS_EQUAL = 'LESS_EQUAL'

    GREATER_EQUAL = 'GREATER_EQUAL'

    

    # Punctuation

    LPAREN = 'LPAREN'

    RPAREN = 'RPAREN'

    LBRACKET = 'LBRACKET'

    RBRACKET = 'RBRACKET'

    ARROW = 'ARROW'

    PIPE = 'PIPE'

    COMMA = 'COMMA'

    ASSIGN = 'ASSIGN'

    

    # Special

    EOF = 'EOF'



Each token is represented as a simple object containing its type and value:



class Token:

    def __init__(self, token_type, value, line, column):

        self.type = token_type

        self.value = value

        self.line = line

        self.column = column

    

    def __repr__(self):

        return f'Token({self.type}, {self.value}, {self.line}:{self.column})'



The lexer itself maintains the current position in the source code and produces tokens one at a time:



class Lexer:

    def __init__(self, source):

        self.source = source

        self.position = 0

        self.line = 1

        self.column = 1

        self.current_char = self.source[0] if source else None

        

        self.keywords = {

            'let': TokenType.LET,

            'in': TokenType.IN,

            'fn': TokenType.FN,

            'if': TokenType.IF,

            'then': TokenType.THEN,

            'else': TokenType.ELSE,

            'match': TokenType.MATCH,

            'with': TokenType.WITH,

        }

    

    def advance(self):

        """Move to the next character in the source."""

        if self.current_char == '\n':

            self.line += 1

            self.column = 1

        else:

            self.column += 1

        

        self.position += 1

        if self.position >= len(self.source):

            self.current_char = None

        else:

            self.current_char = self.source[self.position]

    

    def peek(self, offset=1):

        """Look ahead at the next character without consuming it."""

        peek_pos = self.position + offset

        if peek_pos >= len(self.source):

            return None

        return self.source[peek_pos]

    

    def skip_whitespace(self):

        """Skip over whitespace characters."""

        while self.current_char is not None and self.current_char.isspace():

            self.advance()

    

    def skip_comment(self):

        """Skip over comments that start with #."""

        while self.current_char is not None and self.current_char != '\n':

            self.advance()

    

    def read_number(self):

        """Read a numeric literal."""

        start_line = self.line

        start_column = self.column

        num_str = ''

        

        while self.current_char is not None and (self.current_char.isdigit() or self.current_char == '.'):

            num_str += self.current_char

            self.advance()

        

        if '.' in num_str:

            return Token(TokenType.NUMBER, float(num_str), start_line, start_column)

        else:

            return Token(TokenType.NUMBER, int(num_str), start_line, start_column)

    

    def read_identifier(self):

        """Read an identifier or keyword."""

        start_line = self.line

        start_column = self.column

        id_str = ''

        

        while self.current_char is not None and (self.current_char.isalnum() or self.current_char == '_'):

            id_str += self.current_char

            self.advance()

        

        token_type = self.keywords.get(id_str, TokenType.IDENTIFIER)

        return Token(token_type, id_str, start_line, start_column)

    

    def get_next_token(self):

        """Get the next token from the source."""

        while self.current_char is not None:

            if self.current_char.isspace():

                self.skip_whitespace()

                continue

            

            if self.current_char == '#':

                self.skip_comment()

                continue

            

            if self.current_char.isdigit():

                return self.read_number()

            

            if self.current_char.isalpha() or self.current_char == '_':

                return self.read_identifier()

            

            # Single character tokens

            line = self.line

            column = self.column

            

            if self.current_char == '+':

                self.advance()

                return Token(TokenType.PLUS, '+', line, column)

            

            if self.current_char == '*':

                self.advance()

                return Token(TokenType.MULTIPLY, '*', line, column)

            

            if self.current_char == '/':

                self.advance()

                return Token(TokenType.DIVIDE, '/', line, column)

            

            if self.current_char == '(':

                self.advance()

                return Token(TokenType.LPAREN, '(', line, column)

            

            if self.current_char == ')':

                self.advance()

                return Token(TokenType.RPAREN, ')', line, column)

            

            if self.current_char == '[':

                self.advance()

                return Token(TokenType.LBRACKET, '[', line, column)

            

            if self.current_char == ']':

                self.advance()

                return Token(TokenType.RBRACKET, ']', line, column)

            

            if self.current_char == ',':

                self.advance()

                return Token(TokenType.COMMA, ',', line, column)

            

            if self.current_char == '|':

                self.advance()

                return Token(TokenType.PIPE, '|', line, column)

            

            # Multi-character tokens

            if self.current_char == '-':

                if self.peek() == '>':

                    self.advance()

                    self.advance()

                    return Token(TokenType.ARROW, '->', line, column)

                else:

                    self.advance()

                    return Token(TokenType.MINUS, '-', line, column)

            

            if self.current_char == '=':

                if self.peek() == '=':

                    self.advance()

                    self.advance()

                    return Token(TokenType.EQUALS, '==', line, column)

                else:

                    self.advance()

                    return Token(TokenType.ASSIGN, '=', line, column)

            

            if self.current_char == '!':

                if self.peek() == '=':

                    self.advance()

                    self.advance()

                    return Token(TokenType.NOT_EQUALS, '!=', line, column)

            

            if self.current_char == '<':

                if self.peek() == '=':

                    self.advance()

                    self.advance()

                    return Token(TokenType.LESS_EQUAL, '<=', line, column)

                else:

                    self.advance()

                    return Token(TokenType.LESS_THAN, '<', line, column)

            

            if self.current_char == '>':

                if self.peek() == '=':

                    self.advance()

                    self.advance()

                    return Token(TokenType.GREATER_EQUAL, '>=', line, column)

                else:

                    self.advance()

                    return Token(TokenType.GREATER_THAN, '>', line, column)

            

            raise SyntaxError(f'Unexpected character: {self.current_char} at {line}:{column}')

        

        return Token(TokenType.EOF, None, self.line, self.column)

    

    def tokenize(self):

        """Tokenize the entire source and return a list of tokens."""

        tokens = []

        while True:

            token = self.get_next_token()

            tokens.append(token)

            if token.type == TokenType.EOF:

                break

        return tokens


This lexer handles all the syntactic elements of PureFunc. It correctly identifies keywords, operators, numbers, and identifiers while skipping whitespace and comments. The lexer maintains line and column information for error reporting.


BUILDING THE ABSTRACT SYNTAX TREE

The Abstract Syntax Tree represents the structure of our program. Each node in the tree corresponds to a language construct. We define classes for each type of AST node.

The base class for all AST nodes provides a common interface:


class ASTNode:

    """Base class for all AST nodes."""

    pass


Literal values such as numbers are the simplest nodes:


class NumberLiteral(ASTNode):

    def __init__(self, value):

        self.value = value

    

    def __repr__(self):

        return f'NumberLiteral({self.value})'


Variables are represented by identifier nodes:


class Variable(ASTNode):

    def __init__(self, name):

        self.name = name

    

    def __repr__(self):

        return f'Variable({self.name})'


Binary operations combine two expressions with an operator:


class BinaryOp(ASTNode):

    def __init__(self, left, operator, right):

        self.left = left

        self.operator = operator

        self.right = right

    

    def __repr__(self):

        return f'BinaryOp({self.left}, {self.operator}, {self.right})'


Function definitions capture parameters and the body expression:


class FunctionDef(ASTNode):

    def __init__(self, parameters, body):

        self.parameters = parameters

        self.body = body

    

    def __repr__(self):

        return f'FunctionDef({self.parameters}, {self.body})'


Function application applies a function to arguments:


class FunctionCall(ASTNode):

    def __init__(self, function, arguments):

        self.function = function

        self.arguments = arguments

    

    def __repr__(self):

        return f'FunctionCall({self.function}, {self.arguments})'


Conditional expressions evaluate one of two branches based on a condition:


class IfExpression(ASTNode):

    def __init__(self, condition, then_branch, else_branch):

        self.condition = condition

        self.then_branch = then_branch

        self.else_branch = else_branch

    

    def __repr__(self):

        return f'IfExpression({self.condition}, {self.then_branch}, {self.else_branch})'


Let bindings introduce local variables:


class LetBinding(ASTNode):

    def __init__(self, name, value, body):

        self.name = name

        self.value = value

        self.body = body

    

    def __repr__(self):

        return f'LetBinding({self.name}, {self.value}, {self.body})'


Lists are fundamental data structures:


class ListLiteral(ASTNode):

    def __init__(self, elements):

        self.elements = elements

    

    def __repr__(self):

        return f'ListLiteral({self.elements})'


Pattern matching enables destructuring:


class MatchExpression(ASTNode):

    def __init__(self, value, cases):

        self.value = value

        self.cases = cases

    

    def __repr__(self):

        return f'MatchExpression({self.value}, {self.cases})'


class MatchCase(ASTNode):

    def __init__(self, pattern, result):

        self.pattern = pattern

        self.result = result

    

    def __repr__(self):

        return f'MatchCase({self.pattern}, {self.result})'


Patterns can be empty lists, variables, or cons patterns:


class EmptyListPattern(ASTNode):

    def __repr__(self):

        return 'EmptyListPattern()'


class VariablePattern(ASTNode):

    def __init__(self, name):

        self.name = name

    

    def __repr__(self):

        return f'VariablePattern({self.name})'


class ConsPattern(ASTNode):

    def __init__(self, head, tail):

        self.head = head

        self.tail = tail

    

    def __repr__(self):

        return f'ConsPattern({self.head}, {self.tail})'


IMPLEMENTING THE PARSER

The parser transforms the token stream into an AST. We use a recursive descent parser, which is straightforward to implement and understand. Each grammar rule becomes a parsing method.


The parser maintains the current position in the token stream and provides methods to consume tokens:


class Parser:

    def __init__(self, tokens):

        self.tokens = tokens

        self.position = 0

        self.current_token = self.tokens[0] if tokens else None

    

    def advance(self):

        """Move to the next token."""

        self.position += 1

        if self.position < len(self.tokens):

            self.current_token = self.tokens[self.position]

        else:

            self.current_token = None

    

    def expect(self, token_type):

        """Consume a token of the expected type or raise an error."""

        if self.current_token is None:

            raise SyntaxError(f'Expected {token_type} but reached end of input')

        if self.current_token.type != token_type:

            raise SyntaxError(f'Expected {token_type} but got {self.current_token.type} at {self.current_token.line}:{self.current_token.column}')

        token = self.current_token

        self.advance()

        return token

    

    def parse(self):

        """Parse the entire program."""

        return self.parse_expression()

    

    def parse_expression(self):

        """Parse an expression (the top-level grammar rule)."""

        if self.current_token.type == TokenType.LET:

            return self.parse_let_binding()

        elif self.current_token.type == TokenType.FN:

            return self.parse_function_def()

        elif self.current_token.type == TokenType.IF:

            return self.parse_if_expression()

        elif self.current_token.type == TokenType.MATCH:

            return self.parse_match_expression()

        else:

            return self.parse_comparison()

    

    def parse_let_binding(self):

        """Parse a let binding: let x = value in body"""

        self.expect(TokenType.LET)

        name_token = self.expect(TokenType.IDENTIFIER)

        self.expect(TokenType.ASSIGN)

        value = self.parse_expression()

        self.expect(TokenType.IN)

        body = self.parse_expression()

        return LetBinding(name_token.value, value, body)

    

    def parse_function_def(self):

        """Parse a function definition: fn x y -> body"""

        self.expect(TokenType.FN)

        parameters = []

        while self.current_token.type == TokenType.IDENTIFIER:

            param_token = self.expect(TokenType.IDENTIFIER)

            parameters.append(param_token.value)

        self.expect(TokenType.ARROW)

        body = self.parse_expression()

        return FunctionDef(parameters, body)

    

    def parse_if_expression(self):

        """Parse an if expression: if cond then expr1 else expr2"""

        self.expect(TokenType.IF)

        condition = self.parse_expression()

        self.expect(TokenType.THEN)

        then_branch = self.parse_expression()

        self.expect(TokenType.ELSE)

        else_branch = self.parse_expression()

        return IfExpression(condition, then_branch, else_branch)

    

    def parse_match_expression(self):

        """Parse a match expression: match value with cases"""

        self.expect(TokenType.MATCH)

        value = self.parse_expression()

        self.expect(TokenType.WITH)

        cases = []

        while self.current_token.type != TokenType.EOF:

            pattern = self.parse_pattern()

            self.expect(TokenType.ARROW)

            result = self.parse_expression()

            cases.append(MatchCase(pattern, result))

            if self.current_token.type not in [TokenType.LBRACKET, TokenType.IDENTIFIER]:

                break

        return MatchExpression(value, cases)

    

    def parse_pattern(self):

        """Parse a pattern for match expressions."""

        if self.current_token.type == TokenType.LBRACKET:

            self.advance()

            if self.current_token.type == TokenType.RBRACKET:

                self.advance()

                return EmptyListPattern()

            else:

                head_token = self.expect(TokenType.IDENTIFIER)

                head = VariablePattern(head_token.value)

                self.expect(TokenType.PIPE)

                tail_token = self.expect(TokenType.IDENTIFIER)

                tail = VariablePattern(tail_token.value)

                self.expect(TokenType.RBRACKET)

                return ConsPattern(head, tail)

        elif self.current_token.type == TokenType.IDENTIFIER:

            name_token = self.expect(TokenType.IDENTIFIER)

            return VariablePattern(name_token.value)

        else:

            raise SyntaxError(f'Invalid pattern at {self.current_token.line}:{self.current_token.column}')

    

    def parse_comparison(self):

        """Parse comparison operations."""

        left = self.parse_additive()

        

        while self.current_token and self.current_token.type in [

            TokenType.EQUALS, TokenType.NOT_EQUALS,

            TokenType.LESS_THAN, TokenType.GREATER_THAN,

            TokenType.LESS_EQUAL, TokenType.GREATER_EQUAL

        ]:

            operator = self.current_token.type

            self.advance()

            right = self.parse_additive()

            left = BinaryOp(left, operator, right)

        

        return left

    

    def parse_additive(self):

        """Parse addition and subtraction."""

        left = self.parse_multiplicative()

        

        while self.current_token and self.current_token.type in [TokenType.PLUS, TokenType.MINUS]:

            operator = self.current_token.type

            self.advance()

            right = self.parse_multiplicative()

            left = BinaryOp(left, operator, right)

        

        return left

    

    def parse_multiplicative(self):

        """Parse multiplication and division."""

        left = self.parse_application()

        

        while self.current_token and self.current_token.type in [TokenType.MULTIPLY, TokenType.DIVIDE]:

            operator = self.current_token.type

            self.advance()

            right = self.parse_application()

            left = BinaryOp(left, operator, right)

        

        return left

    

    def parse_application(self):

        """Parse function application."""

        left = self.parse_primary()

        

        while self.current_token and self.current_token.type in [

            TokenType.NUMBER, TokenType.IDENTIFIER, TokenType.LPAREN, TokenType.LBRACKET

        ]:

            argument = self.parse_primary()

            left = FunctionCall(left, [argument])

        

        return left

    

    def parse_primary(self):

        """Parse primary expressions (literals, variables, parenthesized expressions, lists)."""

        if self.current_token.type == TokenType.NUMBER:

            value = self.current_token.value

            self.advance()

            return NumberLiteral(value)

        

        elif self.current_token.type == TokenType.IDENTIFIER:

            name = self.current_token.value

            self.advance()

            return Variable(name)

        

        elif self.current_token.type == TokenType.LPAREN:

            self.advance()

            expr = self.parse_expression()

            self.expect(TokenType.RPAREN)

            return expr

        

        elif self.current_token.type == TokenType.LBRACKET:

            return self.parse_list()

        

        else:

            raise SyntaxError(f'Unexpected token: {self.current_token.type} at {self.current_token.line}:{self.current_token.column}')

    

    def parse_list(self):

        """Parse a list literal."""

        self.expect(TokenType.LBRACKET)

        elements = []

        

        if self.current_token.type == TokenType.RBRACKET:

            self.advance()

            return ListLiteral(elements)

        

        elements.append(self.parse_expression())

        

        while self.current_token.type == TokenType.COMMA:

            self.advance()

            elements.append(self.parse_expression())

        

        self.expect(TokenType.RBRACKET)

        return ListLiteral(elements)


This parser implements the complete grammar of PureFunc using recursive descent. Each method corresponds to a grammar rule and builds the appropriate AST node. The parser handles operator precedence correctly by having separate methods for different precedence levels.


IMPLEMENTING THE EVALUATOR

The evaluator executes the AST by recursively evaluating each node. We use an environment to track variable bindings. The environment is a dictionary mapping variable names to their values.

First, we define value types that can exist at runtime:


class Value:

    """Base class for runtime values."""

    pass


class NumberValue(Value):

    def __init__(self, value):

        self.value = value

    

    def __repr__(self):

        return f'NumberValue({self.value})'

    

    def __eq__(self, other):

        return isinstance(other, NumberValue) and self.value == other.value


class BoolValue(Value):

    def __init__(self, value):

        self.value = value

    

    def __repr__(self):

        return f'BoolValue({self.value})'

    

    def __eq__(self, other):

        return isinstance(other, BoolValue) and self.value == other.value


class ListValue(Value):

    def __init__(self, elements):

        self.elements = tuple(elements)  # Immutable

    

    def __repr__(self):

        return f'ListValue({list(self.elements)})'

    

    def __eq__(self, other):

        return isinstance(other, ListValue) and self.elements == other.elements


class FunctionValue(Value):

    def __init__(self, parameters, body, closure):

        self.parameters = parameters

        self.body = body

        self.closure = closure  # Captured environment

    

    def __repr__(self):

        return f'FunctionValue({self.parameters}, ...)'


The environment is implemented as an immutable chain of scopes:


class Environment:

    def __init__(self, parent=None):

        self.bindings = {}

        self.parent = parent

    

    def define(self, name, value):

        """Create a new environment with an additional binding."""

        new_env = Environment(self.parent)

        new_env.bindings = self.bindings.copy()

        new_env.bindings[name] = value

        return new_env

    

    def lookup(self, name):

        """Look up a variable in the environment chain."""

        if name in self.bindings:

            return self.bindings[name]

        elif self.parent is not None:

            return self.parent.lookup(name)

        else:

            raise NameError(f'Undefined variable: {name}')

    

    def extend(self, names, values):

        """Create a new environment with multiple bindings."""

        new_env = Environment(self)

        for name, value in zip(names, values):

            new_env.bindings[name] = value

        return new_env


The evaluator recursively evaluates AST nodes:


class Evaluator:

    def __init__(self):

        self.global_env = self.create_global_environment()

    

    def create_global_environment(self):

        """Create the global environment with built-in functions."""

        env = Environment()

        

        # Built-in functions will be added here

        # For now, we start with an empty environment

        

        return env

    

    def evaluate(self, node, env):

        """Evaluate an AST node in the given environment."""

        if isinstance(node, NumberLiteral):

            return NumberValue(node.value)

        

        elif isinstance(node, Variable):

            return env.lookup(node.name)

        

        elif isinstance(node, BinaryOp):

            return self.evaluate_binary_op(node, env)

        

        elif isinstance(node, FunctionDef):

            return FunctionValue(node.parameters, node.body, env)

        

        elif isinstance(node, FunctionCall):

            return self.evaluate_function_call(node, env)

        

        elif isinstance(node, IfExpression):

            return self.evaluate_if_expression(node, env)

        

        elif isinstance(node, LetBinding):

            return self.evaluate_let_binding(node, env)

        

        elif isinstance(node, ListLiteral):

            return self.evaluate_list_literal(node, env)

        

        elif isinstance(node, MatchExpression):

            return self.evaluate_match_expression(node, env)

        

        else:

            raise RuntimeError(f'Unknown AST node type: {type(node)}')

    

    def evaluate_binary_op(self, node, env):

        """Evaluate binary operations."""

        left_val = self.evaluate(node.left, env)

        right_val = self.evaluate(node.right, env)

        

        if node.operator == TokenType.PLUS:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                return NumberValue(left_val.value + right_val.value)

            else:

                raise TypeError('Addition requires numbers')

        

        elif node.operator == TokenType.MINUS:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                return NumberValue(left_val.value - right_val.value)

            else:

                raise TypeError('Subtraction requires numbers')

        

        elif node.operator == TokenType.MULTIPLY:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                return NumberValue(left_val.value * right_val.value)

            else:

                raise TypeError('Multiplication requires numbers')

        

        elif node.operator == TokenType.DIVIDE:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                if right_val.value == 0:

                    raise ZeroDivisionError('Division by zero')

                return NumberValue(left_val.value / right_val.value)

            else:

                raise TypeError('Division requires numbers')

        

        elif node.operator == TokenType.EQUALS:

            return BoolValue(left_val == right_val)

        

        elif node.operator == TokenType.NOT_EQUALS:

            return BoolValue(left_val != right_val)

        

        elif node.operator == TokenType.LESS_THAN:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                return BoolValue(left_val.value < right_val.value)

            else:

                raise TypeError('Comparison requires numbers')

        

        elif node.operator == TokenType.GREATER_THAN:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                return BoolValue(left_val.value > right_val.value)

            else:

                raise TypeError('Comparison requires numbers')

        

        elif node.operator == TokenType.LESS_EQUAL:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                return BoolValue(left_val.value <= right_val.value)

            else:

                raise TypeError('Comparison requires numbers')

        

        elif node.operator == TokenType.GREATER_EQUAL:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                return BoolValue(left_val.value >= right_val.value)

            else:

                raise TypeError('Comparison requires numbers')

        

        else:

            raise RuntimeError(f'Unknown operator: {node.operator}')

    

    def evaluate_function_call(self, node, env):

        """Evaluate function application."""

        func_val = self.evaluate(node.function, env)

        

        if not isinstance(func_val, FunctionValue):

            raise TypeError(f'Cannot call non-function value: {func_val}')

        

        arg_vals = [self.evaluate(arg, env) for arg in node.arguments]

        

        # Handle currying: if we have fewer arguments than parameters,

        # return a new function that expects the remaining arguments

        if len(arg_vals) < len(func_val.parameters):

            bound_params = func_val.parameters[:len(arg_vals)]

            remaining_params = func_val.parameters[len(arg_vals):]

            new_env = func_val.closure.extend(bound_params, arg_vals)

            return FunctionValue(remaining_params, func_val.body, new_env)

        

        # If we have exactly the right number of arguments, evaluate the body

        elif len(arg_vals) == len(func_val.parameters):

            new_env = func_val.closure.extend(func_val.parameters, arg_vals)

            return self.evaluate(func_val.body, new_env)

        

        # If we have too many arguments, apply the function and then

        # apply the result to the remaining arguments

        else:

            first_args = arg_vals[:len(func_val.parameters)]

            remaining_args = arg_vals[len(func_val.parameters):]

            new_env = func_val.closure.extend(func_val.parameters, first_args)

            result = self.evaluate(func_val.body, new_env)

            

            for arg in remaining_args:

                if not isinstance(result, FunctionValue):

                    raise TypeError('Too many arguments to function')

                new_env = result.closure.extend(result.parameters, [arg])

                result = self.evaluate(result.body, new_env)

            

            return result

    

    def evaluate_if_expression(self, node, env):

        """Evaluate conditional expressions."""

        condition_val = self.evaluate(node.condition, env)

        

        if not isinstance(condition_val, BoolValue):

            raise TypeError('Condition must be a boolean')

        

        if condition_val.value:

            return self.evaluate(node.then_branch, env)

        else:

            return self.evaluate(node.else_branch, env)

    

    def evaluate_let_binding(self, node, env):

        """Evaluate let bindings."""

        value = self.evaluate(node.value, env)

        new_env = env.define(node.name, value)

        return self.evaluate(node.body, new_env)

    

    def evaluate_list_literal(self, node, env):

        """Evaluate list literals."""

        element_vals = [self.evaluate(elem, env) for elem in node.elements]

        return ListValue(element_vals)

    

    def evaluate_match_expression(self, node, env):

        """Evaluate match expressions."""

        value = self.evaluate(node.value, env)

        

        for case in node.cases:

            match_env = self.try_match_pattern(case.pattern, value, env)

            if match_env is not None:

                return self.evaluate(case.result, match_env)

        

        raise RuntimeError('No matching pattern found')

    

    def try_match_pattern(self, pattern, value, env):

        """Try to match a pattern against a value, returning a new environment if successful."""

        if isinstance(pattern, EmptyListPattern):

            if isinstance(value, ListValue) and len(value.elements) == 0:

                return env

            else:

                return None

        

        elif isinstance(pattern, VariablePattern):

            return env.define(pattern.name, value)

        

        elif isinstance(pattern, ConsPattern):

            if isinstance(value, ListValue) and len(value.elements) > 0:

                head = value.elements[0]

                tail = ListValue(value.elements[1:])

                

                env = self.try_match_pattern(pattern.head, head, env)

                if env is None:

                    return None

                

                env = self.try_match_pattern(pattern.tail, tail, env)

                return env

            else:

                return None

        

        else:

            raise RuntimeError(f'Unknown pattern type: {type(pattern)}')


This evaluator handles all the core features of PureFunc. It correctly implements closures by capturing the environment when a function is created. It supports currying by allowing partial application of functions. Pattern matching is implemented by trying each case in order until one matches.


ADDING BUILT-IN FUNCTIONS

To make PureFunc practical, we need to provide some built-in functions. These are functions implemented in the host language (Python) that are available in the global environment.


We extend the create_global_environment method to add built-in functions:


def create_global_environment(self):

    """Create the global environment with built-in functions."""

    env = Environment()

    

    # print function

    def builtin_print(arg):

        print(self.value_to_string(arg))

        return arg

    

    env.bindings['print'] = self.create_builtin_function('print', 1, builtin_print)

    

    # head function (get first element of list)

    def builtin_head(lst):

        if not isinstance(lst, ListValue):

            raise TypeError('head requires a list')

        if len(lst.elements) == 0:

            raise RuntimeError('head of empty list')

        return lst.elements[0]

    

    env.bindings['head'] = self.create_builtin_function('head', 1, builtin_head)

    

    # tail function (get all but first element)

    def builtin_tail(lst):

        if not isinstance(lst, ListValue):

            raise TypeError('tail requires a list')

        if len(lst.elements) == 0:

            raise RuntimeError('tail of empty list')

        return ListValue(lst.elements[1:])

    

    env.bindings['tail'] = self.create_builtin_function('tail', 1, builtin_tail)

    

    # cons function (prepend element to list)

    def builtin_cons(elem, lst):

        if not isinstance(lst, ListValue):

            raise TypeError('cons requires a list as second argument')

        return ListValue([elem] + list(lst.elements))

    

    env.bindings['cons'] = self.create_builtin_function('cons', 2, builtin_cons)

    

    # length function

    def builtin_length(lst):

        if not isinstance(lst, ListValue):

            raise TypeError('length requires a list')

        return NumberValue(len(lst.elements))

    

    env.bindings['length'] = self.create_builtin_function('length', 1, builtin_length)

    

    # isEmpty function

    def builtin_is_empty(lst):

        if not isinstance(lst, ListValue):

            raise TypeError('isEmpty requires a list')

        return BoolValue(len(lst.elements) == 0)

    

    env.bindings['isEmpty'] = self.create_builtin_function('isEmpty', 1, builtin_is_empty)

    

    return env


def create_builtin_function(self, name, arity, implementation):

    """Create a built-in function value."""

    class BuiltinFunction(FunctionValue):

        def __init__(self, name, arity, impl):

            self.name = name

            self.arity = arity

            self.implementation = impl

            self.parameters = [f'arg{i}' for i in range(arity)]

            self.body = None

            self.closure = None

        

        def __repr__(self):

            return f'BuiltinFunction({self.name})'

    

    return BuiltinFunction(name, arity, implementation)


def evaluate_function_call(self, node, env):

    """Evaluate function application (updated to handle built-ins)."""

    func_val = self.evaluate(node.function, env)

    

    if not isinstance(func_val, FunctionValue):

        raise TypeError(f'Cannot call non-function value: {func_val}')

    

    arg_vals = [self.evaluate(arg, env) for arg in node.arguments]

    

    # Handle built-in functions

    if hasattr(func_val, 'implementation') and func_val.implementation is not None:

        if len(arg_vals) < func_val.arity:

            # Partial application of built-in

            bound_args = arg_vals

            remaining_arity = func_val.arity - len(arg_vals)

            

            def partial_impl(*args):

                all_args = bound_args + list(args)

                return func_val.implementation(*all_args)

            

            return self.create_builtin_function(

                f'{func_val.name}_partial',

                remaining_arity,

                partial_impl

            )

        elif len(arg_vals) == func_val.arity:

            return func_val.implementation(*arg_vals)

        else:

            # Too many arguments

            result = func_val.implementation(*arg_vals[:func_val.arity])

            remaining_args = arg_vals[func_val.arity:]

            

            for arg in remaining_args:

                if not isinstance(result, FunctionValue):

                    raise TypeError('Too many arguments to function')

                # Recursively apply

                result = self.evaluate_function_call(

                    FunctionCall(Variable('_temp'), [Variable('_arg')]),

                    env.define('_temp', result).define('_arg', arg)

                )

            

            return result

    

    # Handle user-defined functions (same as before)

    if len(arg_vals) < len(func_val.parameters):

        bound_params = func_val.parameters[:len(arg_vals)]

        remaining_params = func_val.parameters[len(arg_vals):]

        new_env = func_val.closure.extend(bound_params, arg_vals)

        return FunctionValue(remaining_params, func_val.body, new_env)

    

    elif len(arg_vals) == len(func_val.parameters):

        new_env = func_val.closure.extend(func_val.parameters, arg_vals)

        return self.evaluate(func_val.body, new_env)

    

    else:

        first_args = arg_vals[:len(func_val.parameters)]

        remaining_args = arg_vals[len(func_val.parameters):]

        new_env = func_val.closure.extend(func_val.parameters, first_args)

        result = self.evaluate(func_val.body, new_env)

        

        for arg in remaining_args:

            if not isinstance(result, FunctionValue):

                raise TypeError('Too many arguments to function')

            new_env = result.closure.extend(result.parameters, [arg])

            result = self.evaluate(result.body, new_env)

        

        return result


def value_to_string(self, value):

    """Convert a value to a string for printing."""

    if isinstance(value, NumberValue):

        return str(value.value)

    elif isinstance(value, BoolValue):

        return 'true' if value.value else 'false'

    elif isinstance(value, ListValue):

        elements = [self.value_to_string(elem) for elem in value.elements]

        return '[' + ', '.join(elements) + ']'

    elif isinstance(value, FunctionValue):

        return '<function>'

    else:

        return str(value)


These built-in functions provide essential list operations that students will use frequently. They are implemented efficiently in Python but appear as normal functions in PureFunc.


CREATING A REPL

A Read-Eval-Print Loop (REPL) allows interactive exploration of the language. Users can type expressions and immediately see the results. This is invaluable for learning and experimentation.


The REPL repeatedly reads input, evaluates it, and prints the result:


class REPL:

    def __init__(self):

        self.evaluator = Evaluator()

        self.environment = self.evaluator.global_env

    

    def run(self):

        """Run the interactive REPL."""

        print('PureFunc REPL v1.0')

        print('Type expressions to evaluate them. Use Ctrl+C to exit.')

        print()

        

        while True:

            try:

                # Read

                source = input('> ')

                if not source.strip():

                    continue

                

                # Lex

                lexer = Lexer(source)

                tokens = lexer.tokenize()

                

                # Parse

                parser = Parser(tokens)

                ast = parser.parse()

                

                # Evaluate

                result = self.evaluator.evaluate(ast, self.environment)

                

                # Print

                print(self.evaluator.value_to_string(result))

                print()

                

            except KeyboardInterrupt:

                print('\nGoodbye!')

                break

            except EOFError:

                print('\nGoodbye!')

                break

            except Exception as e:

                print(f'Error: {e}')

                print()

    

    def run_file(self, filename):

        """Execute a PureFunc source file."""

        try:

            with open(filename, 'r') as f:

                source = f.read()

            

            lexer = Lexer(source)

            tokens = lexer.tokenize()

            

            parser = Parser(tokens)

            ast = parser.parse()

            

            result = self.evaluator.evaluate(ast, self.environment)

            print(self.evaluator.value_to_string(result))

            

        except FileNotFoundError:

            print(f'Error: File not found: {filename}')

        except Exception as e:

            print(f'Error: {e}')


The REPL provides a friendly interface for experimenting with PureFunc. It catches errors gracefully and allows the user to continue working after mistakes.


EXAMPLE PROGRAMS IN PUREFUNC


Let us explore some example programs to demonstrate the capabilities of PureFunc. These examples illustrate functional programming concepts that students will learn.


Computing the factorial of a number using recursion:


let factorial = fn n ->

  if n == 0

  then 1

  else n * factorial (n - 1)

in

factorial 5


This defines a recursive function that computes factorial. The function calls itself with a smaller argument until reaching the base case.


Mapping a function over a list:


let map = fn f list ->

  match list with

    [] -> []

    [head | tail] -> cons (f head) (map f tail)

in

let double = fn x -> x * 2 in

map double [1, 2, 3, 4, 5]


The map function applies a function to each element of a list, producing a new list. This demonstrates higher-order functions and pattern matching.


Filtering a list based on a predicate:


let filter = fn pred list ->

  match list with

    [] -> []

    [head | tail] ->

      if pred head

      then cons head (filter pred tail)

      else filter pred tail

in

let isPositive = fn x -> x > 0 in

filter isPositive [-2, -1, 0, 1, 2]


The filter function keeps only elements that satisfy a predicate. This shows how functions can be passed as arguments.


Folding a list (reduce operation):


let foldl = fn f acc list ->

  match list with

    [] -> acc

    [head | tail] -> foldl f (f acc head) tail

in

let sum = foldl (fn a b -> a + b) 0 in

sum [1, 2, 3, 4, 5]


The foldl function processes a list from left to right, accumulating a result. This is a fundamental operation in functional programming.


Composing functions:


let compose = fn f g -> fn x -> f (g x) in

let add1 = fn x -> x + 1 in

let double = fn x -> x * 2 in

let add1ThenDouble = compose double add1 in

add1ThenDouble 5


Function composition creates a new function by chaining two functions together. The result of the second function becomes the input to the first.


Computing Fibonacci numbers:


let fib = fn n ->

  if n <= 1

  then n

  else fib (n - 1) + fib (n - 2)

in

fib 10


This classic recursive function demonstrates how PureFunc handles multiple recursive calls.


Finding the length of a list using pattern matching:


let length = fn list ->

  match list with

    [] -> 0

    [head | tail] -> 1 + length tail

in

length [1, 2, 3, 4, 5]


This shows how pattern matching naturally expresses recursive operations on lists.


ADVANCED FEATURES AND OPTIMIZATIONS

While our implementation is complete and functional, there are several advanced features and optimizations that could be added to make PureFunc more powerful and efficient.

Tail call optimization is crucial for functional languages that rely heavily on recursion. Without it, deep recursion can cause stack overflow. We could modify the evaluator to recognize tail calls and convert them to loops.


Lazy evaluation allows expressions to be evaluated only when their values are needed. This enables infinite data structures and can improve performance by avoiding unnecessary computation. We would need to wrap expressions in thunks that delay evaluation.


Type inference using the Hindley-Milner algorithm would allow PureFunc to catch type errors before runtime while still maintaining a clean syntax without explicit type annotations. This requires implementing unification and constraint solving.

Algebraic data types would allow users to define their own data structures with pattern matching. For example, defining a binary tree type and writing functions that operate on trees.


Module system would allow organizing code into separate files and namespaces. This is essential for larger programs and code reuse.

Garbage collection is already handled by Python's runtime, but if we were implementing PureFunc in a lower-level language, we would need to implement reference counting or tracing garbage collection.


Compilation to bytecode or native code would significantly improve performance. Instead of walking the AST, we could compile to an intermediate representation that is faster to execute.


TEACHING FUNCTIONAL PROGRAMMING WITH PUREFUNC

PureFunc is designed specifically for teaching functional programming concepts. The language's simplicity and purity make it ideal for students learning these ideas for the first time.


Immutability is enforced throughout the language. All values are immutable, which eliminates entire classes of bugs related to shared mutable state. Students learn to think in terms of transformations rather than mutations.


First-class functions mean that functions can be passed as arguments, returned from other functions, and stored in data structures. This is fundamental to functional programming and enables powerful abstractions.


Pattern matching provides a clear and concise way to work with data structures. Students learn to think about data in terms of its shape and structure rather than imperative operations.


Recursion is the primary looping mechanism in PureFunc. Students learn to solve problems recursively, which often leads to more elegant solutions than iterative approaches.


Higher-order functions like map, filter, and fold are essential tools in functional programming. Students learn to compose these building blocks to solve complex problems.


Closures capture variables from their surrounding scope, enabling powerful programming techniques like currying and partial application.

The REPL provides immediate feedback, allowing students to experiment and learn interactively. They can try small examples and see the results immediately.


COMPLETE RUNNING EXAMPLE

Below is the complete, production-ready implementation of PureFunc with all components integrated. This code can be run directly and includes all the features discussed in this tutorial.


#!/usr/bin/env python3

"""

PureFunc: A Minimal Functional Programming Language


This is a complete implementation of a functional programming language

designed for teaching functional programming concepts. All data types

are immutable, and the language supports first-class functions,

pattern matching, recursion, and higher-order functions.

"""


import sys

from typing import List, Optional, Any, Dict, Tuple



# ============================================================================

# TOKEN DEFINITIONS

# ============================================================================


class TokenType:

    """Enumeration of all token types in PureFunc."""

    # Keywords

    LET = 'LET'

    IN = 'IN'

    FN = 'FN'

    IF = 'IF'

    THEN = 'THEN'

    ELSE = 'ELSE'

    MATCH = 'MATCH'

    WITH = 'WITH'

    TRUE = 'TRUE'

    FALSE = 'FALSE'

    

    # Literals

    NUMBER = 'NUMBER'

    IDENTIFIER = 'IDENTIFIER'

    

    # Operators

    PLUS = 'PLUS'

    MINUS = 'MINUS'

    MULTIPLY = 'MULTIPLY'

    DIVIDE = 'DIVIDE'

    MODULO = 'MODULO'

    EQUALS = 'EQUALS'

    NOT_EQUALS = 'NOT_EQUALS'

    LESS_THAN = 'LESS_THAN'

    GREATER_THAN = 'GREATER_THAN'

    LESS_EQUAL = 'LESS_EQUAL'

    GREATER_EQUAL = 'GREATER_EQUAL'

    AND = 'AND'

    OR = 'OR'

    NOT = 'NOT'

    

    # Punctuation

    LPAREN = 'LPAREN'

    RPAREN = 'RPAREN'

    LBRACKET = 'LBRACKET'

    RBRACKET = 'RBRACKET'

    ARROW = 'ARROW'

    PIPE = 'PIPE'

    COMMA = 'COMMA'

    ASSIGN = 'ASSIGN'

    

    # Special

    EOF = 'EOF'



class Token:

    """Represents a single token in the source code."""

    

    def __init__(self, token_type: str, value: Any, line: int, column: int):

        self.type = token_type

        self.value = value

        self.line = line

        self.column = column

    

    def __repr__(self):

        return f'Token({self.type}, {self.value}, {self.line}:{self.column})'



# ============================================================================

# LEXER

# ============================================================================


class Lexer:

    """

    Lexical analyzer for PureFunc.

    Converts source code into a stream of tokens.

    """

    

    def __init__(self, source: str):

        self.source = source

        self.position = 0

        self.line = 1

        self.column = 1

        self.current_char = self.source[0] if source else None

        

        self.keywords = {

            'let': TokenType.LET,

            'in': TokenType.IN,

            'fn': TokenType.FN,

            'if': TokenType.IF,

            'then': TokenType.THEN,

            'else': TokenType.ELSE,

            'match': TokenType.MATCH,

            'with': TokenType.WITH,

            'true': TokenType.TRUE,

            'false': TokenType.FALSE,

            'and': TokenType.AND,

            'or': TokenType.OR,

            'not': TokenType.NOT,

            'mod': TokenType.MODULO,

        }

    

    def error(self, message: str):

        """Raise a lexer error with position information."""

        raise SyntaxError(f'{message} at line {self.line}, column {self.column}')

    

    def advance(self):

        """Move to the next character in the source."""

        if self.current_char == '\n':

            self.line += 1

            self.column = 1

        else:

            self.column += 1

        

        self.position += 1

        if self.position >= len(self.source):

            self.current_char = None

        else:

            self.current_char = self.source[self.position]

    

    def peek(self, offset: int = 1) -> Optional[str]:

        """Look ahead at the next character without consuming it."""

        peek_pos = self.position + offset

        if peek_pos >= len(self.source):

            return None

        return self.source[peek_pos]

    

    def skip_whitespace(self):

        """Skip over whitespace characters."""

        while self.current_char is not None and self.current_char.isspace():

            self.advance()

    

    def skip_comment(self):

        """Skip over comments that start with #."""

        while self.current_char is not None and self.current_char != '\n':

            self.advance()

    

    def read_number(self) -> Token:

        """Read a numeric literal (integer or float)."""

        start_line = self.line

        start_column = self.column

        num_str = ''

        has_decimal = False

        

        while self.current_char is not None and (self.current_char.isdigit() or self.current_char == '.'):

            if self.current_char == '.':

                if has_decimal:

                    self.error('Invalid number: multiple decimal points')

                has_decimal = True

            num_str += self.current_char

            self.advance()

        

        if has_decimal:

            return Token(TokenType.NUMBER, float(num_str), start_line, start_column)

        else:

            return Token(TokenType.NUMBER, int(num_str), start_line, start_column)

    

    def read_identifier(self) -> Token:

        """Read an identifier or keyword."""

        start_line = self.line

        start_column = self.column

        id_str = ''

        

        while self.current_char is not None and (self.current_char.isalnum() or self.current_char == '_'):

            id_str += self.current_char

            self.advance()

        

        token_type = self.keywords.get(id_str, TokenType.IDENTIFIER)

        return Token(token_type, id_str, start_line, start_column)

    

    def get_next_token(self) -> Token:

        """Get the next token from the source."""

        while self.current_char is not None:

            if self.current_char.isspace():

                self.skip_whitespace()

                continue

            

            if self.current_char == '#':

                self.skip_comment()

                continue

            

            if self.current_char.isdigit():

                return self.read_number()

            

            if self.current_char.isalpha() or self.current_char == '_':

                return self.read_identifier()

            

            # Single and multi-character operators

            line = self.line

            column = self.column

            

            if self.current_char == '+':

                self.advance()

                return Token(TokenType.PLUS, '+', line, column)

            

            if self.current_char == '*':

                self.advance()

                return Token(TokenType.MULTIPLY, '*', line, column)

            

            if self.current_char == '/':

                self.advance()

                return Token(TokenType.DIVIDE, '/', line, column)

            

            if self.current_char == '(':

                self.advance()

                return Token(TokenType.LPAREN, '(', line, column)

            

            if self.current_char == ')':

                self.advance()

                return Token(TokenType.RPAREN, ')', line, column)

            

            if self.current_char == '[':

                self.advance()

                return Token(TokenType.LBRACKET, '[', line, column)

            

            if self.current_char == ']':

                self.advance()

                return Token(TokenType.RBRACKET, ']', line, column)

            

            if self.current_char == ',':

                self.advance()

                return Token(TokenType.COMMA, ',', line, column)

            

            if self.current_char == '|':

                self.advance()

                return Token(TokenType.PIPE, '|', line, column)

            

            if self.current_char == '-':

                if self.peek() == '>':

                    self.advance()

                    self.advance()

                    return Token(TokenType.ARROW, '->', line, column)

                else:

                    self.advance()

                    return Token(TokenType.MINUS, '-', line, column)

            

            if self.current_char == '=':

                if self.peek() == '=':

                    self.advance()

                    self.advance()

                    return Token(TokenType.EQUALS, '==', line, column)

                else:

                    self.advance()

                    return Token(TokenType.ASSIGN, '=', line, column)

            

            if self.current_char == '!':

                if self.peek() == '=':

                    self.advance()

                    self.advance()

                    return Token(TokenType.NOT_EQUALS, '!=', line, column)

                else:

                    self.error(f'Unexpected character: {self.current_char}')

            

            if self.current_char == '<':

                if self.peek() == '=':

                    self.advance()

                    self.advance()

                    return Token(TokenType.LESS_EQUAL, '<=', line, column)

                else:

                    self.advance()

                    return Token(TokenType.LESS_THAN, '<', line, column)

            

            if self.current_char == '>':

                if self.peek() == '=':

                    self.advance()

                    self.advance()

                    return Token(TokenType.GREATER_EQUAL, '>=', line, column)

                else:

                    self.advance()

                    return Token(TokenType.GREATER_THAN, '>', line, column)

            

            self.error(f'Unexpected character: {self.current_char}')

        

        return Token(TokenType.EOF, None, self.line, self.column)

    

    def tokenize(self) -> List[Token]:

        """Tokenize the entire source and return a list of tokens."""

        tokens = []

        while True:

            token = self.get_next_token()

            tokens.append(token)

            if token.type == TokenType.EOF:

                break

        return tokens



# ============================================================================

# ABSTRACT SYNTAX TREE NODES

# ============================================================================


class ASTNode:

    """Base class for all AST nodes."""

    pass



class NumberLiteral(ASTNode):

    """Represents a numeric literal."""

    

    def __init__(self, value: float):

        self.value = value

    

    def __repr__(self):

        return f'NumberLiteral({self.value})'



class BoolLiteral(ASTNode):

    """Represents a boolean literal."""

    

    def __init__(self, value: bool):

        self.value = value

    

    def __repr__(self):

        return f'BoolLiteral({self.value})'



class Variable(ASTNode):

    """Represents a variable reference."""

    

    def __init__(self, name: str):

        self.name = name

    

    def __repr__(self):

        return f'Variable({self.name})'



class BinaryOp(ASTNode):

    """Represents a binary operation."""

    

    def __init__(self, left: ASTNode, operator: str, right: ASTNode):

        self.left = left

        self.operator = operator

        self.right = right

    

    def __repr__(self):

        return f'BinaryOp({self.left}, {self.operator}, {self.right})'



class UnaryOp(ASTNode):

    """Represents a unary operation."""

    

    def __init__(self, operator: str, operand: ASTNode):

        self.operator = operator

        self.operand = operand

    

    def __repr__(self):

        return f'UnaryOp({self.operator}, {self.operand})'



class FunctionDef(ASTNode):

    """Represents a function definition."""

    

    def __init__(self, parameters: List[str], body: ASTNode):

        self.parameters = parameters

        self.body = body

    

    def __repr__(self):

        return f'FunctionDef({self.parameters}, {self.body})'



class FunctionCall(ASTNode):

    """Represents a function call."""

    

    def __init__(self, function: ASTNode, arguments: List[ASTNode]):

        self.function = function

        self.arguments = arguments

    

    def __repr__(self):

        return f'FunctionCall({self.function}, {self.arguments})'



class IfExpression(ASTNode):

    """Represents a conditional expression."""

    

    def __init__(self, condition: ASTNode, then_branch: ASTNode, else_branch: ASTNode):

        self.condition = condition

        self.then_branch = then_branch

        self.else_branch = else_branch

    

    def __repr__(self):

        return f'IfExpression({self.condition}, {self.then_branch}, {self.else_branch})'



class LetBinding(ASTNode):

    """Represents a let binding."""

    

    def __init__(self, name: str, value: ASTNode, body: ASTNode):

        self.name = name

        self.value = value

        self.body = body

    

    def __repr__(self):

        return f'LetBinding({self.name}, {self.value}, {self.body})'



class ListLiteral(ASTNode):

    """Represents a list literal."""

    

    def __init__(self, elements: List[ASTNode]):

        self.elements = elements

    

    def __repr__(self):

        return f'ListLiteral({self.elements})'



class MatchExpression(ASTNode):

    """Represents a pattern match expression."""

    

    def __init__(self, value: ASTNode, cases: List['MatchCase']):

        self.value = value

        self.cases = cases

    

    def __repr__(self):

        return f'MatchExpression({self.value}, {self.cases})'



class MatchCase(ASTNode):

    """Represents a single case in a match expression."""

    

    def __init__(self, pattern: 'Pattern', result: ASTNode):

        self.pattern = pattern

        self.result = result

    

    def __repr__(self):

        return f'MatchCase({self.pattern}, {self.result})'



# Pattern types

class Pattern(ASTNode):

    """Base class for patterns."""

    pass



class EmptyListPattern(Pattern):

    """Matches an empty list."""

    

    def __repr__(self):

        return 'EmptyListPattern()'



class VariablePattern(Pattern):

    """Matches any value and binds it to a variable."""

    

    def __init__(self, name: str):

        self.name = name

    

    def __repr__(self):

        return f'VariablePattern({self.name})'



class ConsPattern(Pattern):

    """Matches a non-empty list, binding head and tail."""

    

    def __init__(self, head: Pattern, tail: Pattern):

        self.head = head

        self.tail = tail

    

    def __repr__(self):

        return f'ConsPattern({self.head}, {self.tail})'



class LiteralPattern(Pattern):

    """Matches a specific literal value."""

    

    def __init__(self, value: Any):

        self.value = value

    

    def __repr__(self):

        return f'LiteralPattern({self.value})'



# ============================================================================

# PARSER

# ============================================================================


class Parser:

    """

    Recursive descent parser for PureFunc.

    Converts a stream of tokens into an Abstract Syntax Tree.

    """

    

    def __init__(self, tokens: List[Token]):

        self.tokens = tokens

        self.position = 0

        self.current_token = self.tokens[0] if tokens else None

    

    def error(self, message: str):

        """Raise a parser error with position information."""

        if self.current_token:

            raise SyntaxError(f'{message} at line {self.current_token.line}, column {self.current_token.column}')

        else:

            raise SyntaxError(f'{message} at end of input')

    

    def advance(self):

        """Move to the next token."""

        self.position += 1

        if self.position < len(self.tokens):

            self.current_token = self.tokens[self.position]

        else:

            self.current_token = None

    

    def expect(self, token_type: str) -> Token:

        """Consume a token of the expected type or raise an error."""

        if self.current_token is None:

            self.error(f'Expected {token_type} but reached end of input')

        if self.current_token.type != token_type:

            self.error(f'Expected {token_type} but got {self.current_token.type}')

        token = self.current_token

        self.advance()

        return token

    

    def parse(self) -> ASTNode:

        """Parse the entire program."""

        result = self.parse_expression()

        if self.current_token.type != TokenType.EOF:

            self.error('Unexpected tokens after expression')

        return result

    

    def parse_expression(self) -> ASTNode:

        """Parse an expression (the top-level grammar rule)."""

        if self.current_token.type == TokenType.LET:

            return self.parse_let_binding()

        elif self.current_token.type == TokenType.FN:

            return self.parse_function_def()

        elif self.current_token.type == TokenType.IF:

            return self.parse_if_expression()

        elif self.current_token.type == TokenType.MATCH:

            return self.parse_match_expression()

        else:

            return self.parse_logical_or()

    

    def parse_let_binding(self) -> LetBinding:

        """Parse a let binding: let x = value in body"""

        self.expect(TokenType.LET)

        name_token = self.expect(TokenType.IDENTIFIER)

        self.expect(TokenType.ASSIGN)

        value = self.parse_expression()

        self.expect(TokenType.IN)

        body = self.parse_expression()

        return LetBinding(name_token.value, value, body)

    

    def parse_function_def(self) -> FunctionDef:

        """Parse a function definition: fn x y -> body"""

        self.expect(TokenType.FN)

        parameters = []

        while self.current_token and self.current_token.type == TokenType.IDENTIFIER:

            param_token = self.expect(TokenType.IDENTIFIER)

            parameters.append(param_token.value)

        

        if not parameters:

            self.error('Function must have at least one parameter')

        

        self.expect(TokenType.ARROW)

        body = self.parse_expression()

        return FunctionDef(parameters, body)

    

    def parse_if_expression(self) -> IfExpression:

        """Parse an if expression: if cond then expr1 else expr2"""

        self.expect(TokenType.IF)

        condition = self.parse_expression()

        self.expect(TokenType.THEN)

        then_branch = self.parse_expression()

        self.expect(TokenType.ELSE)

        else_branch = self.parse_expression()

        return IfExpression(condition, then_branch, else_branch)

    

    def parse_match_expression(self) -> MatchExpression:

        """Parse a match expression: match value with cases"""

        self.expect(TokenType.MATCH)

        value = self.parse_expression()

        self.expect(TokenType.WITH)

        cases = []

        

        # Parse at least one case

        pattern = self.parse_pattern()

        self.expect(TokenType.ARROW)

        result = self.parse_expression()

        cases.append(MatchCase(pattern, result))

        

        # Parse additional cases (optional)

        while self.current_token and self.current_token.type in [TokenType.LBRACKET, TokenType.IDENTIFIER, TokenType.NUMBER, TokenType.TRUE, TokenType.FALSE]:

            # Check if this looks like a pattern

            if self.current_token.type == TokenType.PIPE:

                break

            if self.current_token.type in [TokenType.IN, TokenType.THEN, TokenType.ELSE, TokenType.COMMA, TokenType.RPAREN, TokenType.RBRACKET]:

                break

            

            pattern = self.parse_pattern()

            self.expect(TokenType.ARROW)

            result = self.parse_expression()

            cases.append(MatchCase(pattern, result))

        

        return MatchExpression(value, cases)

    

    def parse_pattern(self) -> Pattern:

        """Parse a pattern for match expressions."""

        if self.current_token.type == TokenType.LBRACKET:

            self.advance()

            if self.current_token.type == TokenType.RBRACKET:

                self.advance()

                return EmptyListPattern()

            else:

                # List pattern: [head | tail]

                head_token = self.expect(TokenType.IDENTIFIER)

                head = VariablePattern(head_token.value)

                self.expect(TokenType.PIPE)

                tail_token = self.expect(TokenType.IDENTIFIER)

                tail = VariablePattern(tail_token.value)

                self.expect(TokenType.RBRACKET)

                return ConsPattern(head, tail)

        

        elif self.current_token.type == TokenType.IDENTIFIER:

            name_token = self.expect(TokenType.IDENTIFIER)

            return VariablePattern(name_token.value)

        

        elif self.current_token.type == TokenType.NUMBER:

            value = self.current_token.value

            self.advance()

            return LiteralPattern(value)

        

        elif self.current_token.type in [TokenType.TRUE, TokenType.FALSE]:

            value = self.current_token.type == TokenType.TRUE

            self.advance()

            return LiteralPattern(value)

        

        else:

            self.error(f'Invalid pattern: {self.current_token.type}')

    

    def parse_logical_or(self) -> ASTNode:

        """Parse logical OR operations."""

        left = self.parse_logical_and()

        

        while self.current_token and self.current_token.type == TokenType.OR:

            operator = self.current_token.type

            self.advance()

            right = self.parse_logical_and()

            left = BinaryOp(left, operator, right)

        

        return left

    

    def parse_logical_and(self) -> ASTNode:

        """Parse logical AND operations."""

        left = self.parse_comparison()

        

        while self.current_token and self.current_token.type == TokenType.AND:

            operator = self.current_token.type

            self.advance()

            right = self.parse_comparison()

            left = BinaryOp(left, operator, right)

        

        return left

    

    def parse_comparison(self) -> ASTNode:

        """Parse comparison operations."""

        left = self.parse_additive()

        

        while self.current_token and self.current_token.type in [

            TokenType.EQUALS, TokenType.NOT_EQUALS,

            TokenType.LESS_THAN, TokenType.GREATER_THAN,

            TokenType.LESS_EQUAL, TokenType.GREATER_EQUAL

        ]:

            operator = self.current_token.type

            self.advance()

            right = self.parse_additive()

            left = BinaryOp(left, operator, right)

        

        return left

    

    def parse_additive(self) -> ASTNode:

        """Parse addition and subtraction."""

        left = self.parse_multiplicative()

        

        while self.current_token and self.current_token.type in [TokenType.PLUS, TokenType.MINUS]:

            operator = self.current_token.type

            self.advance()

            right = self.parse_multiplicative()

            left = BinaryOp(left, operator, right)

        

        return left

    

    def parse_multiplicative(self) -> ASTNode:

        """Parse multiplication, division, and modulo."""

        left = self.parse_unary()

        

        while self.current_token and self.current_token.type in [TokenType.MULTIPLY, TokenType.DIVIDE, TokenType.MODULO]:

            operator = self.current_token.type

            self.advance()

            right = self.parse_unary()

            left = BinaryOp(left, operator, right)

        

        return left

    

    def parse_unary(self) -> ASTNode:

        """Parse unary operations."""

        if self.current_token and self.current_token.type in [TokenType.MINUS, TokenType.NOT]:

            operator = self.current_token.type

            self.advance()

            operand = self.parse_unary()

            return UnaryOp(operator, operand)

        

        return self.parse_application()

    

    def parse_application(self) -> ASTNode:

        """Parse function application."""

        left = self.parse_primary()

        

        while self.current_token and self.current_token.type in [

            TokenType.NUMBER, TokenType.IDENTIFIER, TokenType.LPAREN, 

            TokenType.LBRACKET, TokenType.TRUE, TokenType.FALSE

        ]:

            # Make sure we're not starting a new expression

            if self.current_token.type in [TokenType.IN, TokenType.THEN, TokenType.ELSE, TokenType.WITH]:

                break

            

            argument = self.parse_primary()

            left = FunctionCall(left, [argument])

        

        return left

    

    def parse_primary(self) -> ASTNode:

        """Parse primary expressions (literals, variables, parenthesized expressions, lists)."""

        if self.current_token.type == TokenType.NUMBER:

            value = self.current_token.value

            self.advance()

            return NumberLiteral(value)

        

        elif self.current_token.type == TokenType.TRUE:

            self.advance()

            return BoolLiteral(True)

        

        elif self.current_token.type == TokenType.FALSE:

            self.advance()

            return BoolLiteral(False)

        

        elif self.current_token.type == TokenType.IDENTIFIER:

            name = self.current_token.value

            self.advance()

            return Variable(name)

        

        elif self.current_token.type == TokenType.LPAREN:

            self.advance()

            expr = self.parse_expression()

            self.expect(TokenType.RPAREN)

            return expr

        

        elif self.current_token.type == TokenType.LBRACKET:

            return self.parse_list()

        

        else:

            self.error(f'Unexpected token: {self.current_token.type}')

    

    def parse_list(self) -> ListLiteral:

        """Parse a list literal."""

        self.expect(TokenType.LBRACKET)

        elements = []

        

        if self.current_token.type == TokenType.RBRACKET:

            self.advance()

            return ListLiteral(elements)

        

        elements.append(self.parse_expression())

        

        while self.current_token.type == TokenType.COMMA:

            self.advance()

            elements.append(self.parse_expression())

        

        self.expect(TokenType.RBRACKET)

        return ListLiteral(elements)



# ============================================================================

# RUNTIME VALUES

# ============================================================================


class Value:

    """Base class for runtime values."""

    pass



class NumberValue(Value):

    """Represents a numeric value at runtime."""

    

    def __init__(self, value: float):

        self.value = value

    

    def __repr__(self):

        return f'NumberValue({self.value})'

    

    def __eq__(self, other):

        return isinstance(other, NumberValue) and self.value == other.value

    

    def __hash__(self):

        return hash(self.value)



class BoolValue(Value):

    """Represents a boolean value at runtime."""

    

    def __init__(self, value: bool):

        self.value = value

    

    def __repr__(self):

        return f'BoolValue({self.value})'

    

    def __eq__(self, other):

        return isinstance(other, BoolValue) and self.value == other.value

    

    def __hash__(self):

        return hash(self.value)



class ListValue(Value):

    """Represents an immutable list at runtime."""

    

    def __init__(self, elements: List[Value]):

        self.elements = tuple(elements)  # Immutable tuple

    

    def __repr__(self):

        return f'ListValue({list(self.elements)})'

    

    def __eq__(self, other):

        return isinstance(other, ListValue) and self.elements == other.elements

    

    def __hash__(self):

        return hash(self.elements)



class FunctionValue(Value):

    """Represents a function value at runtime."""

    

    def __init__(self, parameters: List[str], body: ASTNode, closure: 'Environment'):

        self.parameters = parameters

        self.body = body

        self.closure = closure

    

    def __repr__(self):

        return f'FunctionValue({self.parameters}, ...)'



# ============================================================================

# ENVIRONMENT

# ============================================================================


class Environment:

    """

    Represents a lexical environment for variable bindings.

    Environments are immutable and form a chain through parent pointers.

    """

    

    def __init__(self, parent: Optional['Environment'] = None):

        self.bindings: Dict[str, Value] = {}

        self.parent = parent

    

    def define(self, name: str, value: Value) -> 'Environment':

        """Create a new environment with an additional binding."""

        new_env = Environment(self.parent)

        new_env.bindings = self.bindings.copy()

        new_env.bindings[name] = value

        return new_env

    

    def lookup(self, name: str) -> Value:

        """Look up a variable in the environment chain."""

        if name in self.bindings:

            return self.bindings[name]

        elif self.parent is not None:

            return self.parent.lookup(name)

        else:

            raise NameError(f'Undefined variable: {name}')

    

    def extend(self, names: List[str], values: List[Value]) -> 'Environment':

        """Create a new environment with multiple bindings."""

        new_env = Environment(self)

        for name, value in zip(names, values):

            new_env.bindings[name] = value

        return new_env



# ============================================================================

# EVALUATOR

# ============================================================================


class Evaluator:

    """

    Evaluates PureFunc AST nodes to produce runtime values.

    Uses environment-based interpretation with support for closures.

    """

    

    def __init__(self):

        self.global_env = self.create_global_environment()

    

    def create_global_environment(self) -> Environment:

        """Create the global environment with built-in functions."""

        env = Environment()

        

        # Built-in: print

        def builtin_print(arg: Value) -> Value:

            print(self.value_to_string(arg))

            return arg

        

        env.bindings['print'] = self.create_builtin('print', 1, builtin_print)

        

        # Built-in: head (first element of list)

        def builtin_head(lst: Value) -> Value:

            if not isinstance(lst, ListValue):

                raise TypeError('head requires a list')

            if len(lst.elements) == 0:

                raise RuntimeError('head of empty list')

            return lst.elements[0]

        

        env.bindings['head'] = self.create_builtin('head', 1, builtin_head)

        

        # Built-in: tail (all but first element)

        def builtin_tail(lst: Value) -> Value:

            if not isinstance(lst, ListValue):

                raise TypeError('tail requires a list')

            if len(lst.elements) == 0:

                raise RuntimeError('tail of empty list')

            return ListValue(list(lst.elements[1:]))

        

        env.bindings['tail'] = self.create_builtin('tail', 1, builtin_tail)

        

        # Built-in: cons (prepend element to list)

        def builtin_cons(elem: Value, lst: Value) -> Value:

            if not isinstance(lst, ListValue):

                raise TypeError('cons requires a list as second argument')

            return ListValue([elem] + list(lst.elements))

        

        env.bindings['cons'] = self.create_builtin('cons', 2, builtin_cons)

        

        # Built-in: length

        def builtin_length(lst: Value) -> Value:

            if not isinstance(lst, ListValue):

                raise TypeError('length requires a list')

            return NumberValue(len(lst.elements))

        

        env.bindings['length'] = self.create_builtin('length', 1, builtin_length)

        

        # Built-in: isEmpty

        def builtin_is_empty(lst: Value) -> Value:

            if not isinstance(lst, ListValue):

                raise TypeError('isEmpty requires a list')

            return BoolValue(len(lst.elements) == 0)

        

        env.bindings['isEmpty'] = self.create_builtin('isEmpty', 1, builtin_is_empty)

        

        # Built-in: append (concatenate two lists)

        def builtin_append(lst1: Value, lst2: Value) -> Value:

            if not isinstance(lst1, ListValue) or not isinstance(lst2, ListValue):

                raise TypeError('append requires two lists')

            return ListValue(list(lst1.elements) + list(lst2.elements))

        

        env.bindings['append'] = self.create_builtin('append', 2, builtin_append)

        

        # Built-in: range (create list of numbers)

        def builtin_range(start: Value, end: Value) -> Value:

            if not isinstance(start, NumberValue) or not isinstance(end, NumberValue):

                raise TypeError('range requires two numbers')

            return ListValue([NumberValue(i) for i in range(int(start.value), int(end.value))])

        

        env.bindings['range'] = self.create_builtin('range', 2, builtin_range)

        

        return env

    

    def create_builtin(self, name: str, arity: int, implementation) -> FunctionValue:

        """Create a built-in function value."""

        class BuiltinFunction(FunctionValue):

            def __init__(self, name, arity, impl):

                self.name = name

                self.arity = arity

                self.implementation = impl

                self.parameters = [f'arg{i}' for i in range(arity)]

                self.body = None

                self.closure = None

            

            def __repr__(self):

                return f'<builtin {self.name}>'

        

        return BuiltinFunction(name, arity, implementation)

    

    def evaluate(self, node: ASTNode, env: Environment) -> Value:

        """Evaluate an AST node in the given environment."""

        if isinstance(node, NumberLiteral):

            return NumberValue(node.value)

        

        elif isinstance(node, BoolLiteral):

            return BoolValue(node.value)

        

        elif isinstance(node, Variable):

            return env.lookup(node.name)

        

        elif isinstance(node, BinaryOp):

            return self.evaluate_binary_op(node, env)

        

        elif isinstance(node, UnaryOp):

            return self.evaluate_unary_op(node, env)

        

        elif isinstance(node, FunctionDef):

            return FunctionValue(node.parameters, node.body, env)

        

        elif isinstance(node, FunctionCall):

            return self.evaluate_function_call(node, env)

        

        elif isinstance(node, IfExpression):

            return self.evaluate_if_expression(node, env)

        

        elif isinstance(node, LetBinding):

            return self.evaluate_let_binding(node, env)

        

        elif isinstance(node, ListLiteral):

            return self.evaluate_list_literal(node, env)

        

        elif isinstance(node, MatchExpression):

            return self.evaluate_match_expression(node, env)

        

        else:

            raise RuntimeError(f'Unknown AST node type: {type(node).__name__}')

    

    def evaluate_binary_op(self, node: BinaryOp, env: Environment) -> Value:

        """Evaluate binary operations."""

        left_val = self.evaluate(node.left, env)

        right_val = self.evaluate(node.right, env)

        

        if node.operator == TokenType.PLUS:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                return NumberValue(left_val.value + right_val.value)

            else:

                raise TypeError('Addition requires numbers')

        

        elif node.operator == TokenType.MINUS:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                return NumberValue(left_val.value - right_val.value)

            else:

                raise TypeError('Subtraction requires numbers')

        

        elif node.operator == TokenType.MULTIPLY:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                return NumberValue(left_val.value * right_val.value)

            else:

                raise TypeError('Multiplication requires numbers')

        

        elif node.operator == TokenType.DIVIDE:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                if right_val.value == 0:

                    raise ZeroDivisionError('Division by zero')

                return NumberValue(left_val.value / right_val.value)

            else:

                raise TypeError('Division requires numbers')

        

        elif node.operator == TokenType.MODULO:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                return NumberValue(left_val.value % right_val.value)

            else:

                raise TypeError('Modulo requires numbers')

        

        elif node.operator == TokenType.EQUALS:

            return BoolValue(left_val == right_val)

        

        elif node.operator == TokenType.NOT_EQUALS:

            return BoolValue(left_val != right_val)

        

        elif node.operator == TokenType.LESS_THAN:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                return BoolValue(left_val.value < right_val.value)

            else:

                raise TypeError('Comparison requires numbers')

        

        elif node.operator == TokenType.GREATER_THAN:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                return BoolValue(left_val.value > right_val.value)

            else:

                raise TypeError('Comparison requires numbers')

        

        elif node.operator == TokenType.LESS_EQUAL:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                return BoolValue(left_val.value <= right_val.value)

            else:

                raise TypeError('Comparison requires numbers')

        

        elif node.operator == TokenType.GREATER_EQUAL:

            if isinstance(left_val, NumberValue) and isinstance(right_val, NumberValue):

                return BoolValue(left_val.value >= right_val.value)

            else:

                raise TypeError('Comparison requires numbers')

        

        elif node.operator == TokenType.AND:

            if isinstance(left_val, BoolValue) and isinstance(right_val, BoolValue):

                return BoolValue(left_val.value and right_val.value)

            else:

                raise TypeError('Logical AND requires booleans')

        

        elif node.operator == TokenType.OR:

            if isinstance(left_val, BoolValue) and isinstance(right_val, BoolValue):

                return BoolValue(left_val.value or right_val.value)

            else:

                raise TypeError('Logical OR requires booleans')

        

        else:

            raise RuntimeError(f'Unknown operator: {node.operator}')

    

    def evaluate_unary_op(self, node: UnaryOp, env: Environment) -> Value:

        """Evaluate unary operations."""

        operand_val = self.evaluate(node.operand, env)

        

        if node.operator == TokenType.MINUS:

            if isinstance(operand_val, NumberValue):

                return NumberValue(-operand_val.value)

            else:

                raise TypeError('Unary minus requires a number')

        

        elif node.operator == TokenType.NOT:

            if isinstance(operand_val, BoolValue):

                return BoolValue(not operand_val.value)

            else:

                raise TypeError('Logical NOT requires a boolean')

        

        else:

            raise RuntimeError(f'Unknown unary operator: {node.operator}')

    

    def evaluate_function_call(self, node: FunctionCall, env: Environment) -> Value:

        """Evaluate function application with support for currying."""

        func_val = self.evaluate(node.function, env)

        

        if not isinstance(func_val, FunctionValue):

            raise TypeError(f'Cannot call non-function value')

        

        arg_vals = [self.evaluate(arg, env) for arg in node.arguments]

        

        # Handle built-in functions

        if hasattr(func_val, 'implementation') and func_val.implementation is not None:

            if len(arg_vals) < func_val.arity:

                # Partial application

                bound_args = arg_vals

                remaining_arity = func_val.arity - len(arg_vals)

                

                def partial_impl(*args):

                    all_args = bound_args + list(args)

                    return func_val.implementation(*all_args)

                

                return self.create_builtin(

                    f'{func_val.name}_partial',

                    remaining_arity,

                    partial_impl

                )

            

            elif len(arg_vals) == func_val.arity:

                return func_val.implementation(*arg_vals)

            

            else:

                # Apply with exact arity, then apply result to remaining args

                result = func_val.implementation(*arg_vals[:func_val.arity])

                for arg in arg_vals[func_val.arity:]:

                    if not isinstance(result, FunctionValue):

                        raise TypeError('Too many arguments to function')

                    result = self.evaluate_function_call(

                        FunctionCall(Variable('_'), [Variable('_')]),

                        Environment().define('_', result).define('_', arg)

                    )

                return result

        

        # Handle user-defined functions

        if len(arg_vals) < len(func_val.parameters):

            # Partial application

            bound_params = func_val.parameters[:len(arg_vals)]

            remaining_params = func_val.parameters[len(arg_vals):]

            new_env = func_val.closure.extend(bound_params, arg_vals)

            return FunctionValue(remaining_params, func_val.body, new_env)

        

        elif len(arg_vals) == len(func_val.parameters):

            # Exact application

            new_env = func_val.closure.extend(func_val.parameters, arg_vals)

            return self.evaluate(func_val.body, new_env)

        

        else:

            # Over-application

            first_args = arg_vals[:len(func_val.parameters)]

            remaining_args = arg_vals[len(func_val.parameters):]

            new_env = func_val.closure.extend(func_val.parameters, first_args)

            result = self.evaluate(func_val.body, new_env)

            

            for arg in remaining_args:

                if not isinstance(result, FunctionValue):

                    raise TypeError('Too many arguments to function')

                if hasattr(result, 'implementation'):

                    result = result.implementation(arg)

                else:

                    new_env = result.closure.extend(result.parameters, [arg])

                    result = self.evaluate(result.body, new_env)

            

            return result

    

    def evaluate_if_expression(self, node: IfExpression, env: Environment) -> Value:

        """Evaluate conditional expressions."""

        condition_val = self.evaluate(node.condition, env)

        

        if not isinstance(condition_val, BoolValue):

            raise TypeError('Condition must be a boolean')

        

        if condition_val.value:

            return self.evaluate(node.then_branch, env)

        else:

            return self.evaluate(node.else_branch, env)

    

    def evaluate_let_binding(self, node: LetBinding, env: Environment) -> Value:

        """Evaluate let bindings."""

        value = self.evaluate(node.value, env)

        new_env = env.define(node.name, value)

        return self.evaluate(node.body, new_env)

    

    def evaluate_list_literal(self, node: ListLiteral, env: Environment) -> Value:

        """Evaluate list literals."""

        element_vals = [self.evaluate(elem, env) for elem in node.elements]

        return ListValue(element_vals)

    

    def evaluate_match_expression(self, node: MatchExpression, env: Environment) -> Value:

        """Evaluate match expressions."""

        value = self.evaluate(node.value, env)

        

        for case in node.cases:

            match_env = self.try_match_pattern(case.pattern, value, env)

            if match_env is not None:

                return self.evaluate(case.result, match_env)

        

        raise RuntimeError('No matching pattern found')

    

    def try_match_pattern(self, pattern: Pattern, value: Value, env: Environment) -> Optional[Environment]:

        """Try to match a pattern against a value."""

        if isinstance(pattern, EmptyListPattern):

            if isinstance(value, ListValue) and len(value.elements) == 0:

                return env

            else:

                return None

        

        elif isinstance(pattern, VariablePattern):

            return env.define(pattern.name, value)

        

        elif isinstance(pattern, ConsPattern):

            if isinstance(value, ListValue) and len(value.elements) > 0:

                head = value.elements[0]

                tail = ListValue(list(value.elements[1:]))

                

                env = self.try_match_pattern(pattern.head, head, env)

                if env is None:

                    return None

                

                env = self.try_match_pattern(pattern.tail, tail, env)

                return env

            else:

                return None

        

        elif isinstance(pattern, LiteralPattern):

            if isinstance(value, NumberValue) and value.value == pattern.value:

                return env

            elif isinstance(value, BoolValue) and value.value == pattern.value:

                return env

            else:

                return None

        

        else:

            raise RuntimeError(f'Unknown pattern type: {type(pattern).__name__}')

    

    def value_to_string(self, value: Value) -> str:

        """Convert a value to a string for display."""

        if isinstance(value, NumberValue):

            if value.value == int(value.value):

                return str(int(value.value))

            else:

                return str(value.value)

        elif isinstance(value, BoolValue):

            return 'true' if value.value else 'false'

        elif isinstance(value, ListValue):

            elements = [self.value_to_string(elem) for elem in value.elements]

            return '[' + ', '.join(elements) + ']'

        elif isinstance(value, FunctionValue):

            if hasattr(value, 'name'):

                return f'<builtin {value.name}>'

            else:

                return '<function>'

        else:

            return str(value)



# ============================================================================

# REPL

# ============================================================================


class REPL:

    """

    Read-Eval-Print Loop for interactive PureFunc programming.

    """

    

    def __init__(self):

        self.evaluator = Evaluator()

        self.environment = self.evaluator.global_env

    

    def run(self):

        """Run the interactive REPL."""

        print('=' * 60)

        print('PureFunc REPL v1.0')

        print('A minimal functional programming language')

        print('=' * 60)

        print('Type expressions to evaluate them.')

        print('Use Ctrl+C or Ctrl+D to exit.')

        print('=' * 60)

        print()

        

        while True:

            try:

                source = input('> ')

                if not source.strip():

                    continue

                

                # Special commands

                if source.strip() == ':quit' or source.strip() == ':q':

                    print('Goodbye!')

                    break

                

                if source.strip() == ':help' or source.strip() == ':h':

                    self.print_help()

                    continue

                

                # Lex, parse, and evaluate

                lexer = Lexer(source)

                tokens = lexer.tokenize()

                

                parser = Parser(tokens)

                ast = parser.parse()

                

                result = self.evaluator.evaluate(ast, self.environment)

                

                print(self.evaluator.value_to_string(result))

                print()

                

            except KeyboardInterrupt:

                print('\nGoodbye!')

                break

            except EOFError:

                print('\nGoodbye!')

                break

            except Exception as e:

                print(f'Error: {e}')

                print()

    

    def print_help(self):

        """Print help information."""

        print()

        print('PureFunc Help')

        print('=' * 60)

        print('Commands:')

        print('  :help, :h    Show this help message')

        print('  :quit, :q    Exit the REPL')

        print()

        print('Examples:')

        print('  42                              Number literal')

        print('  [1, 2, 3]                       List literal')

        print('  fn x -> x + 1                   Function definition')

        print('  let add = fn x y -> x + y in    Let binding')

        print('    add 3 5')

        print('  if x > 0 then x else 0          Conditional')

        print('  match list with                 Pattern matching')

        print('    [] -> 0')

        print('    [h | t] -> h')

        print()

        print('Built-in functions:')

        print('  print, head, tail, cons, length, isEmpty, append, range')

        print('=' * 60)

        print()

    

    def run_file(self, filename: str):

        """Execute a PureFunc source file."""

        try:

            with open(filename, 'r') as f:

                source = f.read()

            

            lexer = Lexer(source)

            tokens = lexer.tokenize()

            

            parser = Parser(tokens)

            ast = parser.parse()

            

            result = self.evaluator.evaluate(ast, self.environment)

            print(self.evaluator.value_to_string(result))

            

        except FileNotFoundError:

            print(f'Error: File not found: {filename}')

            sys.exit(1)

        except Exception as e:

            print(f'Error: {e}')

            sys.exit(1)



# ============================================================================

# MAIN ENTRY POINT

# ============================================================================


def main():

    """Main entry point for the PureFunc interpreter."""

    if len(sys.argv) > 1:

        # Run file

        repl = REPL()

        repl.run_file(sys.argv[1])

    else:

        # Interactive REPL

        repl = REPL()

        repl.run()



if __name__ == '__main__':

    main()



CONCLUSION AND FUTURE DIRECTIONS

We have built a complete, functional programming language from scratch. PureFunc demonstrates all the essential concepts of functional programming: immutability, first-class functions, higher-order functions, pattern matching, closures, and recursion. The implementation is clean, well-documented, and suitable for teaching purposes.


Students using PureFunc will learn to think functionally. They will understand how immutability eliminates entire classes of bugs. They will see how functions can be composed to build complex behavior from simple pieces. They will appreciate the elegance of pattern matching for working with data structures. They will master recursion as a natural way to express repetitive computation.


The language is minimal but complete. It includes everything needed to write real programs while remaining simple enough to understand fully. The implementation demonstrates important computer science concepts: lexical analysis, parsing, abstract syntax trees, environments, closures, and evaluation strategies.


Future enhancements could include type inference, algebraic data types, a module system, and compilation to bytecode. However, the current implementation provides a solid foundation for learning functional programming. Students can experiment with the language, write programs, and even extend the interpreter itself as a learning exercise.


Building a programming language is one of the most rewarding projects in computer science. It combines theory and practice, requiring understanding of formal grammars, data structures, algorithms, and software engineering. This tutorial has guided you through the entire process, from tokenization to evaluation, with complete working code that you can run, modify, and extend.



EXAMPLE PROGRAMS FOR PUREFUNC

Below are comprehensive example programs demonstrating the capabilities of PureFunc. Each example includes detailed explanations and shows different aspects of functional programming.


EXAMPLE 1: FACTORIAL COMPUTATION


The factorial function is a classic example of recursion. It computes the product of all positive integers less than or equal to a given number.


let factorial = fn n ->

  if n == 0

  then 1

  else n * factorial (n - 1)

in

factorial 5


This program defines a recursive function that computes factorial. When n equals zero, we return one as the base case. Otherwise, we multiply n by the factorial of n minus one. The result for factorial of five is one hundred twenty.


We can also write an iterative version using an accumulator:


let factorialIter = fn n ->

  let helper = fn acc count ->

    if count == 0

    then acc

    else helper (acc * count) (count - 1)

  in

  helper 1 n

in

factorialIter 6


This version uses tail recursion with an accumulator. The helper function multiplies the accumulator by the current count and decrements the count until it reaches zero. This approach is more efficient because it can be optimized by a compiler.


EXAMPLE 2: FIBONACCI SEQUENCE


The Fibonacci sequence is another classic recursive problem where each number is the sum of the two preceding ones.


let fib = fn n ->

  if n <= 1

  then n

  else fib (n - 1) + fib (n - 2)

in

fib 10


This straightforward implementation computes Fibonacci numbers recursively. However, it is inefficient because it recalculates the same values multiple times. For fibonacci of ten, the result is fifty-five.


A more efficient version uses memoization through an iterative approach:


let fibIter = fn n ->

  let helper = fn a b count ->

    if count == 0

    then a

    else helper b (a + b) (count - 1)

  in

  helper 0 1 n

in

fibIter 15


This version maintains two accumulators representing consecutive Fibonacci numbers and iterates n times. It runs in linear time instead of exponential time.


EXAMPLE 3: LIST OPERATIONS


Lists are fundamental in functional programming. Here we demonstrate various list operations.


let map = fn f list ->

  match list with

    [] -> []

    [head | tail] -> cons (f head) (map f tail)

in

let double = fn x -> x * 2 in

map double [1, 2, 3, 4, 5]


The map function applies a function to every element of a list. It uses pattern matching to handle the empty list base case and the recursive case where we apply the function to the head and recursively process the tail. The result is a list with each element doubled: two, four, six, eight, ten.


Here is a filter function that keeps only elements satisfying a predicate:


let filter = fn pred list ->

  match list with

    [] -> []

    [head | tail] ->

      if pred head

      then cons head (filter pred tail)

      else filter pred tail

in

let isEven = fn x -> x mod 2 == 0 in

filter isEven [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


The filter function examines each element. If the predicate returns true, we include the element in the result. Otherwise, we skip it. This example filters for even numbers, producing two, four, six, eight, ten.


EXAMPLE 4: FOLD OPERATIONS


Folding (also called reducing) is a fundamental operation that processes a list to produce a single value.


let foldl = fn f acc list ->

  match list with

    [] -> acc

    [head | tail] -> foldl f (f acc head) tail

in

let sum = foldl (fn a b -> a + b) 0 in

sum [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


The foldl function processes a list from left to right, accumulating a result. We start with an initial accumulator and apply the function to the accumulator and each element. This example sums all numbers from one to ten, producing fifty-five.


We can use fold to implement many other operations:


let product = foldl (fn a b -> a * b) 1 in

let maximum = fn list ->

  match list with

    [] -> 0

    [head | tail] -> foldl (fn a b -> if a > b then a else b) head tail

in

let concat = fn lists ->

  foldl (fn a b -> append a b) [] lists

in

product [1, 2, 3, 4, 5]


This demonstrates computing the product of a list, finding the maximum element, and concatenating multiple lists.


EXAMPLE 5: HIGHER-ORDER FUNCTIONS


Higher-order functions take functions as arguments or return functions as results. They enable powerful abstractions.


let compose = fn f g -> fn x -> f (g x) in

let add1 = fn x -> x + 1 in

let double = fn x -> x * 2 in

let add1ThenDouble = compose double add1 in

add1ThenDouble 5


Function composition creates a new function by chaining two functions. The result of the second function becomes the input to the first. This example adds one to five (getting six) then doubles it (getting twelve).


Here is a more complex example with partial application:


let add = fn x y -> x + y in

let increment = add 1 in

let add10 = add 10 in

map increment [1, 2, 3, 4, 5]


Partial application allows us to create specialized functions from general ones. The increment function is created by partially applying add with one. We can then use it with map to increment every element of a list.


EXAMPLE 6: QUICKSORT


Quicksort is an elegant sorting algorithm that demonstrates recursion and list manipulation.


let quicksort = fn list ->

  match list with

    [] -> []

    [pivot | rest] ->

      let smaller = filter (fn x -> x < pivot) rest in

      let larger = filter (fn x -> x >= pivot) rest in

      append (append (quicksort smaller) [pivot]) (quicksort larger)

in

let filter = fn pred list ->

  match list with

    [] -> []

    [head | tail] ->

      if pred head

      then cons head (filter pred tail)

      else filter pred tail

in

quicksort [3, 7, 1, 9, 2, 8, 4, 6, 5]


This implementation picks the first element as the pivot, partitions the rest into smaller and larger elements, recursively sorts both partitions, and concatenates them with the pivot in the middle. The result is a sorted list: one, two, three, four, five, six, seven, eight, nine.


EXAMPLE 7: TREE OPERATIONS


Although PureFunc does not have built-in tree types, we can represent trees as nested lists and operate on them functionally.


let sumTree = fn tree ->

  match tree with

    [] -> 0

    [value | children] ->

      if isEmpty children

      then value

      else value + foldl (fn acc child -> acc + sumTree child) 0 children

in

let foldl = fn f acc list ->

  match list with

    [] -> acc

    [head | tail] -> foldl f (f acc head) tail

in

sumTree [1, [[2, []], [3, [[4, []], [5, []]]]]]


This represents a tree where each node is a list containing a value and a list of children. The sumTree function recursively sums all values in the tree. This example tree has nodes with values one, two, three, four, and five, summing to fifteen.


EXAMPLE 8: PRIME NUMBERS


Computing prime numbers demonstrates filtering and mathematical operations.


let isPrime = fn n ->

  let helper = fn divisor ->

    if divisor * divisor > n

    then true

    else if n mod divisor == 0

    then false

    else helper (divisor + 1)

  in

  if n < 2

  then false

  else helper 2

in

let primesUpTo = fn n ->

  filter isPrime (range 2 n)

in

primesUpTo 30


The isPrime function checks if a number is prime by testing divisibility up to its square root. The primesUpTo function generates all primes up to a given number by filtering the range. This produces two, three, five, seven, eleven, thirteen, seventeen, nineteen, twenty-three, and twenty-nine.


EXAMPLE 9: LIST REVERSAL


Reversing a list is a common operation that can be implemented efficiently with an accumulator.


let reverse = fn list ->

  let helper = fn acc remaining ->

    match remaining with

      [] -> acc

      [head | tail] -> helper (cons head acc) tail

  in

  helper [] list

in

reverse [1, 2, 3, 4, 5]


This implementation uses tail recursion with an accumulator. We process each element from the original list and prepend it to the accumulator, effectively reversing the order. The result is five, four, three, two, one.


EXAMPLE 10: FLATTEN NESTED LISTS


Flattening converts a nested list structure into a single-level list.


let flatten = fn list ->

  match list with

    [] -> []

    [head | tail] ->

      if isEmpty head

      then flatten tail

      else append (flatten head) (flatten tail)

in

flatten [[1, 2], [3, 4, 5], [6]]


This recursive implementation processes each element. If the element is a list, we recursively flatten it and append the results. This example flattens nested lists into one, two, three, four, five, six.


EXAMPLE 11: TAKE AND DROP


These functions extract portions of lists.


let take = fn n list ->

  if n == 0

  then []

  else match list with

    [] -> []

    [head | tail] -> cons head (take (n - 1) tail)

in

let drop = fn n list ->

  if n == 0

  then list

  else match list with

    [] -> []

    [head | tail] -> drop (n - 1) tail

in

take 3 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


The take function returns the first n elements of a list. The drop function removes the first n elements and returns the rest. Taking three elements from the list produces one, two, three.


EXAMPLE 12: ZIP LISTS


Zipping combines two lists into a list of pairs.


let zip = fn list1 list2 ->

  match list1 with

    [] -> []

    [h1 | t1] ->

      match list2 with

        [] -> []

        [h2 | t2] -> cons [h1, h2] (zip t1 t2)

in

zip [1, 2, 3] [4, 5, 6]


This function pairs corresponding elements from two lists. The result is a list of pairs: one with four, two with five, three with six, represented as nested lists.


EXAMPLE 13: ALL AND ANY


These predicates test whether all or any elements satisfy a condition.


let all = fn pred list ->

  match list with

    [] -> true

    [head | tail] ->

      if pred head

      then all pred tail

      else false

in

let any = fn pred list ->

  match list with

    [] -> false

    [head | tail] ->

      if pred head

      then true

      else any pred tail

in

let isPositive = fn x -> x > 0 in

all isPositive [1, 2, 3, 4, 5]


The all function returns true if every element satisfies the predicate. The any function returns true if at least one element satisfies the predicate. This example checks if all numbers are positive, returning true.


EXAMPLE 14: GENERATE SEQUENCES


Generating sequences demonstrates recursive list building.


let replicate = fn n value ->

  if n == 0

  then []

  else cons value (replicate (n - 1) value)

in

let iterate = fn f initial n ->

  if n == 0

  then []

  else cons initial (iterate f (f initial) (n - 1))

in

let powers = iterate (fn x -> x * 2) 1 10 in

powers


The replicate function creates a list with n copies of a value. The iterate function generates a sequence by repeatedly applying a function. This example generates powers of two: one, two, four, eight, sixteen, thirty-two, sixty-four, one hundred twenty-eight, two hundred fifty-six, five hundred twelve.


EXAMPLE 15: PARTITION


Partitioning splits a list into two lists based on a predicate.


let partition = fn pred list ->

  let helper = fn trueList falseList remaining ->

    match remaining with

      [] -> [trueList, falseList]

      [head | tail] ->

        if pred head

        then helper (append trueList [head]) falseList tail

        else helper trueList (append falseList [head]) tail

  in

  helper [] [] list

in

let isEven = fn x -> x mod 2 == 0 in

partition isEven [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


This function returns a list containing two lists: elements satisfying the predicate and elements not satisfying it. The result separates even and odd numbers.


EXAMPLE 16: MERGE SORTED LISTS


Merging is essential for merge sort and working with sorted data.


let merge = fn list1 list2 ->

  match list1 with

    [] -> list2

    [h1 | t1] ->

      match list2 with

        [] -> list1

        [h2 | t2] ->

          if h1 < h2

          then cons h1 (merge t1 list2)

          else cons h2 (merge list1 t2)

in

merge [1, 3, 5, 7] [2, 4, 6, 8]


This function merges two sorted lists into a single sorted list. It compares the heads of both lists and takes the smaller one, then recursively merges the remainder. The result is one, two, three, four, five, six, seven, eight.


EXAMPLE 17: GROUP CONSECUTIVE ELEMENTS


Grouping consecutive equal elements is useful for run-length encoding.


let group = fn list ->

  match list with

    [] -> []

    [head | tail] ->

      let takeWhile = fn pred lst ->

        match lst with

          [] -> []

          [h | t] ->

            if pred h

            then cons h (takeWhile pred t)

            else []

      in

      let dropWhile = fn pred lst ->

        match lst with

          [] -> []

          [h | t] ->

            if pred h

            then dropWhile pred t

            else lst

      in

      let sameAsHead = fn x -> x == head in

      let currentGroup = cons head (takeWhile sameAsHead tail) in

      let remaining = dropWhile sameAsHead tail in

      cons currentGroup (group remaining)

in

group [1, 1, 2, 2, 2, 3, 1, 1, 1]


This groups consecutive equal elements into sublists. The result is nested lists: two ones, three twos, one three, three ones.


EXAMPLE 18: CARTESIAN PRODUCT


The cartesian product combines elements from two lists in all possible ways.


let cartesian = fn list1 list2 ->

  let flatMap = fn f lst ->

    match lst with

      [] -> []

      [head | tail] -> append (f head) (flatMap f tail)

  in

  flatMap (fn x -> map (fn y -> [x, y]) list2) list1

in

let map = fn f list ->

  match list with

    [] -> []

    [head | tail] -> cons (f head) (map f tail)

in

cartesian [1, 2] [3, 4]


This produces all pairs combining elements from both lists: one with three, one with four, two with three, two with four.


EXAMPLE 19: PASCAL'S TRIANGLE

Pascal's triangle demonstrates recursive list generation with mathematical properties.


let pascalRow = fn n ->

  let helper = fn row count ->

    if count == 0

    then row

    else

      let nextRow = fn r ->

        match r with

          [] -> [1]

          [single] -> [1, 1]

          [first | rest] ->

            let pairs = zip r rest in

            let sums = map (fn pair -> head pair + head (tail pair)) pairs in

            cons 1 (append sums [1])

      in

      helper (nextRow row) (count - 1)

  in

  helper [1] n

in

let zip = fn list1 list2 ->

  match list1 with

    [] -> []

    [h1 | t1] ->

      match list2 with

        [] -> []

        [h2 | t2] -> cons [h1, h2] (zip t1 t2)

in

let map = fn f list ->

  match list with

    [] -> []

    [head | tail] -> cons (f head) (map f tail)

in

pascalRow 5


This generates the nth row of Pascal's triangle. Each element is the sum of the two elements above it. Row five is one, five, ten, ten, five, one.


EXAMPLE 20: COMPLETE DEMONSTRATION PROGRAM


Here is a comprehensive program combining multiple concepts:


let compose = fn f g -> fn x -> f (g x) in

let map = fn f list ->

  match list with

    [] -> []

    [head | tail] -> cons (f head) (map f tail)

in

let filter = fn pred list ->

  match list with

    [] -> []

    [head | tail] ->

      if pred head

      then cons head (filter pred tail)

      else filter pred tail

in

let foldl = fn f acc list ->

  match list with

    [] -> acc

    [head | tail] -> foldl f (f acc head) tail

in

let isPrime = fn n ->

  let helper = fn divisor ->

    if divisor * divisor > n

    then true

    else if n mod divisor == 0

    then false

    else helper (divisor + 1)

  in

  if n < 2

  then false

  else helper 2

in

let square = fn x -> x * x in

let sum = foldl (fn a b -> a + b) 0 in

let numbers = range 1 20 in

let primes = filter isPrime numbers in

let squaredPrimes = map square primes in

sum squaredPrimes


This program finds all prime numbers from one to twenty, squares each prime, and sums the results. It demonstrates composition of multiple functional operations: filtering for primes, mapping to compute squares, and folding to sum. The result is one thousand, one hundred, and ninety-one.


These examples demonstrate the power and elegance of functional programming in PureFunc. They show how complex behavior emerges from composing simple, pure functions. Students can experiment with these examples, modify them, and create their own programs to deepen their understanding of functional programming concepts.


ADDENDUM - THE "IN" KEYWORD IN PUREFUNC

The "in" keyword is a crucial part of the "let" binding syntax in PureFunc. It separates the binding definition from the expression where that binding is used. Let me explain this in detail.

BASIC STRUCTURE OF LET BINDINGS

A let binding in PureFunc follows this pattern:

let variableName = valueExpression in bodyExpression


The "in" keyword marks the boundary between two parts:

First, the binding part comes before "in". This is where you define what value the variable should have. The expression after the equals sign is evaluated and bound to the variable name.

Second, the body part comes after "in". This is the expression where you can actually use the variable you just defined. The variable is only available within this body expression.

WHY WE NEED THE "IN" KEYWORD

The "in" keyword is necessary because let bindings in functional languages are expressions, not statements. Unlike imperative languages where you might write:

x = 5
y = x + 3
print(y)

In PureFunc, a let binding must produce a value. The "in" keyword tells the language where to look for that value. Consider this example:

let x = 10 in x + 5

This entire construct is an expression that evaluates to fifteen. The "in" keyword separates the definition of x (which is ten) from the expression that uses x (which is x plus five).

SCOPE AND THE "IN" KEYWORD

The "in" keyword defines the scope of the variable. The variable only exists in the expression after "in". Here is an example:

let x = 5 in x * 2

In this expression, x is bound to five, and the body expression (x times two) evaluates to ten. After this entire let expression completes, x no longer exists. You cannot reference it outside this expression.

This is different from:

let x = 5 in let y = x + 3 in y * 2

Here we have nested let bindings. The first let binds x to five. Within its body (after the first "in"), we have another let binding that binds y to x plus three (which is eight). The innermost body (after the second "in") evaluates y times two, giving sixteen.

MULTIPLE BINDINGS

You can chain multiple let bindings to create a sequence of definitions:

let x = 10 in
let y = 20 in
let z = 30 in
x + y + z

Each "in" keyword introduces the scope where the previous binding is available. This reads as: let x be ten, and in that context, let y be twenty, and in that context, let z be thirty, and in that context, compute x plus y plus z.

This is equivalent to nested scopes:

let x = 10 in (
  let y = 20 in (
    let z = 30 in (
      x + y + z
    )
  )
)

COMPARISON WITH OTHER LANGUAGES

In languages like JavaScript or Python, you might write:

let x = 10;
let y = 20;
return x + y;

The semicolons separate statements, and the return keyword indicates what value to produce. In PureFunc, the "in" keyword serves both purposes: it separates the binding from its usage and indicates where to find the result value.

In mathematical notation, you might write:

"let x = 10, then x + 5"

The "in" keyword in PureFunc is like the "then" in this mathematical phrasing.

PRACTICAL EXAMPLES

Here is a simple calculation using let bindings:

let radius = 5 in
let pi = 3.14159 in
let area = pi * radius * radius in
area

This computes the area of a circle. Each "in" keyword introduces the scope where we can use the previously defined variables. The final result is the value of area.

Here is a more complex example with functions:

let double = fn x -> x * 2 in
let triple = fn x -> x * 3 in
let applyBoth = fn x -> double x + triple x in
applyBoth 5

We define double and triple functions, then define applyBoth which uses both of them. The "in" keywords create nested scopes where each definition is available. The result is ten plus fifteen, which equals twenty-five.

THE "IN" KEYWORD WITH RECURSION

When defining recursive functions, the "in" keyword is essential because it creates the scope where the function can reference itself:

let factorial = fn n ->
  if n == 0
  then 1
  else n * factorial (n - 1)
in
factorial 5

The "in" keyword creates a scope where the name "factorial" is bound to the function. Inside the function body, we can reference "factorial" recursively because we are within the scope created by "in".

WHAT HAPPENS WITHOUT "IN"

If PureFunc did not have the "in" keyword, the language would be ambiguous. Consider:

let x = 10 x + 5

Without "in", it is unclear where the binding ends and where the expression using the binding begins. Is this trying to bind x to "10 x + 5" (which would be an error), or is it trying to bind x to 10 and then evaluate "x + 5"?

The "in" keyword makes this unambiguous:

let x = 10 in x + 5

Now it is clear: bind x to ten, then evaluate x plus five in that context.

SUMMARY

The "in" keyword serves three critical purposes in PureFunc:

First, it separates the binding definition from the expression that uses the binding. Everything before "in" defines what value the variable has. Everything after "in" is where you use that variable.

Second, it defines the scope of the variable. The variable only exists in the expression after "in". Once that expression is fully evaluated, the variable is no longer accessible.

Third, it makes let bindings into expressions rather than statements. The entire "let variable equals value in body" construct evaluates to whatever the body evaluates to, making it composable with other expressions.

Understanding the "in" keyword is fundamental to reading and writing PureFunc code. It is the mechanism that creates local scopes and allows you to build complex expressions from simpler named parts.

No comments: