Functional Programming in Sigil
Sigil is designed for a functional programming style while remaining practical. This guide demonstrates idiomatic patterns for writing clear, composable code.
Core Principles
- Prefer immutability - Transform data, don't mutate it
- Use pure functions - Same inputs produce same outputs
- Compose small functions - Build complex behavior from simple pieces
- Let data flow - Use pipelines, not nested calls
Data Transformation Pipelines
The -> threading macro is central to idiomatic Sigil. Instead of reading inside-out:
;; Hard to follow
(take 5 (filter even? (map square (range 20))))Write top-to-bottom pipelines:
;; Clear flow
(-> (range 20)
(map square _)
(filter even? _)
(take 5 _))
; => (0 4 16 36 64)The _ placeholder shows where the threaded value goes. Without _, the value is inserted as the first argument (thread-first):
;; These are equivalent:
(-> 5 (+ 1)) ; => 6
(-> 5 (+ _ 1)) ; => 6
;; Use _ when you need a different position:
(-> 5 (- 10 _)) ; => 5 (10 - 5)Nil-Safe Pipelines
Use some-> when steps might return #f:
(define (get-user-email user-id)
(some-> (find-user user-id) ; might return #f
(dict-ref _ settings:) ; might not exist
(dict-ref _ email:))) ; might not existIf any step returns #f, the pipeline short-circuits and returns #f.
Working with Collections
Higher-Order Functions
The core operations work on lists by default:
(map square '(1 2 3 4 5))
; => (1 4 9 16 25)
(filter even? '(1 2 3 4 5 6))
; => (2 4 6)
(fold + 0 '(1 2 3 4 5))
; => 15
(find (lambda (x) (> x 3)) '(1 2 3 4 5))
; => 4Combine them:
(-> '(1 2 3 4 5 6 7 8 9 10)
(filter even? _)
(map square _)
(fold + 0 _))
; => 220 (4 + 16 + 36 + 64 + 100)Polymorphic Operations
Import (sigil seq) for operations that work on any collection type:
(import (sigil seq))
;; Same operations, different collection types
(map square '(1 2 3)) ; => (1 4 9)
(map square #(1 2 3)) ; => #(1 4 9)
(filter even? '(1 2 3 4)) ; => (2 4)
(filter even? #(1 2 3 4)) ; => #(2 4)
;; Works on dicts too (iterates over entries)
(map cdr #{ a: 1 b: 2 c: 3 }) ; => (1 2 3)Transducers for Composition
When you have complex transformations, compose them with transducers:
(import (sigil seq))
(define process-numbers
(comp (filtering positive?)
(mapping square)
(taking 10)))
;; Reusable transformation
(sequence process-numbers '(-2 -1 0 1 2 3 4 5))
; => (1 4 9 16 25)
;; Works on any collection
(into #() process-numbers '(1 2 3 4 5))
; => #(1 4 9 16 25)Transducers compose left-to-right (in reading order) and are more efficient than chaining separate operations.
Pattern Matching
Use match for clean dispatch on data shape:
(define (describe value)
(match value
('() "empty list")
((x) (format "single element: ~a" x))
((x y) (format "pair: ~a and ~a" x y))
((x . rest) (format "list starting with ~a" x))
((? number? n) (format "number: ~a" n))
((? string? s) (format "string: ~s" s))
(_ "something else")))Inline Pattern Matching
Use match-lambda directly in pipelines:
;; Format alist entries inline
(-> '((name . "Alice") (age . 30))
(map (match-lambda
((key . value) (format "~a = ~a" key value)))
_))
; => ("name = Alice" "age = 30")Use match-let for destructuring bindings:
(define (rectangle-area rect)
(match-let (((x y w h) rect))
(* w h)))
(rectangle-area '(10 20 100 50))
; => 5000Matching Structs
Match on struct fields with the : pattern using keyword syntax:
(import (sigil struct))
(define-struct point (x y))
(define (point-quadrant p)
(match p
((: point x: (? positive?) y: (? positive?)) 'first)
((: point x: (? negative?) y: (? positive?)) 'second)
((: point x: (? negative?) y: (? negative?)) 'third)
((: point x: (? positive?) y: (? negative?)) 'fourth)
(_ 'origin)))
(point-quadrant (point x: 10 y: 20)) ; => firstYou can bind fields to variables by name or with explicit bindings:
(match p
((: point x: y:) (+ x y))) ; binds x and y
(match p
((: point x: px y: py) (+ px py))) ; explicit namesMatching Dicts
Use dict literal syntax #{ } to match on keys:
(define (greet-user user)
(match user
(#{ name: n age: a }
(format "Hello ~a, you are ~a years old" n a))
(#{ name: n }
(format "Hello ~a" n))
(_ "Hello stranger")))
(greet-user #{ name: "Alice" age: 30 })
; => "Hello Alice, you are 30 years old"Function Composition
Import (sigil fn) for composition utilities:
(import (sigil fn))
;; Partial application
(define add10 (partial + 10))
(define double (partial * 2))
(add10 5) ; => 15
(double 5) ; => 10
;; Right-to-left composition (mathematical order)
;; (compose f g) means: apply g first, then f
(define double-then-add10 (compose add10 double))
(double-then-add10 5) ; => 20 (5 * 2 = 10, then 10 + 10 = 20)
;; Left-to-right composition (pipeline order)
;; (pipe f g) means: apply f first, then g
(define add10-then-double (pipe add10 double))
(add10-then-double 5) ; => 30 (5 + 10 = 15, then 15 * 2 = 30)Useful Combinators
;; Always return the same value
(map (const 0) '(a b c)) ; => (0 0 0)
;; Negate a predicate
(filter (complement null?) '(() (1) () (2)))
; => ((1) (2))
;; Swap argument order
((flip -) 3 10) ; => 7 (10 - 3)
;; Apply multiple functions
((juxt car cdr length) '(1 2 3))
; => (1 (2 3) 3)Working with Dicts
Dicts are the preferred data structure for structured data:
(define user
#{ name: "Alice"
email: "alice@example.com"
settings: #{ theme: "dark"
notifications: #t }})
;; Access
(dict-ref user name:) ; => "Alice"
;; Update (returns new dict)
(dict-set user name: "Bob")
;; Nested access
(dict-get-in user '(settings: theme:)) ; => "dark"
;; Transform
(-> user
(dict-update email: string-downcase _)
(dict-set verified: #t _))Building Dicts
;; From pairs
(define config
(-> '((host . "localhost")
(port . 8080))
(map (lambda (p) (cons (symbol->keyword (car p)) (cdr p))) _)
alist->dict))
;; Merge with defaults
(define (with-defaults opts)
(dict-merge #{ timeout: 30 retries: 3 } opts))Error Handling
Using guard
Catch errors functionally:
(define (safe-divide a b)
(guard (e (else #f))
(/ a b)))
(safe-divide 10 2) ; => 5
(safe-divide 10 0) ; => #fResult Pattern
Return success/failure explicitly:
(define (parse-int str)
(guard (e (else #{ ok: #f error: "Invalid number" }))
#{ ok: #t value: (string->number str) }))
;; Check result with dict-ref
(let ((result (parse-int "42")))
(if (dict-ref result ok:)
(dict-ref result value:)
(error (dict-ref result error:))))Practical Example
Here's a complete example processing a list of users:
(import (sigil seq)
(sigil fn)
(sigil string))
(define users
(list #{ name: "Alice" age: 30 active: #t }
#{ name: "Bob" age: 17 active: #t }
#{ name: "Carol" age: 25 active: #f }
#{ name: "Dave" age: 45 active: #t }))
;; Find active adult users, format their names
(define (format-active-adults users)
(-> users
(filter (lambda (u) (dict-ref u active:)) _)
(filter (lambda (u) (>= (dict-ref u age:) 18)) _)
(map (lambda (u) (dict-ref u name:)) _)
(map string-upcase _)
(string-join ", " _)))
(format-active-adults users)
; => "ALICE, DAVE"
;; Same thing with transducers
(define adult-names-xform
(comp (filtering (lambda (u) (dict-ref u active:)))
(filtering (lambda (u) (>= (dict-ref u age:) 18)))
(mapping (lambda (u) (dict-ref u name:)))
(mapping string-upcase)))
(-> users
(sequence adult-names-xform _)
(string-join ", " _))
; => "ALICE, DAVE"Summary
- Use
->pipelines for data transformation - Use
matchfor dispatch on data shape - Use
(sigil seq)for polymorphic collection operations - Use transducers for composable, reusable transformations
- Use
(sigil fn)for function composition - Prefer dicts for structured data
- Handle errors with
guardor explicit result types