sigildocs

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

  1. Prefer immutability - Transform data, don't mutate it
  2. Use pure functions - Same inputs produce same outputs
  3. Compose small functions - Build complex behavior from simple pieces
  4. 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 exist

If 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))
; => 4

Combine 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))
; => 5000

Matching 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))  ; => first

You 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 names

Matching 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)  ; => #f

Result 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 match for 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 guard or explicit result types