Saturday, September 05, 2026

Coordinating Autonomous Drone Swarms for Complex Mission Objectives

 



Introduction

The coordination of autonomous drone swarms represents one of the most challenging and fascinating problems in modern robotics and artificial intelligence. When multiple drones must work together to achieve complex goals such as infrastructure inspection, surveillance operations, or search and rescue missions, the system must balance individual drone autonomy with collective intelligence. This article explores the architecture, algorithms, and implementation strategies for coordinating drone swarms using Large Language Model agents, examining both centralized and decentralized control paradigms.

The fundamental challenge in drone swarm coordination lies in enabling multiple autonomous agents to work cooperatively while managing constraints such as energy limitations, obstacle avoidance, task allocation, and communication bandwidth. Each drone must make real-time decisions about navigation, sensing, and task execution while maintaining awareness of the broader mission context and the activities of other drones in the swarm.

Architectural Paradigms for Swarm Coordination

There are three primary architectural approaches to coordinating drone swarms, each with distinct advantages and trade-offs. The choice of architecture depends on mission requirements, communication infrastructure, computational resources, and failure tolerance needs.

Centralized Control Architecture

In a centralized architecture, a ground-based controller system serves as the orchestrator for all drone operations. This base station runs a sophisticated LLM-based agent that maintains global situational awareness, performs task decomposition, assigns missions to individual drones, and monitors progress. Each drone communicates its status, sensor data, and completion reports to the base station, which then issues new commands and task assignments.

The primary advantage of centralized control is optimal global coordination. The base station can see the entire mission space, track all drone positions and states, and make globally optimal decisions about task allocation and resource management. This architecture also simplifies individual drone software, as drones primarily execute commands rather than making high-level strategic decisions.

However, centralized control introduces a critical single point of failure. If communication with the base station is lost or the base station itself fails, the entire swarm becomes inoperative. Additionally, communication bandwidth requirements scale with swarm size, and latency in command-response loops can reduce system responsiveness.

Distributed Autonomous Architecture

In a fully distributed architecture, each drone operates as an independent LLM-based agent with complete decision-making authority. Drones coordinate through peer-to-peer communication, sharing information about discovered targets, completed tasks, and current positions. There is no central controller; instead, coordination emerges from local interactions and shared protocols.

This architecture offers maximum resilience and scalability. Individual drone failures do not compromise the entire mission, and the system can operate effectively even with intermittent communication. Each drone can respond immediately to local conditions without waiting for central commands.

The challenge with distributed control is achieving global coordination without a centralized view. Drones may duplicate effort, miss coverage gaps, or make locally optimal decisions that are globally suboptimal. Implementing consensus algorithms and distributed task allocation mechanisms adds significant complexity to individual drone software.

Hybrid Architecture

A hybrid approach combines centralized strategic planning with distributed tactical execution. The base station performs high-level mission planning, task decomposition, and initial task allocation, but individual drones have autonomy to make tactical decisions about navigation, obstacle avoidance, and local task execution. Drones can also coordinate peer-to-peer for immediate concerns like collision avoidance while reporting status and requesting new tasks from the base station.

This architecture balances the benefits of global coordination with local responsiveness and fault tolerance. The base station provides strategic direction and can replan when mission conditions change, while drones handle real-time challenges without communication delays. If base communication is lost, drones can continue operating autonomously using their last known mission parameters and peer coordination.

System Components and Capabilities

Base Station Controller Agent

The base station serves as the mission command center and implements a sophisticated LLM-based orchestrator agent. This agent is responsible for mission planning, task decomposition, resource allocation, and progress monitoring.

The base station maintains a comprehensive world model that includes the mission area map, known obstacles, target locations, current drone positions and states, task queue, and completion status. This world model is continuously updated as drones report new information.

class BaseStationController:
    def __init__(self, mission_area, llm_interface):
        self.mission_area = mission_area
        self.llm = llm_interface
        self.drone_fleet = {}
        self.task_queue = []
        self.completed_tasks = []
        self.world_model = WorldModel(mission_area)
        
    def initialize_mission(self, mission_objective):
        """
        Uses LLM to decompose high-level mission into specific tasks.
        For example: 'Inspect all bridges in area' becomes individual
        bridge inspection tasks with specific GPS coordinates.
        """
        mission_prompt = f"""
        Mission Objective: {mission_objective}
        Mission Area: {self.mission_area.bounds}
        Available Drones: {len(self.drone_fleet)}
        
        Decompose this mission into specific, executable tasks.
        Each task should include: location (GPS), task type, priority,
        estimated duration, and required drone capabilities.
        """
        
        task_plan = self.llm.generate_task_plan(mission_prompt)
        self.task_queue = self.parse_task_plan(task_plan)
        return self.task_queue
    
    def allocate_tasks(self):
        """
        Assigns tasks to available drones based on proximity,
        energy state, and capability matching.
        """
        available_drones = [d for d in self.drone_fleet.values() 
                          if d.status == 'ready' or d.status == 'idle']
        
        for drone in available_drones:
            if not self.task_queue:
                break
                
            best_task = self.find_optimal_task(drone)
            if best_task and self.can_complete_task(drone, best_task):
                self.assign_task(drone, best_task)
                self.task_queue.remove(best_task)
    
    def find_optimal_task(self, drone):
        """
        Selects the best task for a drone considering distance,
        priority, and energy constraints.
        """
        if not self.task_queue:
            return None
            
        scored_tasks = []
        for task in self.task_queue:
            distance = self.calculate_distance(drone.gps_location, 
                                              task.location)
            energy_needed = self.estimate_energy_requirement(drone, task)
            
            # Calculate score: higher priority, closer distance is better
            score = task.priority / (distance + 1) 
            
            if drone.battery_level >= energy_needed:
                scored_tasks.append((score, task))
        
        if not scored_tasks:
            return None
            
        scored_tasks.sort(reverse=True, key=lambda x: x[0])
        return scored_tasks[0][1]
    
    def can_complete_task(self, drone, task):
        """
        Verifies drone has sufficient energy to complete task
        and return to base.
        """
        task_distance = self.calculate_distance(drone.gps_location, 
                                                task.location)
        return_distance = self.calculate_distance(task.location, 
                                                  self.base_location)
        total_distance = task_distance + return_distance
        
        energy_for_travel = total_distance * drone.energy_per_km
        energy_for_task = task.estimated_duration * drone.hover_energy_rate
        total_energy_needed = energy_for_travel + energy_for_task
        
        # Add safety margin
        return drone.battery_level >= total_energy_needed * 1.2

The base station controller continuously monitors drone status through regular communication updates. When a drone completes a task, reports low battery, or encounters an obstacle it cannot navigate, the base station receives this information and can replan accordingly.

The LLM interface enables the base station to interpret complex mission objectives expressed in natural language and translate them into concrete task specifications. For example, when given the objective "Inspect all bridges in the area for structural defects," the LLM can query a geographic database to identify bridge locations, generate inspection waypoints around each bridge structure, and create tasks that specify the required camera angles and image capture parameters.

Individual Drone Agent

Each drone in the swarm operates as an autonomous agent with sensing, navigation, and decision-making capabilities. When equipped with an onboard LLM, the drone can interpret task descriptions, plan execution strategies, and adapt to unexpected situations.

class DroneAgent:
    def __init__(self, drone_id, base_gps_location, llm_interface=None):
        self.drone_id = drone_id
        self.base_location = base_gps_location
        self.current_location = base_gps_location
        self.llm = llm_interface
        
        # Physical state
        self.battery_level = 100.0  # percentage
        self.max_range_km = 50.0
        self.energy_per_km = 2.0  # percent per km
        self.hover_energy_rate = 0.5  # percent per minute
        
        # Navigation state
        self.current_path = []
        self.current_task = None
        self.status = 'idle'  # idle, navigating, executing, returning
        
        # Sensor state
        self.camera_angle = {'horizontal': 0, 'vertical': -45}
        self.obstacle_sensors = ObstacleSensorArray()
        
        # Communication
        self.message_queue = []
        
    def execute_mission_loop(self):
        """
        Main execution loop for autonomous drone operation.
        Continuously monitors state and executes appropriate actions.
        """
        while True:
            self.update_sensor_readings()
            self.check_battery_level()
            
            if self.status == 'idle':
                self.request_new_task()
            elif self.status == 'navigating':
                self.navigate_to_target()
            elif self.status == 'executing':
                self.execute_current_task()
            elif self.status == 'returning':
                self.return_to_base()
                
            self.process_messages()
            time.sleep(0.1)  # 10 Hz control loop
    
    def check_battery_level(self):
        """
        Monitors battery and initiates return if energy is low.
        """
        distance_to_base = self.calculate_distance(self.current_location, 
                                                   self.base_location)
        energy_to_return = distance_to_base * self.energy_per_km
        
        # Return if battery would be below 20% after returning
        if self.battery_level < energy_to_return + 20.0:
            if self.current_task:
                self.report_to_base(f"Insufficient energy for task {self.current_task.id}, returning to base")
                self.current_task = None
            self.status = 'returning'
            self.plan_return_path()
    
    def navigate_to_target(self):
        """
        Follows planned path while avoiding obstacles.
        """
        if not self.current_path:
            self.status = 'executing'
            return
            
        next_waypoint = self.current_path[0]
        
        # Check for obstacles
        obstacles = self.obstacle_sensors.detect_obstacles()
        if obstacles:
            self.handle_obstacles(obstacles, next_waypoint)
            return
        
        # Move toward waypoint
        self.move_toward(next_waypoint)
        
        # Check if waypoint reached
        if self.distance_to(next_waypoint) < 1.0:  # within 1 meter
            self.current_path.pop(0)
            
        # Update battery based on movement
        self.update_battery_consumption()
    
    def handle_obstacles(self, obstacles, target_waypoint):
        """
        Recalculates path to avoid detected obstacles.
        Uses LLM to determine best avoidance strategy if available.
        """
        if self.llm:
            # Use LLM to analyze obstacle configuration and suggest strategy
            obstacle_description = self.describe_obstacles(obstacles)
            prompt = f"""
            Current position: {self.current_location}
            Target waypoint: {target_waypoint}
            Obstacles detected: {obstacle_description}
            
            Suggest an optimal avoidance strategy. Options:
            1. Increase altitude and fly over
            2. Navigate around to the left
            3. Navigate around to the right
            4. Wait for obstacle to clear (if moving)
            
            Consider energy efficiency and mission urgency.
            """
            
            strategy = self.llm.generate_response(prompt)
            self.execute_avoidance_strategy(strategy)
        else:
            # Use simple geometric avoidance
            self.calculate_alternative_path(obstacles, target_waypoint)
    
    def execute_current_task(self):
        """
        Executes the assigned task based on task type.
        Uses LLM to interpret task requirements if available.
        """
        if not self.current_task:
            self.status = 'idle'
            return
            
        task_type = self.current_task.task_type
        
        if task_type == 'inspect_structure':
            self.perform_structure_inspection()
        elif task_type == 'search_area':
            self.perform_area_search()
        elif task_type == 'surveillance':
            self.perform_surveillance()
        elif task_type == 'search_rescue':
            self.perform_search_rescue()
    
    def perform_structure_inspection(self):
        """
        Inspects a structure like a bridge by capturing images
        from multiple angles.
        """
        structure_location = self.current_task.target_location
        inspection_points = self.current_task.inspection_waypoints
        
        for waypoint in inspection_points:
            self.navigate_to(waypoint)
            
            # Position camera for optimal view
            camera_angle = self.calculate_optimal_camera_angle(
                waypoint, structure_location)
            self.adjust_camera(camera_angle)
            
            # Capture high-resolution image
            image = self.capture_image(high_resolution=True)
            
            # Use LLM to analyze image for defects if available
            if self.llm:
                analysis_result = self.analyze_image_for_defects(image)
                if analysis_result['defects_detected']:
                    self.report_to_base(f"Defects detected at {waypoint}: {analysis_result['description']}")
                    # Capture additional detailed images
                    self.capture_detailed_images(analysis_result['defect_locations'])
            
            # Store image with metadata
            self.store_image(image, waypoint, camera_angle)
        
        self.report_task_completion()
        self.status = 'idle'
    
    def perform_area_search(self):
        """
        Searches an area using a systematic search pattern.
        """
        search_area = self.current_task.search_area
        search_target = self.current_task.target_description
        
        # Generate search pattern (e.g., lawn mower pattern)
        search_waypoints = self.generate_search_pattern(search_area)
        
        for waypoint in search_waypoints:
            self.navigate_to(waypoint)
            
            # Capture video while moving
            video_segment = self.capture_video(duration=5)
            
            # Use LLM to analyze video for target
            if self.llm:
                detection_result = self.analyze_video_for_target(
                    video_segment, search_target)
                
                if detection_result['target_found']:
                    self.report_to_base(f"Target found at {self.current_location}: {detection_result['description']}")
                    # Capture detailed documentation
                    self.document_target(detection_result)
                    
                    # Check if task requires finding all instances or just one
                    if self.current_task.find_all:
                        continue
                    else:
                        break
        
        self.report_task_completion()
        self.status = 'idle'

The drone agent maintains continuous awareness of its physical state including battery level, GPS location, and sensor readings. The navigation system implements path following with real-time obstacle avoidance, recalculating routes when necessary to maintain mission progress while ensuring safety.

When equipped with an LLM interface, the drone can perform sophisticated analysis of sensor data. For example, during bridge inspection, the LLM can analyze captured images to identify potential structural defects such as cracks, corrosion, or deformation. The LLM can describe the nature and severity of detected issues in natural language, enabling human operators to quickly understand findings without manually reviewing thousands of images.

Communication Protocol

Effective communication between drones and the base station is essential for coordinated operation. The communication protocol must handle task assignment, status updates, sensor data transmission, and emergency notifications.

class CommunicationProtocol:
    def __init__(self, drone_id, base_address):
        self.drone_id = drone_id
        self.base_address = base_address
        self.message_sequence = 0
        
    def send_status_update(self, drone_state):
        """
        Sends periodic status update to base station.
        """
        message = {
            'type': 'status_update',
            'drone_id': self.drone_id,
            'sequence': self.message_sequence,
            'timestamp': time.time(),
            'location': drone_state.current_location,
            'battery_level': drone_state.battery_level,
            'status': drone_state.status,
            'current_task_id': drone_state.current_task.id if drone_state.current_task else None
        }
        
        self.transmit_message(message)
        self.message_sequence += 1
    
    def request_task(self, current_location, battery_level):
        """
        Requests new task assignment from base station.
        """
        message = {
            'type': 'task_request',
            'drone_id': self.drone_id,
            'sequence': self.message_sequence,
            'timestamp': time.time(),
            'location': current_location,
            'battery_level': battery_level,
            'capabilities': self.get_drone_capabilities()
        }
        
        response = self.transmit_and_wait_response(message)
        self.message_sequence += 1
        return response
    
    def report_task_completion(self, task_id, results):
        """
        Reports completed task with results to base station.
        """
        message = {
            'type': 'task_completion',
            'drone_id': self.drone_id,
            'sequence': self.message_sequence,
            'timestamp': time.time(),
            'task_id': task_id,
            'completion_status': 'success',
            'results': results,
            'images_captured': len(results.get('images', [])),
            'findings': results.get('findings', [])
        }
        
        self.transmit_message(message)
        self.message_sequence += 1
    
    def report_emergency(self, emergency_type, details):
        """
        Sends high-priority emergency notification.
        """
        message = {
            'type': 'emergency',
            'priority': 'high',
            'drone_id': self.drone_id,
            'sequence': self.message_sequence,
            'timestamp': time.time(),
            'emergency_type': emergency_type,
            'location': details.get('location'),
            'details': details
        }
        
        self.transmit_message(message, priority=True)
        self.message_sequence += 1

The communication protocol implements message sequencing to detect lost messages and ensure reliable delivery. Status updates are sent periodically to maintain situational awareness at the base station. Emergency messages receive priority transmission to ensure rapid response to critical situations such as drone malfunctions or discovery of urgent mission-critical information.

Task Allocation Strategies

Efficient task allocation is crucial for maximizing swarm productivity and ensuring mission completion within energy and time constraints. Several strategies can be employed depending on mission requirements and system architecture.

Centralized Task Assignment

In centralized task assignment, the base station maintains the complete task queue and assigns tasks to drones based on optimization criteria. This approach enables globally optimal allocation but requires continuous communication with the base station.

The base station evaluates each available drone against pending tasks, considering factors such as distance to task location, remaining battery capacity, drone capabilities, and task priority. The assignment algorithm seeks to minimize total mission completion time while ensuring all drones can complete assigned tasks and return to base safely.

class CentralizedTaskAllocator:
    def __init__(self, base_station):
        self.base = base_station
        
    def allocate_all_tasks(self):
        """
        Performs global task allocation optimization across all
        available drones and pending tasks.
        """
        available_drones = self.get_available_drones()
        pending_tasks = self.base.task_queue.copy()
        
        assignments = []
        
        while pending_tasks and available_drones:
            # Find best drone-task pairing
            best_assignment = None
            best_score = float('-inf')
            
            for drone in available_drones:
                for task in pending_tasks:
                    score = self.calculate_assignment_score(drone, task)
                    
                    if score > best_score and self.is_feasible(drone, task):
                        best_score = score
                        best_assignment = (drone, task)
            
            if best_assignment:
                drone, task = best_assignment
                assignments.append(best_assignment)
                available_drones.remove(drone)
                pending_tasks.remove(task)
            else:
                break  # No more feasible assignments
        
        # Execute assignments
        for drone, task in assignments:
            self.assign_task_to_drone(drone, task)
        
        return assignments
    
    def calculate_assignment_score(self, drone, task):
        """
        Calculates score for assigning a specific task to a drone.
        Higher scores indicate better assignments.
        """
        # Distance component: prefer closer tasks
        distance = self.calculate_distance(drone.current_location, 
                                          task.location)
        distance_score = 1.0 / (distance + 1.0)
        
        # Priority component: prefer high-priority tasks
        priority_score = task.priority / 10.0
        
        # Energy efficiency component
        energy_required = self.estimate_energy(drone, task)
        energy_available = drone.battery_level
        energy_score = (energy_available - energy_required) / energy_available
        
        # Capability match component
        capability_score = self.calculate_capability_match(drone, task)
        
        # Weighted combination
        total_score = (0.3 * distance_score + 
                      0.3 * priority_score + 
                      0.2 * energy_score + 
                      0.2 * capability_score)
        
        return total_score
    
    def is_feasible(self, drone, task):
        """
        Checks if drone can feasibly complete task and return to base.
        """
        task_distance = self.calculate_distance(drone.current_location, 
                                                task.location)
        return_distance = self.calculate_distance(task.location, 
                                                  self.base.base_location)
        
        total_travel = task_distance + return_distance
        travel_energy = total_travel * drone.energy_per_km
        task_energy = task.estimated_duration * drone.hover_energy_rate
        total_energy = travel_energy + task_energy
        
        # Require 20% safety margin
        return drone.battery_level >= total_energy * 1.2

The centralized allocator can also implement more sophisticated optimization techniques such as the Hungarian algorithm for optimal bipartite matching or genetic algorithms for complex multi-objective optimization. The LLM can assist in defining optimization objectives based on mission context, adjusting weights for different scoring components based on mission phase or changing conditions.

Distributed Task Selection

In distributed task selection, tasks are published to a shared blackboard or task pool that all drones can access. Each drone independently selects tasks based on local decision-making, claiming tasks to prevent duplication.

class DistributedTaskSelector:
    def __init__(self, drone_agent, task_blackboard):
        self.drone = drone_agent
        self.blackboard = task_blackboard
        
    def select_next_task(self):
        """
        Drone independently selects best available task from
        shared task pool.
        """
        available_tasks = self.blackboard.get_unclaimed_tasks()
        
        if not available_tasks:
            return None
        
        # Score all available tasks
        scored_tasks = []
        for task in available_tasks:
            if self.is_feasible(task):
                score = self.calculate_task_score(task)
                scored_tasks.append((score, task))
        
        if not scored_tasks:
            return None
        
        # Select highest scoring task
        scored_tasks.sort(reverse=True, key=lambda x: x[0])
        selected_task = scored_tasks[0][1]
        
        # Attempt to claim task (may fail if another drone claims first)
        if self.blackboard.claim_task(selected_task.id, self.drone.drone_id):
            return selected_task
        else:
            # Task was claimed by another drone, try again
            return self.select_next_task()
    
    def calculate_task_score(self, task):
        """
        Calculates task score from drone's perspective.
        Prioritizes nearby tasks that match capabilities.
        """
        distance = self.calculate_distance(self.drone.current_location, 
                                          task.location)
        
        # Strong preference for nearby tasks to minimize travel
        distance_score = 100.0 / (distance + 1.0)
        
        # Consider task priority
        priority_score = task.priority * 10.0
        
        # Bonus for tasks in same general area (clustering)
        clustering_bonus = 0.0
        for other_task in self.drone.planned_tasks:
            if self.calculate_distance(task.location, 
                                      other_task.location) < 5.0:
                clustering_bonus += 20.0
        
        return distance_score + priority_score + clustering_bonus

Distributed task selection enables drones to continue operating even when communication with the base station is intermittent. The blackboard pattern provides a coordination mechanism that allows drones to work independently while avoiding task duplication through the claim mechanism.

The LLM can enhance distributed task selection by enabling drones to reason about strategic task sequencing. For example, a drone might recognize that completing a nearby low-priority task first would position it optimally for a high-priority task that will likely become available soon.

Auction-Based Allocation

Auction-based allocation implements a market mechanism where tasks are auctioned to drones. Each drone submits a bid representing its cost or suitability for completing the task, and the task is awarded to the drone with the best bid.

class AuctionBasedAllocator:
    def __init__(self, communication_network):
        self.network = communication_network
        self.active_auctions = {}
        
    def initiate_task_auction(self, task):
        """
        Initiates an auction for a task, soliciting bids from all drones.
        """
        auction_id = self.generate_auction_id()
        
        auction_announcement = {
            'auction_id': auction_id,
            'task': task,
            'deadline': time.time() + 5.0  # 5 second bidding window
        }
        
        # Broadcast auction to all drones
        self.network.broadcast(auction_announcement)
        
        self.active_auctions[auction_id] = {
            'task': task,
            'bids': [],
            'deadline': auction_announcement['deadline']
        }
        
        return auction_id
    
    def submit_bid(self, auction_id, drone_id, bid_value):
        """
        Drone submits bid for a task auction.
        """
        if auction_id not in self.active_auctions:
            return False
        
        auction = self.active_auctions[auction_id]
        
        if time.time() > auction['deadline']:
            return False
        
        bid = {
            'drone_id': drone_id,
            'bid_value': bid_value,
            'timestamp': time.time()
        }
        
        auction['bids'].append(bid)
        return True
    
    def resolve_auction(self, auction_id):
        """
        Determines auction winner and assigns task.
        Lower bid values win (representing cost/distance).
        """
        auction = self.active_auctions[auction_id]
        
        if not auction['bids']:
            return None
        
        # Find lowest bid (best offer)
        winning_bid = min(auction['bids'], key=lambda b: b['bid_value'])
        
        winner_drone_id = winning_bid['drone_id']
        task = auction['task']
        
        # Assign task to winning drone
        self.assign_task(winner_drone_id, task)
        
        # Notify all participants of result
        result = {
            'auction_id': auction_id,
            'winner': winner_drone_id,
            'winning_bid': winning_bid['bid_value']
        }
        self.network.broadcast(result)
        
        del self.active_auctions[auction_id]
        return winner_drone_id
    
    def calculate_bid_value(self, drone, task):
        """
        Drone calculates its bid value for a task.
        Lower values indicate better suitability.
        """
        distance = self.calculate_distance(drone.current_location, 
                                          task.location)
        energy_cost = self.estimate_energy_cost(drone, task)
        
        # Bid is combination of distance and energy cost
        bid_value = distance * 0.5 + energy_cost * 0.5
        
        # Adjust bid based on current workload
        if len(drone.task_queue) > 3:
            bid_value *= 1.5  # Less competitive if already busy
        
        return bid_value

Auction mechanisms provide a decentralized approach to task allocation that can achieve near-optimal results without requiring a central optimizer. Drones naturally gravitate toward tasks they are best suited to complete, and the competitive bidding process ensures efficient resource utilization.

The LLM can enhance auction-based allocation by enabling sophisticated bid calculation strategies. A drone might consider not just immediate cost but also strategic positioning for anticipated future tasks, or it might adjust its bidding strategy based on learned patterns about task distributions and competition from other drones.

Navigation and Path Planning

Effective navigation is fundamental to drone swarm operation. Each drone must plan efficient paths to task locations while avoiding obstacles and managing energy consumption.

Waypoint-Based Navigation

The simplest navigation approach uses waypoint-based path following. A path is defined as a sequence of GPS coordinates, and the drone navigates from waypoint to waypoint until reaching the destination.

class WaypointNavigator:
    def __init__(self, drone_control_interface):
        self.control = drone_control_interface
        self.current_path = []
        self.path_index = 0
        
    def set_path(self, waypoints):
        """
        Sets a new navigation path as sequence of GPS waypoints.
        """
        self.current_path = waypoints
        self.path_index = 0
    
    def navigate_path(self):
        """
        Executes navigation along the planned path.
        """
        if self.path_index >= len(self.current_path):
            return True  # Path complete
        
        current_waypoint = self.current_path[self.path_index]
        current_position = self.control.get_gps_position()
        
        # Calculate direction to waypoint
        bearing = self.calculate_bearing(current_position, current_waypoint)
        distance = self.calculate_distance(current_position, current_waypoint)
        
        # Navigate toward waypoint
        if distance > 1.0:  # More than 1 meter away
            self.control.set_heading(bearing)
            self.control.set_speed(self.calculate_optimal_speed(distance))
            return False  # Still navigating
        else:
            # Waypoint reached, advance to next
            self.path_index += 1
            return False
    
    def calculate_optimal_speed(self, distance_to_waypoint):
        """
        Calculates optimal speed based on distance to waypoint.
        Slows down when approaching waypoint for precision.
        """
        max_speed = 15.0  # meters per second
        min_speed = 2.0
        
        if distance_to_waypoint > 50.0:
            return max_speed
        elif distance_to_waypoint > 10.0:
            return max_speed * 0.7
        elif distance_to_waypoint > 5.0:
            return max_speed * 0.4
        else:
            return min_speed

Waypoint navigation is simple and reliable but does not adapt to obstacles or changing conditions. The path must be pre-planned or replanned when obstacles are encountered.

Dynamic Path Planning with Obstacle Avoidance

More sophisticated navigation systems implement dynamic path planning that continuously adapts to detected obstacles and changing conditions.

class DynamicPathPlanner:
    def __init__(self, drone_sensors, drone_control):
        self.sensors = drone_sensors
        self.control = drone_control
        self.target_location = None
        self.obstacle_map = ObstacleMap()
        
    def plan_path_to_target(self, target_gps):
        """
        Plans a path to target using A* algorithm with obstacle avoidance.
        """
        self.target_location = target_gps
        current_position = self.control.get_gps_position()
        
        # Update obstacle map with recent sensor data
        self.update_obstacle_map()
        
        # Use A* pathfinding to find optimal path
        path = self.a_star_search(current_position, target_gps)
        
        return path
    
    def update_obstacle_map(self):
        """
        Updates internal obstacle map with recent sensor detections.
        """
        current_position = self.control.get_gps_position()
        detected_obstacles = self.sensors.get_obstacle_detections()
        
        for obstacle in detected_obstacles:
            obstacle_gps = self.convert_to_gps(current_position, obstacle)
            self.obstacle_map.add_obstacle(obstacle_gps, obstacle.size)
    
    def a_star_search(self, start, goal):
        """
        Implements A* pathfinding algorithm for optimal path planning.
        """
        # Create grid representation of search space
        grid = self.create_navigation_grid(start, goal)
        
        # Initialize open and closed sets
        open_set = PriorityQueue()
        open_set.put((0, start))
        came_from = {}
        g_score = {start: 0}
        f_score = {start: self.heuristic(start, goal)}
        
        while not open_set.empty():
            current = open_set.get()[1]
            
            if self.is_at_goal(current, goal):
                return self.reconstruct_path(came_from, current)
            
            for neighbor in self.get_neighbors(current, grid):
                tentative_g_score = g_score[current] + self.distance(current, neighbor)
                
                if neighbor not in g_score or tentative_g_score < g_score[neighbor]:
                    came_from[neighbor] = current
                    g_score[neighbor] = tentative_g_score
                    f_score[neighbor] = tentative_g_score + self.heuristic(neighbor, goal)
                    
                    if neighbor not in [item[1] for item in open_set.queue]:
                        open_set.put((f_score[neighbor], neighbor))
        
        return None  # No path found
    
    def heuristic(self, position, goal):
        """
        Heuristic function for A* (straight-line distance to goal).
        """
        return self.calculate_distance(position, goal)
    
    def get_neighbors(self, position, grid):
        """
        Returns valid neighboring positions that are not obstacles.
        """
        neighbors = []
        
        # Check 8 directions (N, NE, E, SE, S, SW, W, NW)
        directions = [
            (0, 1), (1, 1), (1, 0), (1, -1),
            (0, -1), (-1, -1), (-1, 0), (-1, 1)
        ]
        
        for dx, dy in directions:
            neighbor = (position[0] + dx, position[1] + dy)
            
            if self.is_valid_position(neighbor, grid):
                neighbors.append(neighbor)
        
        return neighbors
    
    def is_valid_position(self, position, grid):
        """
        Checks if position is within bounds and not an obstacle.
        """
        x, y = position
        
        if x < 0 or x >= grid.width or y < 0 or y >= grid.height:
            return False
        
        if self.obstacle_map.is_obstacle(position):
            return False
        
        return True
    
    def navigate_with_dynamic_replanning(self):
        """
        Navigates toward target with continuous path replanning
        when new obstacles are detected.
        """
        while not self.at_target():
            # Check for new obstacles
            if self.sensors.new_obstacles_detected():
                # Replan path from current position
                current_pos = self.control.get_gps_position()
                new_path = self.plan_path_to_target(self.target_location)
                
                if new_path:
                    self.follow_path(new_path)
                else:
                    # No path available, report to base
                    self.report_path_blocked()
                    return False
            
            # Continue following current path
            self.execute_navigation_step()
        
        return True

Dynamic path planning enables drones to navigate complex environments with obstacles. The A* algorithm finds optimal paths considering both distance and obstacle avoidance. When new obstacles are detected during navigation, the path is replanned from the current position to maintain progress toward the goal.

The LLM can enhance path planning by reasoning about obstacle types and suggesting appropriate avoidance strategies. For example, when encountering a flock of birds, the LLM might recommend waiting for them to pass rather than attempting to navigate around them. When encountering a building, the LLM might suggest flying over it if altitude permits, or routing around it if altitude is constrained.

Energy-Aware Path Planning

For long-duration missions, energy efficiency becomes critical. Energy-aware path planning optimizes routes to minimize energy consumption while still completing mission objectives.

class EnergyAwarePathPlanner:
    def __init__(self, drone_specs):
        self.drone_specs = drone_specs
        self.wind_model = WindModel()
        
    def plan_energy_optimal_path(self, start, goal, battery_level):
        """
        Plans path that minimizes energy consumption.
        Considers altitude, wind, and route efficiency.
        """
        # Generate candidate paths with different altitude profiles
        candidate_paths = []
        
        # Low altitude path (more obstacles, less wind resistance)
        low_path = self.plan_path_at_altitude(start, goal, altitude=50)
        low_energy = self.estimate_path_energy(low_path, altitude=50)
        candidate_paths.append((low_path, low_energy))
        
        # Medium altitude path (balanced)
        med_path = self.plan_path_at_altitude(start, goal, altitude=100)
        med_energy = self.estimate_path_energy(med_path, altitude=100)
        candidate_paths.append((med_path, med_energy))
        
        # High altitude path (fewer obstacles, more wind)
        high_path = self.plan_path_at_altitude(start, goal, altitude=150)
        high_energy = self.estimate_path_energy(high_path, altitude=150)
        candidate_paths.append((high_path, high_energy))
        
        # Select path with lowest energy consumption that is feasible
        candidate_paths.sort(key=lambda x: x[1])
        
        for path, energy in candidate_paths:
            if energy < battery_level * 0.8:  # Leave 20% margin
                return path
        
        return None  # No feasible path with current battery
    
    def estimate_path_energy(self, path, altitude):
        """
        Estimates energy consumption for following a path.
        Considers distance, altitude changes, and wind conditions.
        """
        total_energy = 0.0
        
        for i in range(len(path) - 1):
            segment_start = path[i]
            segment_end = path[i + 1]
            
            # Horizontal distance energy
            horizontal_distance = self.calculate_horizontal_distance(
                segment_start, segment_end)
            horizontal_energy = horizontal_distance * self.drone_specs.energy_per_km
            
            # Altitude change energy
            altitude_change = abs(segment_end.altitude - segment_start.altitude)
            climb_energy = altitude_change * self.drone_specs.energy_per_meter_climb
            
            # Wind resistance energy
            wind_vector = self.wind_model.get_wind_at_location(
                segment_start, altitude)
            wind_energy = self.calculate_wind_energy_cost(
                segment_start, segment_end, wind_vector)
            
            total_energy += horizontal_energy + climb_energy + wind_energy
        
        return total_energy
    
    def calculate_wind_energy_cost(self, start, end, wind_vector):
        """
        Calculates additional energy cost due to wind resistance.
        Headwinds increase energy, tailwinds decrease energy.
        """
        flight_vector = self.calculate_vector(start, end)
        
        # Dot product determines if wind is headwind or tailwind
        wind_alignment = self.dot_product(flight_vector, wind_vector)
        
        wind_speed = self.magnitude(wind_vector)
        
        if wind_alignment < 0:  # Headwind
            energy_cost = abs(wind_alignment) * wind_speed * 0.1
        else:  # Tailwind
            energy_cost = -wind_alignment * wind_speed * 0.05
        
        return energy_cost

Energy-aware path planning is particularly important for missions covering large areas or requiring extended operation times. By considering factors such as wind conditions and altitude optimization, drones can extend their operational range and complete more tasks per battery charge.

Collision Avoidance and Swarm Coordination

When multiple drones operate in close proximity, collision avoidance becomes critical. The system must ensure drones can work cooperatively without interfering with each other.

Reactive Collision Avoidance

Reactive collision avoidance uses sensor data to detect nearby drones and obstacles, then adjusts trajectory to maintain safe separation.

class CollisionAvoidanceSystem:
    def __init__(self, drone_id, safety_radius=5.0):
        self.drone_id = drone_id
        self.safety_radius = safety_radius  # meters
        self.proximity_sensors = ProximitySensorArray()
        
    def check_collision_risk(self):
        """
        Checks for potential collisions with nearby objects.
        """
        detections = self.proximity_sensors.get_detections()
        
        collision_risks = []
        for detection in detections:
            if detection.distance < self.safety_radius:
                collision_risks.append(detection)
        
        return collision_risks
    
    def execute_avoidance_maneuver(self, collision_risks):
        """
        Executes maneuver to avoid detected collision risks.
        """
        if not collision_risks:
            return None
        
        # Calculate avoidance vector away from all threats
        avoidance_vector = self.calculate_avoidance_vector(collision_risks)
        
        # Determine if vertical or horizontal avoidance is better
        if self.should_avoid_vertically(collision_risks):
            return self.vertical_avoidance_maneuver(avoidance_vector)
        else:
            return self.horizontal_avoidance_maneuver(avoidance_vector)
    
    def calculate_avoidance_vector(self, collision_risks):
        """
        Calculates vector pointing away from all collision risks.
        """
        total_vector = Vector3D(0, 0, 0)
        
        for risk in collision_risks:
            # Vector pointing away from threat
            away_vector = self.vector_from_to(risk.position, 
                                             self.current_position)
            
            # Weight by proximity (closer threats have more influence)
            weight = 1.0 / (risk.distance + 0.1)
            
            total_vector = total_vector.add(away_vector.scale(weight))
        
        return total_vector.normalize()
    
    def should_avoid_vertically(self, collision_risks):
        """
        Determines if vertical avoidance is preferable to horizontal.
        """
        # Prefer vertical avoidance if obstacles are primarily horizontal
        # (e.g., other drones at similar altitude)
        
        altitude_variance = self.calculate_altitude_variance(collision_risks)
        
        # If obstacles are at similar altitude, avoid vertically
        return altitude_variance < 2.0
    
    def vertical_avoidance_maneuver(self, avoidance_vector):
        """
        Executes vertical avoidance by changing altitude.
        """
        if avoidance_vector.z > 0:
            # Climb to avoid
            return {'action': 'climb', 'rate': 2.0}  # 2 m/s climb
        else:
            # Descend to avoid
            return {'action': 'descend', 'rate': 2.0}
    
    def horizontal_avoidance_maneuver(self, avoidance_vector):
        """
        Executes horizontal avoidance by changing heading.
        """
        avoidance_heading = self.vector_to_heading(avoidance_vector)
        
        return {
            'action': 'turn',
            'heading': avoidance_heading,
            'speed': 'reduce'  # Slow down while avoiding
        }

Reactive collision avoidance provides immediate response to detected threats but can lead to oscillating behavior if multiple drones react to each other simultaneously. To prevent this, drones can implement priority-based avoidance where lower-ID drones yield to higher-ID drones, ensuring consistent avoidance behavior.

Coordinated Trajectory Planning

For more sophisticated coordination, drones can share their planned trajectories and coordinate to avoid conflicts before they occur.

class CoordinatedTrajectoryPlanner:
    def __init__(self, drone_id, communication_network):
        self.drone_id = drone_id
        self.network = communication_network
        self.planned_trajectory = []
        self.other_drone_trajectories = {}
        
    def plan_coordinated_trajectory(self, goal_location):
        """
        Plans trajectory to goal while coordinating with other drones.
        """
        # Generate initial trajectory
        initial_trajectory = self.generate_trajectory(
            self.current_location, goal_location)
        
        # Check for conflicts with other drone trajectories
        conflicts = self.detect_trajectory_conflicts(initial_trajectory)
        
        if not conflicts:
            # No conflicts, use initial trajectory
            self.planned_trajectory = initial_trajectory
            self.broadcast_trajectory(initial_trajectory)
            return initial_trajectory
        
        # Resolve conflicts through trajectory adjustment
        adjusted_trajectory = self.resolve_trajectory_conflicts(
            initial_trajectory, conflicts)
        
        self.planned_trajectory = adjusted_trajectory
        self.broadcast_trajectory(adjusted_trajectory)
        
        return adjusted_trajectory
    
    def detect_trajectory_conflicts(self, trajectory):
        """
        Checks if trajectory conflicts with other drone trajectories.
        """
        conflicts = []
        
        for other_drone_id, other_trajectory in self.other_drone_trajectories.items():
            conflict_points = self.find_conflict_points(
                trajectory, other_trajectory)
            
            if conflict_points:
                conflicts.append({
                    'drone_id': other_drone_id,
                    'conflict_points': conflict_points
                })
        
        return conflicts
    
    def find_conflict_points(self, trajectory1, trajectory2):
        """
        Finds points where two trajectories come too close in space-time.
        """
        conflict_points = []
        
        # Sample both trajectories at regular time intervals
        for t in range(0, min(len(trajectory1), len(trajectory2))):
            pos1 = trajectory1[t]
            pos2 = trajectory2[t]
            
            distance = self.calculate_distance(pos1, pos2)
            
            if distance < self.safety_radius:
                conflict_points.append({
                    'time': t,
                    'position1': pos1,
                    'position2': pos2,
                    'distance': distance
                })
        
        return conflict_points
    
    def resolve_trajectory_conflicts(self, trajectory, conflicts):
        """
        Adjusts trajectory to resolve conflicts with other drones.
        Uses priority-based resolution: lower ID yields to higher ID.
        """
        adjusted_trajectory = trajectory.copy()
        
        for conflict in conflicts:
            if self.drone_id < conflict['drone_id']:
                # This drone has lower priority, must adjust
                adjusted_trajectory = self.adjust_trajectory_for_conflict(
                    adjusted_trajectory, conflict)
        
        return adjusted_trajectory
    
    def adjust_trajectory_for_conflict(self, trajectory, conflict):
        """
        Modifies trajectory to avoid a specific conflict.
        """
        conflict_time = conflict['conflict_points'][0]['time']
        
        # Insert a detour waypoint before the conflict
        detour_waypoint = self.calculate_detour_waypoint(
            trajectory[conflict_time],
            conflict['conflict_points'][0]['position2'])
        
        # Insert detour into trajectory
        new_trajectory = (trajectory[:conflict_time] + 
                         [detour_waypoint] + 
                         trajectory[conflict_time:])
        
        return new_trajectory
    
    def broadcast_trajectory(self, trajectory):
        """
        Broadcasts planned trajectory to other drones for coordination.
        """
        message = {
            'type': 'trajectory_announcement',
            'drone_id': self.drone_id,
            'trajectory': trajectory,
            'timestamp': time.time()
        }
        
        self.network.broadcast(message)
    
    def receive_trajectory_update(self, message):
        """
        Receives and stores trajectory update from another drone.
        """
        other_drone_id = message['drone_id']
        other_trajectory = message['trajectory']
        
        self.other_drone_trajectories[other_drone_id] = other_trajectory

Coordinated trajectory planning enables smoother swarm operation with fewer last-minute avoidance maneuvers. By sharing planned trajectories, drones can anticipate potential conflicts and adjust their plans proactively.

LLM Integration for Intelligent Decision Making

Integrating Large Language Models into drone agents enables sophisticated reasoning about mission objectives, environmental conditions, and tactical decisions.

Task Interpretation and Planning

The LLM can interpret complex task descriptions and generate detailed execution plans.

class LLMTaskInterpreter:
    def __init__(self, llm_interface):
        self.llm = llm_interface
        
    def interpret_task(self, task_description):
        """
        Uses LLM to interpret natural language task description
        and generate structured execution plan.
        """
        prompt = f"""
        You are an autonomous drone agent. You have received the following task:
        
        Task: {task_description}
        
        Your capabilities include:
        - Flying to GPS coordinates
        - Adjusting altitude (0-200 meters)
        - Capturing photos and video
        - Detecting objects using computer vision
        - Avoiding obstacles
        
        Generate a detailed execution plan for this task. Include:
        1. Key waypoints to visit (GPS coordinates if specified)
        2. Required altitude at each waypoint
        3. Camera settings and capture requirements
        4. Success criteria for task completion
        5. Potential challenges and mitigation strategies
        
        Format your response as structured JSON.
        """
        
        response = self.llm.generate_response(prompt)
        execution_plan = self.parse_execution_plan(response)
        
        return execution_plan
    
    def parse_execution_plan(self, llm_response):
        """
        Parses LLM response into structured execution plan.
        """
        try:
            plan = json.loads(llm_response)
            
            return {
                'waypoints': plan.get('waypoints', []),
                'altitude_profile': plan.get('altitude_profile', {}),
                'camera_settings': plan.get('camera_settings', {}),
                'success_criteria': plan.get('success_criteria', []),
                'challenges': plan.get('challenges', [])
            }
        except json.JSONDecodeError:
            # Fallback to text parsing if JSON parsing fails
            return self.parse_text_plan(llm_response)

The LLM can also help decompose complex missions into subtasks. For example, given the mission "Inspect all bridges in the area for structural defects," the LLM can identify that this requires first locating all bridges, then planning inspection routes around each bridge structure, then analyzing captured images for signs of damage.

Adaptive Decision Making

During mission execution, the LLM enables drones to make intelligent decisions when encountering unexpected situations.

class LLMDecisionMaker:
    def __init__(self, llm_interface, drone_state):
        self.llm = llm_interface
        self.drone_state = drone_state
        
    def make_decision(self, situation_description):
        """
        Uses LLM to make decision about how to handle a situation.
        """
        context = self.build_context_description()
        
        prompt = f"""
        You are an autonomous drone currently executing a mission.
        
        Current Context:
        {context}
        
        Situation:
        {situation_description}
        
        What should you do? Consider:
        - Mission objectives and priorities
        - Safety constraints
        - Energy limitations
        - Alternative approaches
        
        Provide your decision and reasoning.
        """
        
        response = self.llm.generate_response(prompt)
        decision = self.parse_decision(response)
        
        return decision
    
    def build_context_description(self):
        """
        Builds description of current drone state and mission context.
        """
        context = f"""
        Current Location: {self.drone_state.gps_location}
        Battery Level: {self.drone_state.battery_level}%
        Current Task: {self.drone_state.current_task.description}
        Distance to Base: {self.drone_state.distance_to_base} km
        Altitude: {self.drone_state.altitude} meters
        """
        
        return context
    
    def handle_unexpected_obstacle(self, obstacle_description):
        """
        Uses LLM to decide how to handle an unexpected obstacle.
        """
        situation = f"""
        An unexpected obstacle has been detected: {obstacle_description}
        
        The obstacle is blocking the planned route to the next waypoint.
        
        Options:
        1. Increase altitude and fly over the obstacle
        2. Navigate around the obstacle (adds distance)
        3. Wait for the obstacle to move (if it appears to be temporary)
        4. Abort current task and return to base
        
        Which option should be chosen?
        """
        
        decision = self.make_decision(situation)
        return decision
    
    def handle_low_battery_scenario(self):
        """
        Uses LLM to decide how to handle low battery situation.
        """
        situation = f"""
        Battery level is at {self.drone_state.battery_level}%.
        
        Current task is {self.drone_state.current_task.completion_percentage}% complete.
        Distance to base: {self.drone_state.distance_to_base} km
        Estimated energy to return: {self.drone_state.estimated_return_energy}%
        
        Should the drone:
        1. Continue current task and risk not making it back
        2. Abort task immediately and return to base
        3. Complete a partial task (e.g., capture some but not all required images)
        
        What is the best course of action?
        """
        
        decision = self.make_decision(situation)
        return decision

The LLM enables drones to reason about trade-offs and make context-appropriate decisions. For example, when battery is low, the LLM can weigh the importance of completing the current task against the risk of not making it back to base, considering factors such as task priority, mission progress, and availability of other drones to complete the task.

Collaborative Reasoning

When multiple drones encounter complex situations, they can use their LLMs to collaborate on problem-solving.

class CollaborativeReasoningSystem:
    def __init__(self, drone_id, llm_interface, communication_network):
        self.drone_id = drone_id
        self.llm = llm_interface
        self.network = communication_network
        
    def initiate_collaborative_reasoning(self, problem_description):
        """
        Initiates collaborative problem-solving session with nearby drones.
        """
        # Generate initial analysis
        my_analysis = self.analyze_problem(problem_description)
        
        # Request input from other drones
        collaboration_request = {
            'type': 'reasoning_request',
            'initiator': self.drone_id,
            'problem': problem_description,
            'my_analysis': my_analysis
        }
        
        self.network.broadcast(collaboration_request)
        
        # Collect responses
        responses = self.collect_reasoning_responses(timeout=5.0)
        
        # Synthesize collective solution
        collective_solution = self.synthesize_solution(
            my_analysis, responses)
        
        return collective_solution
    
    def analyze_problem(self, problem_description):
        """
        Uses LLM to analyze a problem and propose solutions.
        """
        prompt = f"""
        Problem: {problem_description}
        
        Analyze this problem and propose potential solutions.
        Consider multiple perspectives and evaluate trade-offs.
        """
        
        analysis = self.llm.generate_response(prompt)
        return analysis
    
    def synthesize_solution(self, my_analysis, other_analyses):
        """
        Uses LLM to synthesize insights from multiple drone analyses
        into a coherent solution.
        """
        combined_input = f"""
        My Analysis:
        {my_analysis}
        
        Other Drone Analyses:
        """
        
        for drone_id, analysis in other_analyses.items():
            combined_input += f"\nDrone {drone_id}: {analysis}\n"
        
        synthesis_prompt = f"""
        {combined_input}
        
        Synthesize these different perspectives into a coherent solution.
        Identify common themes, resolve contradictions, and recommend
        the best course of action based on the collective analysis.
        """
        
        solution = self.llm.generate_response(synthesis_prompt)
        return solution

Collaborative reasoning enables the swarm to leverage collective intelligence. When facing complex or ambiguous situations, drones can pool their analytical capabilities to arrive at better solutions than any individual drone could generate alone.

Mission Scenarios and Implementation Examples

Bridge Inspection Mission

A bridge inspection mission requires systematic coverage of bridge structures to identify potential defects such as cracks, corrosion, or structural deformation.

class BridgeInspectionMission:
    def __init__(self, bridge_locations, drone_swarm, base_station):
        self.bridges = bridge_locations
        self.swarm = drone_swarm
        self.base = base_station
        
    def execute_mission(self):
        """
        Coordinates swarm to inspect all bridges in the area.
        """
        # Generate inspection tasks for each bridge
        inspection_tasks = []
        for bridge in self.bridges:
            tasks = self.generate_bridge_inspection_tasks(bridge)
            inspection_tasks.extend(tasks)
        
        # Assign tasks to base station queue
        self.base.add_tasks(inspection_tasks)
        
        # Launch drones
        for drone in self.swarm:
            drone.start_mission()
        
        # Monitor progress
        while not self.all_tasks_complete():
            self.monitor_progress()
            time.sleep(10)
        
        # Collect results
        results = self.collect_inspection_results()
        return results
    
    def generate_bridge_inspection_tasks(self, bridge):
        """
        Generates detailed inspection tasks for a single bridge.
        Creates waypoints around bridge structure for comprehensive coverage.
        """
        tasks = []
        
        # Get bridge dimensions and structure type
        bridge_length = bridge.length
        bridge_width = bridge.width
        bridge_center = bridge.gps_location
        
        # Generate inspection waypoints around bridge perimeter
        waypoints = []
        
        # North side inspection points
        for i in range(0, int(bridge_length), 10):
            waypoint = self.calculate_offset_position(
                bridge_center, north=i, east=-bridge_width/2)
            waypoints.append(waypoint)
        
        # South side inspection points
        for i in range(0, int(bridge_length), 10):
            waypoint = self.calculate_offset_position(
                bridge_center, north=i, east=bridge_width/2)
            waypoints.append(waypoint)
        
        # Underside inspection points (if accessible)
        if bridge.has_clearance_below:
            for i in range(0, int(bridge_length), 10):
                waypoint = self.calculate_offset_position(
                    bridge_center, north=i, altitude=-5)
                waypoints.append(waypoint)
        
        # Create task for each inspection segment
        for i in range(0, len(waypoints), 5):
            segment_waypoints = waypoints[i:i+5]
            
            task = InspectionTask(
                task_id=f"bridge_{bridge.id}_segment_{i}",
                task_type='inspect_structure',
                target_location=bridge_center,
                inspection_waypoints=segment_waypoints,
                camera_requirements={
                    'resolution': 'high',
                    'focus_distance': 10,
                    'capture_interval': 2
                },
                analysis_requirements={
                    'detect': ['cracks', 'corrosion', 'deformation'],
                    'min_defect_size': 0.01  # 1 cm
                }
            )
            
            tasks.append(task)
        
        return tasks
    
    def monitor_progress(self):
        """
        Monitors mission progress and handles issues.
        """
        for drone in self.swarm:
            status = drone.get_status()
            
            if status.has_findings:
                # Drone detected potential defects
                findings = status.findings
                self.process_findings(drone.drone_id, findings)
            
            if status.needs_assistance:
                # Drone encountered problem
                self.handle_drone_issue(drone)
    
    def process_findings(self, drone_id, findings):
        """
        Processes defect findings reported by drones.
        """
        for finding in findings:
            if finding.severity == 'high':
                # High severity finding requires immediate attention
                self.alert_operators(finding)
                
                # Assign additional drone to capture more detailed images
                self.assign_detailed_inspection(finding.location)

The bridge inspection mission demonstrates systematic task decomposition, where the high-level objective is broken down into specific inspection segments that can be distributed across the swarm. Each drone captures images from designated waypoints, and the LLM analyzes images for signs of structural defects. When defects are detected, the system can dynamically assign additional drones to capture more detailed documentation.

Search and Rescue Mission

A search and rescue mission requires covering a large area to locate missing persons, such as someone who has gone overboard from a ship.

class SearchAndRescueMission:
    def __init__(self, search_area, target_description, drone_swarm):
        self.search_area = search_area
        self.target = target_description
        self.swarm = drone_swarm
        self.search_grid = self.generate_search_grid()
        
    def execute_mission(self):
        """
        Coordinates swarm to search area for missing person.
        """
        # Divide search area into grid cells
        grid_cells = self.divide_search_area()
        
        # Assign grid cells to drones
        assignments = self.assign_search_cells(grid_cells)
        
        # Execute search
        for drone, cells in assignments.items():
            search_task = self.create_search_task(cells)
            drone.assign_task(search_task)
            drone.start_mission()
        
        # Monitor for target detection
        while not self.target_found and not self.search_complete():
            self.monitor_search_progress()
            time.sleep(5)
        
        if self.target_found:
            return self.target_location
        else:
            return None
    
    def generate_search_grid(self):
        """
        Generates systematic search grid covering the search area.
        """
        grid = SearchGrid(self.search_area)
        
        # Calculate optimal grid cell size based on drone camera coverage
        camera_fov = 60  # degrees
        altitude = 50  # meters
        
        # Calculate ground coverage at this altitude
        coverage_width = 2 * altitude * math.tan(math.radians(camera_fov / 2))
        
        # Create grid with 20% overlap for redundancy
        cell_size = coverage_width * 0.8
        
        grid.generate_cells(cell_size)
        return grid
    
    def assign_search_cells(self, grid_cells):
        """
        Assigns grid cells to drones to minimize total search time.
        """
        assignments = {}
        num_drones = len(self.swarm)
        
        # Sort cells by priority (e.g., last known location has highest priority)
        prioritized_cells = self.prioritize_cells(grid_cells)
        
        # Distribute cells evenly across drones
        cells_per_drone = len(prioritized_cells) // num_drones
        
        for i, drone in enumerate(self.swarm):
            start_idx = i * cells_per_drone
            end_idx = start_idx + cells_per_drone if i < num_drones - 1 else len(prioritized_cells)
            
            assignments[drone] = prioritized_cells[start_idx:end_idx]
        
        return assignments
    
    def create_search_task(self, grid_cells):
        """
        Creates search task for a set of grid cells.
        """
        # Generate optimal path through grid cells
        search_path = self.calculate_search_path(grid_cells)
        
        task = SearchTask(
            task_type='search_area',
            search_area=grid_cells,
            search_path=search_path,
            target_description=self.target,
            search_altitude=50,
            search_speed=5,  # m/s - slow enough for detailed observation
            detection_requirements={
                'use_thermal': True if self.target.is_person else False,
                'use_visual': True,
                'confidence_threshold': 0.7
            }
        )
        
        return task
    
    def monitor_search_progress(self):
        """
        Monitors search progress and handles target detections.
        """
        for drone in self.swarm:
            status = drone.get_status()
            
            if status.possible_target_detected:
                # Drone detected possible target
                self.investigate_detection(drone, status.detection_info)
    
    def investigate_detection(self, detecting_drone, detection_info):
        """
        Investigates a possible target detection with additional drones.
        """
        # Use LLM to assess detection confidence
        assessment_prompt = f"""
        A drone has detected a possible target during search and rescue.
        
        Target Description: {self.target}
        Detection Information: {detection_info}
        
        Assess the likelihood this is the actual target.
        Should we:
        1. Confirm this is the target and alert rescue teams
        2. Send additional drones for closer investigation
        3. Dismiss as false positive and continue search
        """
        
        assessment = detecting_drone.llm.generate_response(assessment_prompt)
        
        if 'confirm' in assessment.lower():
            self.target_found = True
            self.target_location = detection_info.location
            self.alert_rescue_teams(detection_info)
        elif 'additional drones' in assessment.lower():
            self.assign_verification_drones(detection_info.location)

The search and rescue mission demonstrates adaptive search strategies and collaborative verification. When a drone detects a possible target, the LLM assesses the detection confidence, and additional drones can be dispatched to verify the finding before alerting rescue teams. This reduces false positives while ensuring genuine targets are not missed.

Surveillance Mission

A surveillance mission requires continuous monitoring of a specific location to gather intelligence about enemy positions or activities.

class SurveillanceMission:
    def __init__(self, target_location, drone_swarm, base_station):
        self.target = target_location
        self.swarm = drone_swarm
        self.base = base_station
        self.observation_posts = self.calculate_observation_positions()
        
    def execute_mission(self):
        """
        Establishes persistent surveillance of target location.
        """
        # Assign drones to observation posts
        for drone, position in zip(self.swarm, self.observation_posts):
            surveillance_task = self.create_surveillance_task(position)
            drone.assign_task(surveillance_task)
            drone.start_mission()
        
        # Implement rotation schedule for continuous coverage
        self.manage_surveillance_rotation()
    
    def calculate_observation_positions(self):
        """
        Calculates optimal observation positions around target.
        Positions provide overlapping coverage from different angles.
        """
        positions = []
        
        # Create observation posts in a circle around target
        num_positions = min(len(self.swarm), 8)
        radius = 500  # meters from target
        altitude = 100  # meters
        
        for i in range(num_positions):
            angle = (2 * math.pi * i) / num_positions
            
            offset_north = radius * math.cos(angle)
            offset_east = radius * math.sin(angle)
            
            position = self.calculate_offset_position(
                self.target, 
                north=offset_north, 
                east=offset_east,
                altitude=altitude
            )
            
            positions.append(position)
        
        return positions
    
    def create_surveillance_task(self, observation_position):
        """
        Creates surveillance task for an observation position.
        """
        task = SurveillanceTask(
            task_type='surveillance',
            observation_position=observation_position,
            target_location=self.target,
            duration=1800,  # 30 minutes
            capture_requirements={
                'video': True,
                'photo_interval': 30,  # seconds
                'resolution': 'high',
                'zoom': 'auto'
            },
            analysis_requirements={
                'detect_movement': True,
                'count_personnel': True,
                'identify_vehicles': True,
                'track_activities': True
            }
        )
        
        return task
    
    def manage_surveillance_rotation(self):
        """
        Manages rotation of drones to maintain continuous surveillance
        while allowing drones to return for recharging.
        """
        while self.mission_active:
            for drone in self.swarm:
                status = drone.get_status()
                
                if status.battery_level < 30:
                    # Drone needs to return for recharging
                    replacement = self.find_replacement_drone()
                    
                    if replacement:
                        # Send replacement to observation post
                        self.execute_handoff(drone, replacement)
                    else:
                        # No replacement available, adjust coverage
                        self.adjust_coverage_for_missing_drone(drone)
            
            time.sleep(60)  # Check every minute
    
    def execute_handoff(self, departing_drone, replacement_drone):
        """
        Coordinates handoff between departing and replacement drone
        to maintain continuous coverage.
        """
        observation_post = departing_drone.current_task.observation_position
        
        # Send replacement to observation post
        replacement_task = self.create_surveillance_task(observation_post)
        replacement_drone.assign_task(replacement_task)
        replacement_drone.start_mission()
        
        # Wait for replacement to arrive before recalling departing drone
        while not replacement_drone.at_position(observation_post):
            time.sleep(1)
        
        # Recall departing drone
        departing_drone.return_to_base()

The surveillance mission demonstrates persistent monitoring with drone rotation to maintain continuous coverage despite energy constraints. The system coordinates handoffs between drones to ensure observation posts are never left unattended, and the LLM analyzes captured footage to identify significant activities or changes at the target location.

Conclusion

Coordinating autonomous drone swarms for complex mission objectives requires sophisticated integration of navigation, communication, task allocation, and decision-making capabilities. By leveraging Large Language Models as intelligent agents, both at the individual drone level and at the base station orchestrator level, swarm systems can interpret complex mission objectives, adapt to unexpected situations, and collaborate effectively to achieve goals that would be impossible for individual drones or simpler control systems.

The architectural choice between centralized, distributed, or hybrid control depends on mission requirements, with each approach offering distinct advantages. Centralized control provides optimal global coordination, distributed control offers maximum resilience, and hybrid approaches balance these benefits. Effective collision avoidance and trajectory coordination ensure safe operation when multiple drones work in close proximity.

LLM integration enables natural language task interpretation, adaptive decision-making in response to unexpected situations, and collaborative reasoning when multiple drones encounter complex problems. This intelligence layer transforms the swarm from a collection of simple automated systems into a truly autonomous collective capable of sophisticated mission execution.

The implementation examples demonstrate how these concepts apply to real-world scenarios such as infrastructure inspection, search and rescue operations, and surveillance missions. In each case, the combination of systematic task decomposition, intelligent allocation, adaptive execution, and continuous monitoring enables the swarm to efficiently accomplish objectives that would require extensive human coordination and oversight with traditional systems.

As drone technology continues to advance and LLM capabilities expand, we can expect increasingly sophisticated swarm coordination systems capable of handling ever more complex and dynamic mission scenarios with minimal human intervention.

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.