15: Pipelines and Function Composition

Audience: Experienced programmers Time: 90 minutes Prerequisites: 04-Functions-and-Scope, 06-Strings-and-Unicode You'll learn: Pipeline operator, function composition, readable chains, real-world data flow


The Big Picture

Imagine you have a string and want to: 1. Convert to lowercase 2. Split into words 3. Get the first word 4. Get its length

Without pipelines:

var text = "HELLO WORLD"

var words = text.lower().split(" ") var first_word = words.at(0) var length = first_word.len

With the pipeline operator ->, this reads left-to-right:

var text = "HELLO WORLD"

var length = text -> .lower() -> .split(" ") -> .at(0) -> .len

Pipelines make data transformations flow naturally, like reading prose.


Basic Pipeline Syntax

!Pipeline Data Flow

The -> operator passes the left-hand value to the right-hand expression:

// file: 15_pipeline_basics.zbr

// teaches: pipeline operator // chapter: 15-Pipelines-and-Function-Composition

class Main shared def main var text = "HELLO WORLD"

# Without pipeline (nested calls) var result1 = text.lower().split(" ")

# With pipeline (left-to-right) var result2 = text -> .lower() -> .split(" ")

# Both are equivalent for word in result2 print word

Notice how pipeline reads more naturally: "take text, lowercase it, split it". Each -> says "pass the result of the previous expression to this one".


Chaining Multiple Operations

Pipelines shine when you have many sequential transformations:

// file: 15_pipeline_chain.zbr

// teaches: chaining operations // chapter: 15-Pipelines-and-Function-Composition

class StringProcessor shared def process(text as str) as str return text -> .lower() -> .trim() -> .replace(" ", " ")

class Main shared def main var input = " HELLO WORLD " var output = StringProcessor.process(input) print output # Output: hello world

Each line applies one transformation. This is much clearer than deeply nested method calls.


Pipelines with Collections

Pipelines work great with lists and maps:

// file: 15_pipeline_collections.zbr

// teaches: piping through collections // chapter: 15-Pipelines-and-Function-Composition

class DataProcessor shared def count_words(text as str) as int return text -> .lower() -> .split(" ") -> .count()

class Main shared def main var input = "The Quick Brown Fox" var words = input -> .lower() -> .split(" ")

var count = words.count() print "Word count: ${count}"

Here, the pipeline takes a string, lowercases it, splits into a list, and we can call .count() on the result.


Custom Functions in Pipelines

You can pipe to any function that takes one argument:

// file: 15_pipeline_custom.zbr

// teaches: custom functions in pipelines // chapter: 15-Pipelines-and-Function-Composition

class Utils shared def double(x as int) as int return x * 2

def add_ten(x as int) as int return x + 10

def format_result(x as int) as str return "Result: ${x}"

class Main shared def main var result = 5 -> Utils.double(.) -> Utils.add_ten(.) -> Utils.format_result(.)

print result # Output: Result: 20

The . placeholder represents the piped value. 5 -> Utils.double(.) means "call Utils.double(5)".


Real World: Data Transformation Pipeline

A practical example: process a CSV-like dataset:

// file: 15_pipeline_real_world.zbr

// teaches: realistic pipeline // chapter: 15-Pipelines-and-Function-Composition

class DataAnalysis shared def parse_numbers(line as str) as List(int) var numbers as List(int) = List() var parts = line.split(",") for part in parts # Simplified: real parsing more complex var num = part.trim().toInt() numbers.add(num) return numbers

def sum_list(items as List(int)) as int var total = 0 for item in items total = total + item return total

def average(total as int, count as int) as float if count == 0 return 0.0 return total / count

class Main shared def main var csv_line = "10, 20, 30, 40"

var avg = csv_line -> DataAnalysis.parse_numbers(.) -> DataAnalysis.sum_list(.) -> DataAnalysis.average(., 4)

print "Average: ${avg}"

This reads as: "Parse CSV, sum values, calculate average." Much clearer than nested calls!


Function Composition Pattern

You can create composed functions that combine multiple operations:

// file: 15_function_composition.zbr

// teaches: composing functions // chapter: 15-Pipelines-and-Function-Composition

class Transform shared def lowercase(text as str) as str return text.lower()

def remove_spaces(text as str) as str return text.replace(" ", "")

def reverse_it(text as str) as str return text.reverse()

def compose_all(text as str) as str return text -> Transform.lowercase(.) -> Transform.remove_spaces(.) -> Transform.reverse_it(.)

class Main shared def main var input = "HELLO WORLD" var output = Transform.compose_all(input) print output # Output: dlrowolleh

Each step is a self-contained function. Composition lets you reuse them in different orders.


Pipelines with Result Types

Pipelines work nicely with error handling:

// file: 15_pipeline_results.zbr

// teaches: pipelines with error handling // chapter: 15-Pipelines-and-Function-Composition

class SafeParser shared def parse_int(text as str) as Result(int, str) if text.len == 0 return Result.err("Empty string") # Simplified parsing if text == "42" return Result.ok(42) return Result.err("Not a number")

def double_it(x as int) as int return x * 2

class Main shared def main var input = "42" var result = input -> SafeParser.parse_int(.)

if result.isOk() var doubled = result.okValue() -> SafeParser.double_it(.) print doubled

When an error occurs, stop the pipeline and handle the error.


Common Mistakes

Mistake 1: Forgetting the Dot Placeholder

// WRONG

var result = 5 -> Utils.double() # Error: double() takes 1 argument, 0 given

// CORRECT var result = 5 -> Utils.double(.)

Mistake 2: Breaking the Chain at the Wrong Place

// WRONG - trying to pipe to an intermediate value

var result = "HELLO" -> .lower() -> .split(" ") first_word = .at(0) # Error: can't pipe to assignment

// CORRECT var result = "HELLO" -> .lower() -> .split(" ") var first_word = result.at(0)

Mistake 3: Piping to Functions with Multiple Parameters

// WRONG - only pipes the first argument

var result = 10 -> Utils.add(., 5) # Accidentally clear, but could be confusing

// This is actually fine, but consider: var result = 10 -> Utils.add(.) # Error: add() requires 2 arguments

Mistake 4: Over-Piping (Readability)

// TOO MUCH - hard to follow after many steps

var result = "data" -> .lower() -> .trim() -> .replace("a", "b") -> .reverse() -> .split("") -> Filter.remove_blanks(.) -> Sorter.sort(.) -> Formatter.join_with_commas(.)

// BETTER - break into logical chunks var cleaned = "data" -> .lower() -> .trim() -> .replace("a", "b")

var processed = cleaned -> .reverse() -> .split("")


Exercises

Exercise 1: Text Processing Pipeline

Create a pipeline that takes user input and: converts to lowercase, removes leading/trailing spaces, and counts words:

Solution

class TextStats

shared def count_words(text as str) as int if text.len == 0 return 0 var words = text.split(" ") return words.count()

class Main shared def main var user_input = " HELLO WORLD FOO "

var word_count = user_input -> .lower() -> .trim() -> TextStats.count_words(.)

print "Words: ${word_count}"

Exercise 2: Number Transformation Pipeline

Build a pipeline that: parses an integer, doubles it, adds 10, and formats as a string:

Solution

class NumUtils

shared def parse_safe(text as str) as Result(int, str) if text == "5" return Result.ok(5) return Result.err("Invalid number")

def double_it(x as int) as int return x * 2

def add_ten(x as int) as int return x + 10

def to_message(x as int) as str return "Final result: ${x}"

class Main shared def main var input = "5"

var result = input -> NumUtils.parse_safe(.)

if result.isOk() var final = result.okValue() -> NumUtils.double_it(.) -> NumUtils.add_ten(.) -> NumUtils.to_message(.) print final

Exercise 3: List Filtering Pipeline

Create a pipeline that filters a list of numbers to keep only even values, sums them, and returns the average:

Solution

class ListOps

shared def filter_even(items as List(int)) as List(int) var result as List(int) = List() for item in items if item % 2 == 0 result.add(item) return result

def sum_all(items as List(int)) as int var total = 0 for item in items total = total + item return total

class Main shared def main var numbers as List(int) = List() numbers.add(1) numbers.add(2) numbers.add(3) numbers.add(4) numbers.add(5) numbers.add(6)

var sum = numbers -> ListOps.filter_even(.) -> ListOps.sum_all(.)

print "Sum of evens: ${sum}"


Key Takeaways

- Pipelines make data transformations read naturally — Left-to-right flow matches human thinking - The -> operator passes results forward — Each step receives the previous step's output - Pipelines work with methods and functions — Mix .method() calls with Function(.) calls - Break long pipelines into logical chunks — Maintain readability for complex transformations - Pipelines + Results = clean error handling — Stop processing when an error occurs


Next Steps

- → 16-Project-1 — Build a tool that uses pipelines extensively - → 21-Regular-Expressions — Pipelines + pattern matching = powerful text processing


Pipelines let your code flow like your thoughts. Master them, and complex transformations become natural.