Scoping Implementation: Recursive Functions and Exception-Based Level Control
This document describes the technical implementation of ggblab’s core architectural pattern: using recursive function calls to create scopes and try-except blocks to control dependency levels in geometric scene construction.
Architectural Foundation
The Core Insight
The only practical way to create scopes in Python is through function definition
Python’s scoping rules are limited:
✅ Functions (
def,lambda) create new scopes✅ Classes create scopes (but overkill for most cases)
✅ Modules create scopes (file-level isolation)
❌ try-except does NOT create scopes
❌ with statements do NOT create scopes
❌ if/for/while do NOT create scopes
ggblab’s approach: Use recursive function calls to mirror the hierarchical dependency structure of geometric constructions, where each construction step creates a new scope level.
Exception Propagation Across Recursive Calls
Python exceptions can hook raised exceptions even across recursive function calls
When an exception is raised in a deep recursive call, Python unwinds the call stack until it finds a matching except block:
def level_3():
raise ValueError("Error at deepest level")
def level_2():
return level_3() # No try-except → propagates upward
def level_1():
try:
return level_2()
except ValueError as e:
# Catches error from level_3
print(f"Caught at level 1: {e}")
return "recovered"
Key properties:
Exceptions traverse the entire call stack
Any ancestor can intercept the exception
Unhandled exceptions propagate to the top level
This models dependency chain validation: if a deeply nested object fails, all ancestors are notified
Geometric Scene ↔ Function Call Isomorphism
Mapping Structure
Geometric Concept |
Implementation Pattern |
|---|---|
Base objects (points A, B) |
Top-level function scope (parameters) |
Derived objects (line AB) |
Nested function call: |
Transitive dependencies (perpendicular L from AB) |
Recursive call: |
Dependency validation |
|
Scope level |
Call stack depth |
Scope chain |
Call stack trace |
Invalid dependency |
Exception raised and propagated upward |
Example: Isosceles Triangle Construction
Geometric construction:
Given: Points A, B
1. Construct line AB
2. Construct midpoint M of AB
3. Construct perpendicular L through M
4. Construct point C on L
5. Triangle ABC is isosceles
Implementation with scoped functions:
class GeometricScene:
"""Manages geometric construction with scope-based dependency tracking."""
def __init__(self):
self.objects = {} # Global object registry
self.scope_stack = [] # Track current scope depth
def construct_isosceles_triangle(self, A, B):
"""Top-level construction: establishes base scope."""
try:
# Level 0: Base objects (global scope)
self._register_base_objects(A, B)
# Level 1: Derived objects depending on base
AB = self._construct_line(A, B)
M = self._construct_midpoint(A, B)
# Level 2: Objects depending on Level 1
L = self._construct_perpendicular(M, AB)
# Level 3: Objects depending on Level 2
C = self._construct_point_on_line(L)
# Verification: property check across all scopes
return self._verify_isosceles(A, B, C)
except DependencyError as e:
# Catch any dependency chain failure
print(f"Construction failed: {e}")
return None
def _register_base_objects(self, A, B):
"""Level 0: Register base points (global scope)."""
self.scope_stack.append("base")
try:
self.objects['A'] = A
self.objects['B'] = B
finally:
self.scope_stack.pop()
def _construct_line(self, A, B):
"""Level 1: Create line from two points (nested scope)."""
self.scope_stack.append("line_AB")
try:
if A == B:
raise DependencyError(
f"Cannot create line: points A and B are identical. "
f"Scope chain: {' → '.join(self.scope_stack)}"
)
line = Line(A, B)
self.objects['AB'] = line
return line
finally:
self.scope_stack.pop()
def _construct_midpoint(self, A, B):
"""Level 1: Create midpoint (sibling scope to line_AB)."""
self.scope_stack.append("midpoint_M")
try:
# This function creates its own scope
# Independent of _construct_line but same level
midpoint = Midpoint(A, B)
self.objects['M'] = midpoint
return midpoint
finally:
self.scope_stack.pop()
def _construct_perpendicular(self, M, AB):
"""Level 2: Create perpendicular line (depends on Level 1)."""
self.scope_stack.append("perpendicular_L")
try:
# Validate dependencies from parent scopes
if AB not in self.objects.values():
raise DependencyError(
f"Perpendicular requires line AB from parent scope. "
f"Current scope chain: {' → '.join(self.scope_stack)}"
)
perp = PerpendicularLine(M, AB)
self.objects['L'] = perp
return perp
finally:
self.scope_stack.pop()
def _construct_point_on_line(self, L):
"""Level 3: Create point constrained to line (deeply nested)."""
self.scope_stack.append("point_C")
try:
# Recursive dependency validation
if not self._validate_dependency_chain(L):
raise DependencyError(
f"Point C requires valid line L from ancestor scopes. "
f"Scope chain: {' → '.join(self.scope_stack)}"
)
point = PointOnLine(L)
self.objects['C'] = point
return point
finally:
self.scope_stack.pop()
def _validate_dependency_chain(self, obj):
"""Recursively validate all dependencies exist."""
try:
# Check if object is registered
if obj not in self.objects.values():
return False
# Recursively check dependencies
for dep in obj.dependencies:
if not self._validate_dependency_chain(dep):
return False
return True
except AttributeError:
# Object has no dependencies attribute
return True
def _verify_isosceles(self, A, B, C):
"""Verification scope: read from all levels."""
self.scope_stack.append("verify")
try:
# Access objects from multiple scopes
dist_AC = distance(A, C)
dist_BC = distance(B, C)
if abs(dist_AC - dist_BC) < 1e-10:
return True
else:
raise VerificationError(
f"Triangle is not isosceles: "
f"|AC| = {dist_AC:.6f}, |BC| = {dist_BC:.6f}"
)
finally:
self.scope_stack.pop()
class DependencyError(Exception):
"""Raised when geometric dependency chain is broken."""
pass
class VerificationError(Exception):
"""Raised when geometric property verification fails."""
pass
Execution Flow
When construct_isosceles_triangle(A, B) is called:
Scope Stack Evolution:
├─ [] (empty)
├─ ['base'] → Register A, B
├─ ['base', 'line_AB'] → Construct AB
├─ ['base', 'midpoint_M'] → Construct M
├─ ['base', 'perpendicular_L'] → Construct L (depends on M, AB)
├─ ['base', 'point_C'] → Construct C (depends on L)
└─ ['base', 'verify'] → Verify triangle property
Scope depth = dependency level. Each function creates a new scope, mirroring geometric hierarchy.
Exception-Based Level Control
Propagation Strategy
Exceptions propagate upward through the call stack, allowing higher-level scopes to handle errors from nested constructions:
def top_level_construction():
try:
result = mid_level_construction()
except DependencyError as e:
# Handle dependency failures from any nested level
log_error(f"Construction chain broken: {e}")
return fallback_construction()
except VerificationError as e:
# Handle property verification failures
log_warning(f"Property not satisfied: {e}")
return partial_result()
def mid_level_construction():
# No try-except → propagates errors upward
derived_obj = deep_level_construction()
return process(derived_obj)
def deep_level_construction():
if invalid_state():
raise DependencyError("Cannot proceed: invalid base objects")
return construct_object()
Benefits:
Centralized error handling: Top-level function decides recovery strategy
Scope transparency: Intermediate functions don’t need to know about error handling
Dependency chain tracing: Exception message includes full scope stack
Clean separation: Construction logic vs. error recovery logic
Error Recovery Patterns
Pattern 1: Retry with Modified Parameters
def construct_with_retry(self, A, B, max_attempts=3):
"""Attempt construction, adjusting parameters on failure."""
for attempt in range(max_attempts):
try:
return self.construct_isosceles_triangle(A, B)
except DependencyError as e:
if "identical" in str(e) and attempt < max_attempts - 1:
# Points are identical; perturb slightly
B = (B[0] + 0.01, B[1] + 0.01)
continue
raise # Give up after max attempts
Pattern 2: Fallback to Simpler Construction
def construct_with_fallback(self, A, B):
"""Try complex construction, fall back to simple version."""
try:
# Attempt full construction with perpendicular
return self.construct_isosceles_triangle(A, B)
except DependencyError:
# Fall back to simple triangle
return self.construct_simple_triangle(A, B)
Pattern 3: Partial Results
def construct_partial(self, A, B):
"""Return partial construction if full construction fails."""
partial_objects = {}
try:
AB = self._construct_line(A, B)
partial_objects['line'] = AB
M = self._construct_midpoint(A, B)
partial_objects['midpoint'] = M
L = self._construct_perpendicular(M, AB)
partial_objects['perpendicular'] = L
C = self._construct_point_on_line(L)
partial_objects['point'] = C
except DependencyError as e:
# Return whatever was constructed successfully
partial_objects['error'] = str(e)
return partial_objects
Scope Lifecycle Management
Resource Cleanup
Each scope function uses try-finally to ensure proper cleanup:
def _construct_with_cleanup(self, dependencies):
"""Create object with guaranteed cleanup."""
self.scope_stack.append("construct")
temp_resources = []
try:
# Allocate resources
temp_obj = allocate_temporary_object()
temp_resources.append(temp_obj)
# Perform construction
result = build_object(temp_obj, dependencies)
# Register in global registry
self.objects[result.name] = result
return result
except Exception:
# Clean up on error
for resource in temp_resources:
resource.dispose()
raise
finally:
# Always pop scope, even if exception occurred
self.scope_stack.pop()
Scope Inspection
Debugging utilities for scope introspection:
def get_current_scope_depth(self):
"""Return current nesting level."""
return len(self.scope_stack)
def get_scope_chain(self):
"""Return human-readable scope chain."""
return " → ".join(self.scope_stack)
def get_objects_in_scope(self, scope_name):
"""Return all objects created within a specific scope."""
# Implementation depends on metadata tracking
return [obj for obj in self.objects.values()
if obj.creation_scope == scope_name]
Part 2: Integration and Advanced Topics
Integration with GeoGebra Construction Protocol
Mapping to GeoGebra Commands
Each geometric construction function maps to GeoGebra commands:
async def construct_isosceles_triangle(self, ggb: GeoGebra, A_coords, B_coords):
"""Construct isosceles triangle in GeoGebra with scope tracking."""
try:
# Level 0: Base points
await ggb.command(f"A={A_coords}")
await ggb.command(f"B={B_coords}")
# Level 1: Line and midpoint
AB = await self._ggb_construct_line(ggb, "A", "B")
M = await self._ggb_construct_midpoint(ggb, "A", "B")
# Level 2: Perpendicular
L = await self._ggb_construct_perpendicular(ggb, "M", "AB")
# Level 3: Point on line
C = await self._ggb_construct_point_on_line(ggb, "L")
return await self._ggb_verify_isosceles(ggb, "A", "B", "C")
except Exception as e:
# GeoGebra command failed
raise DependencyError(f"GeoGebra construction failed: {e}")
async def _ggb_construct_line(self, ggb: GeoGebra, point1: str, point2: str):
"""Level 1: Create line in GeoGebra."""
self.scope_stack.append(f"line_{point1}{point2}")
try:
await ggb.command(f"{point1}{point2} = Line[{point1}, {point2}]")
return f"{point1}{point2}"
finally:
self.scope_stack.pop()
Dependency Graph Parsing
ggblab’s ggb_parser class builds NetworkX graphs directly from GeoGebra construction protocols:
from ggblab import ggb_parser
# Initialize parser and load construction
parser = ggb_parser()
parser.initialize_dataframe(file="construction.parquet")
# Build full dependency graph (NetworkX DiGraph)
parser.parse()
# Access the NetworkX graph
print(f"Graph has {parser.G.number_of_nodes()} nodes, {parser.G.number_of_edges()} edges")
print(f"Roots (no dependencies): {parser.roots}")
print(f"Leaves (terminal objects): {parser.leaves}")
# Example graph structure:
# Nodes: ['A', 'B', 'AB', 'M', 'L', 'C']
# Edges: [('A', 'AB'), ('B', 'AB'), ('A', 'M'), ('B', 'M'),
# ('M', 'L'), ('AB', 'L'), ('L', 'C')]
# Calculate scope levels from the graph
def calculate_scope_levels(G):
"""Calculate scope level as longest path from any root."""
levels = {}
roots = [n for n in G.nodes() if G.in_degree(n) == 0]
for node in G.nodes():
if node in roots:
levels[node] = 0
else:
# Level = max(parent_level) + 1
parent_levels = [levels[pred] for pred in G.predecessors(node)]
levels[node] = max(parent_levels) + 1
return levels
scope_levels = calculate_scope_levels(parser.G)
# {
# 'A': 0, 'B': 0, # Global scope (roots)
# 'AB': 1, 'M': 1, # Level 1 (depend on roots)
# 'L': 2, # Level 2 (depends on Level 1)
# 'C': 3 # Level 3 (depends on Level 2)
# }
NetworkX Graph Traversal Integration
Dependency Graph as Directed Acyclic Graph (DAG)
The geometric dependency structure is isomorphic to a directed acyclic graph (DAG), where:
Nodes = geometric objects (points, lines, circles, etc.)
Edges = dependency relationships (A → B means “B depends on A”)
Topological order = valid construction sequence
Scope level = longest path from root nodes
ggblab’s ggb_parser.parse() already builds this NetworkX DiGraph as parser.G. The following sections show how to leverage NetworkX’s graph algorithms for educational purposes.
Human-Centered Subgraph Extraction: parse_subgraph()
Design Philosophy: ggblab’s parse_subgraph() method prioritizes pedagogical clarity over algorithmic efficiency. Rather than using theoretically optimal algorithms (e.g., topological sort + reachability pruning), it implements a human-intuitive exploration process that mirrors how students actually think about geometric construction:
“If I have points A and B, what can I construct?” “If I add point C, what additional constructions become possible?”
This combinatorial approach:
Enumerates all combinations of root objects (base points/givens)
Identifies which derived objects require exactly that combination of roots
Builds a simplified subgraph (G2) showing minimal construction paths
# Human-centered exploration: try all combinations of starting objects
parser.parse_subgraph()
# Result: G2 contains only "essential" edges
print(f"Full graph G: {parser.G.number_of_edges()} edges")
print(f"Simplified G2: {parser.G2.number_of_edges()} edges")
# G2 shows: "To construct X, you need exactly objects {A, B}"
# rather than: "X depends on A, B, and their transitive dependencies"
Why this design choice?
Mirrors student thinking: Students explore “what if” scenarios by trying different combinations of starting conditions
Explicit enumeration is transparent: Each step is visible and debuggable
Matches classroom workflow: Teachers ask “What can we build from these givens?”
Computational thinking pedagogy: Teaches exhaustive search as a valid problem-solving strategy
Trade-offs acknowledged:
⚠️ O(2^n) complexity: With 20+ root objects, enumeration becomes slow
⚠️ Limited n-ary support: Constructions requiring 3+ simultaneous inputs are simplified
✅ Learning value: Students see the exploration process explicitly
✅ Correctness: Finds valid construction paths, even if not always optimal
For classroom use with typical constructions (5-15 root objects), this approach provides understandable, traceable results that support learning goals.
Alternative Approaches (For Reference)
For production systems or complex constructions, theoretically efficient algorithms exist:
# Efficient algorithm (not implemented in ggblab): topological sort + pruning
def extract_subgraph_efficient(G):
"""O(n(n+m)) algorithm for minimal subgraph extraction."""
G_minimal = nx.DiGraph()
for node in nx.topological_sort(G):
direct_parents = list(G.predecessors(node))
# Keep only parents with no alternative path
for parent in direct_parents:
G_without = G.copy()
G_without.remove_edge(parent, node)
# If removing this edge disconnects node from parent, it's essential
if not nx.has_path(G_without, parent, node):
G_minimal.add_edge(parent, node)
return G_minimal
ggblab intentionally does NOT use this approach because:
Students cannot trace the logic of “remove edge, check connectivity”
The algorithm is opaque to learners
Educational value is lost in pursuit of efficiency
The lesson: In educational software, understandability > performance. ggblab’s parse_subgraph() sacrifices algorithmic elegance to preserve pedagogical transparency.
Leveraging NetworkX for Educational Analysis
NetworkX provides powerful tools for analyzing ggblab’s dependency graphs:
import networkx as nx
from typing import Dict, List, Set
class DependencyGraph:
"""Manages geometric dependencies as a NetworkX DAG."""
def __init__(self):
self.graph = nx.DiGraph()
self._scope_levels = {}
def add_object(self, name: str, dependencies: List[str]):
"""Add geometric object with its dependencies."""
self.graph.add_node(name)
for dep in dependencies:
# Edge from dependency to dependent
self.graph.add_edge(dep, name)
def build_from_scene(self, scene_dependencies: Dict[str, List[str]]):
"""Build graph from ggblab parser output."""
for obj_name, deps in scene_dependencies.items():
self.add_object(obj_name, deps)
# Verify DAG property
if not nx.is_directed_acyclic_graph(self.graph):
cycles = list(nx.simple_cycles(self.graph))
raise DependencyError(
f"Circular dependency detected: {cycles[0]}"
)
def calculate_scope_levels(self) -> Dict[str, int]:
"""Calculate scope level for each object (longest path from roots)."""
if self._scope_levels:
return self._scope_levels
# Find root nodes (no dependencies)
roots = [n for n in self.graph.nodes() if self.graph.in_degree(n) == 0]
# Calculate longest path from any root
for node in self.graph.nodes():
if node in roots:
self._scope_levels[node] = 0
else:
# Level = max(parent_level) + 1
parent_levels = [
self._scope_levels[pred]
for pred in self.graph.predecessors(node)
]
self._scope_levels[node] = max(parent_levels) + 1
return self._scope_levels
def topological_construction_order(self) -> List[str]:
"""Return valid construction order (topological sort)."""
try:
return list(nx.topological_sort(self.graph))
except nx.NetworkXError:
raise DependencyError("Cannot construct: circular dependencies exist")
def get_transitive_dependencies(self, obj_name: str) -> Set[str]:
"""Get all objects that obj_name depends on (transitively)."""
# All ancestors in the DAG
return nx.ancestors(self.graph, obj_name)
def get_dependent_objects(self, obj_name: str) -> Set[str]:
"""Get all objects that depend on obj_name (transitively)."""
# All descendants in the DAG
return nx.descendants(self.graph, obj_name)
Graph Traversal Strategies
Depth-First Search (DFS) - Recursive Scope Exploration
DFS naturally corresponds to recursive function calls:
def construct_with_dfs(self, target_object: str):
"""Construct object using DFS (recursive dependencies first)."""
visited = set()
def dfs_construct(obj_name: str):
"""Recursively construct dependencies."""
if obj_name in visited:
return # Already constructed
# DFS: Process dependencies first (recursive scope creation)
for dep in self.graph.predecessors(obj_name):
dfs_construct(dep) # Nested function call = nested scope
# Construct current object after dependencies are ready
self.scope_stack.append(obj_name)
try:
self._construct_object(obj_name)
visited.add(obj_name)
except Exception as e:
raise DependencyError(
f"Failed to construct {obj_name}. "
f"Scope chain: {' → '.join(self.scope_stack)}"
) from e
finally:
self.scope_stack.pop()
dfs_construct(target_object)
DFS characteristics:
✅ Mirrors recursive function call structure
✅ Naturally creates scope depth hierarchy
✅ Exception propagation follows DFS backtracking
❌ May hit stack overflow for deep graphs
Breadth-First Search (BFS) - Level-by-Level Construction
BFS constructs all objects at the same scope level before proceeding:
def construct_with_bfs(self):
"""Construct all objects level-by-level (BFS by scope depth)."""
levels = self.calculate_scope_levels()
max_level = max(levels.values())
for level in range(max_level + 1):
# Get all objects at current scope level
objects_at_level = [
obj for obj, lvl in levels.items() if lvl == level
]
print(f"Constructing scope level {level}: {objects_at_level}")
for obj_name in objects_at_level:
try:
self._construct_object(obj_name)
except Exception as e:
raise DependencyError(
f"Failed at scope level {level}, object {obj_name}: {e}"
)
BFS characteristics:
✅ Iterative (no stack overflow risk)
✅ Clear level-by-level progress
✅ Easy to parallelize objects at same level
❌ Doesn’t mirror recursive structure as naturally
Topological Sort - Guaranteed Valid Order
Topological sort provides any valid construction sequence:
def construct_topological(self):
"""Construct objects in topologically sorted order."""
construction_order = self.topological_construction_order()
for obj_name in construction_order:
scope_level = self._scope_levels[obj_name]
# All dependencies are guaranteed to be constructed
dependencies = list(self.graph.predecessors(obj_name))
print(f"[Level {scope_level}] Constructing {obj_name} "
f"(depends on: {dependencies})")
try:
self._construct_object(obj_name)
except Exception as e:
# Find which dependency caused the failure
failed_deps = [
dep for dep in dependencies
if not self._is_constructed(dep)
]
raise DependencyError(
f"Cannot construct {obj_name}: "
f"missing dependencies {failed_deps}"
) from e
Scope Level Calculation via Longest Path
Scope level is the longest path from any root node:
def calculate_scope_level_detailed(self, obj_name: str) -> int:
"""Calculate scope level using longest path algorithm."""
# Root nodes have level 0
if self.graph.in_degree(obj_name) == 0:
return 0
# Level = max(predecessor_level) + 1
predecessor_levels = []
for pred in self.graph.predecessors(obj_name):
pred_level = self.calculate_scope_level_detailed(pred)
predecessor_levels.append(pred_level)
return max(predecessor_levels) + 1
# Using NetworkX's built-in longest path
def get_longest_path_to_object(self, obj_name: str) -> List[str]:
"""Get the longest dependency chain leading to obj_name."""
# Find all root nodes
roots = [n for n in self.graph.nodes() if self.graph.in_degree(n) == 0]
longest_path = []
for root in roots:
if nx.has_path(self.graph, root, obj_name):
# Use DAG longest path
path = nx.dag_longest_path(
self.graph.subgraph(
nx.ancestors(self.graph, obj_name) | {obj_name, root}
),
weight=None
)
if len(path) > len(longest_path):
longest_path = path
return longest_path
Detecting and Handling Circular Dependencies
def detect_circular_dependencies(self) -> List[List[str]]:
"""Find all circular dependency chains."""
if nx.is_directed_acyclic_graph(self.graph):
return []
# Find all simple cycles
cycles = list(nx.simple_cycles(self.graph))
return cycles
def break_cycle_interactive(self, cycle: List[str]) -> str:
"""Suggest how to break a circular dependency."""
return (
f"Circular dependency detected: {' → '.join(cycle + [cycle[0]])}\n"
f"Suggestions:\n"
f" 1. Remove dependency: {cycle[-1]} → {cycle[0]}\n"
f" 2. Reorder construction to avoid forward reference\n"
f" 3. Introduce intermediate object to break cycle"
)
Example: Complete Integration
from ggblab import ggb_parser
import networkx as nx
# Parse GeoGebra construction
scene = await ggb_parser.parse("isosceles_triangle.ggb")
# Build dependency graph
dep_graph = DependencyGraph()
dep_graph.build_from_scene(scene.dependencies)
# Analyze structure
print("Topological order:", dep_graph.topological_construction_order())
# Output: ['A', 'B', 'AB', 'M', 'L', 'C']
print("Scope levels:", dep_graph.calculate_scope_levels())
# Output: {'A': 0, 'B': 0, 'AB': 1, 'M': 1, 'L': 2, 'C': 3}
# Find what C depends on
print("C depends on:", dep_graph.get_transitive_dependencies('C'))
# Output: {'L', 'M', 'AB', 'A', 'B'}
# Find what depends on AB
print("Depends on AB:", dep_graph.get_dependent_objects('AB'))
# Output: {'L', 'C'}
# Get longest dependency chain to C
longest_path = dep_graph.get_longest_path_to_object('C')
print("Longest path to C:", ' → '.join(longest_path))
# Output: A → AB → L → C (or B → AB → L → C, both length 3)
# Construct using DFS (recursive)
dep_graph.construct_with_dfs('C')
# Or construct all using BFS (iterative, level-by-level)
dep_graph.construct_with_bfs()
Correspondence: Function Calls ↔ Graph Traversal
Concept |
Recursive Functions |
NetworkX Graph |
|---|---|---|
Scope creation |
Function call creates new scope |
DFS visit creates new stack frame |
Scope level |
Call stack depth |
Longest path from root |
Dependency |
Function parameter |
Directed edge in DAG |
Construction order |
Call sequence |
Topological sort order |
Error propagation |
Exception unwinds stack |
Traverse ancestors |
Impact analysis |
Modify parameter → rerun dependents |
Modify node → traverse descendants |
Circular dependency |
Infinite recursion |
Cycle detection in graph |
Educational Benefits
Students learn that:
Graph structure = Scope structure: Both are hierarchical dependency trees
DFS = Recursive construction: Natural isomorphism
BFS = Level-by-level construction: Iterative alternative
Topological sort = Valid execution order: Any valid sequence works
Longest path = Scope depth: Measures nesting level
This bridges graph theory, recursive algorithms, and programming scopes through a single geometric construction example.
Educational Implications
Teaching Scoping Through Geometry
Students see the correspondence:
Geometric construction steps ↔ Function call hierarchy
Dependency chains ↔ Scope chains
Construction errors ↔ Exception propagation
Parameter changes ↔ Scope re-evaluation
Classroom Workflow
# Lesson: Understanding nested scopes through geometric construction
# Step 1: Show simple construction
A = (0, 0)
B = (3, 0)
scene = GeometricScene()
result = scene.construct_isosceles_triangle(A, B)
# Step 2: Inspect scope chain
print(scene.get_scope_chain())
# Output: base → line_AB → perpendicular_L → point_C → verify
# Step 3: Introduce error
A = (0, 0)
B = (0, 0) # Identical points!
try:
result = scene.construct_isosceles_triangle(A, B)
except DependencyError as e:
print(f"Error: {e}")
# Output: Cannot create line: points A and B are identical.
# Scope chain: base → line_AB
# Step 4: Discuss how error propagated from nested scope to top level
Assessment Rubric
Students demonstrate understanding by:
Predicting scope depth from geometric construction sequence
Identifying which scope level an error originated from
Writing recovery code at the appropriate scope level
Explaining dependency chains using scope vocabulary
Performance Considerations
Stack Depth Limits
Python’s default recursion limit is ~1000. For complex scenes:
import sys
sys.setrecursionlimit(5000) # Increase if needed
# Or use iterative approach for very deep constructions
def construct_iterative(self, base_objects):
"""Iterative construction to avoid stack overflow."""
current_objects = base_objects
for level in range(max_depth):
try:
current_objects = self._construct_level(current_objects, level)
except DependencyError as e:
print(f"Construction stopped at level {level}: {e}")
break
return current_objects
Optimization Strategies
Memoization: Cache constructed objects to avoid redundant computation
Lazy evaluation: Only construct objects when accessed
Batch operations: Group GeoGebra commands to reduce async overhead
from functools import lru_cache
@lru_cache(maxsize=128)
def _construct_line_cached(self, point1_hash, point2_hash):
"""Cached line construction."""
# Implementation
pass
Summary
ggblab’s scoping architecture leverages Python’s fundamental limitation—that functions are the only practical way to create scopes—and turns it into a teaching tool.
Core principles:
Recursive functions create scope hierarchy matching geometric dependency structure
try-except propagates exceptions upward through scope chains for validation
Scope depth = dependency level provides a visual, intuitive model
Exception handling = error recovery strategy at each abstraction level
Result: Students learn programming scopes through a domain they already understand (geometry), with immediate visual feedback (GeoGebra) and concrete error messages (scope chain traces).
This is not just a technical implementation—it’s a pedagogical framework that makes abstract concepts tangible.