Skip to content
This repository was archived by the owner on Jul 11, 2025. It is now read-only.

Latest commit

 

History

History
1262 lines (987 loc) · 36 KB

File metadata and controls

1262 lines (987 loc) · 36 KB

Tuple: A Real-World Guide

Table of Contents

  1. Introduction & Core Concepts
  2. Basic Usage Patterns
  3. Real-World Examples
  4. Advanced Features Deep Dive
  5. Practical Patterns & Best Practices
  6. Integration Examples

Introduction & Core Concepts

The Problem Tuple Solves

Working with fixed-size, heterogeneous data structures in JavaScript often leads to type safety issues, verbose code, and runtime errors. Traditional approaches using arrays or objects create several challenges:

// Traditional approach - arrays lose type information
function processCoordinates(point: [number, number]): string {
  // Type information is lost - compiler can't verify structure
  const x = point[0]; // Could be undefined at runtime
  const y = point[1]; // Could be undefined at runtime
  
  // No type safety for transformations
  return `Position: ${x}, ${y}`;
}

// Traditional approach - objects are verbose for simple pairs
interface Point {
  x: number;
  y: number;
}

function createPoint(x: number, y: number): Point {
  return { x, y }; // Verbose for simple data
}

// Traditional approach - no functional operations
function transformPoints(points: Point[]): Point[] {
  const result: Point[] = [];
  for (const point of points) {
    // Manual transformation with mutation
    result.push({
      x: point.x * 2,
      y: point.y * 2
    });
  }
  return result;
}

This approach leads to:

  • Type Erasure - Arrays lose specific length and element type information
  • Runtime Errors - No guarantee that elements exist at specific indices
  • Verbose Code - Objects require more boilerplate for simple heterogeneous data
  • No Functional Operations - Limited composable transformation capabilities
  • Poor Ergonomics - Switching between array indices and object properties

The Tuple Solution

Effect's Tuple module provides type-safe, functional operations for fixed-length heterogeneous data with guaranteed type preservation:

import { Tuple } from "effect"

// Type-safe tuple construction with preserved types
const coordinates = Tuple.make(10, 20); // [number, number]
const userInfo = Tuple.make("john", 25, true); // [string, number, boolean]

// Safe access with preserved types
const x = Tuple.getFirst(coordinates); // number
const y = Tuple.getSecond(coordinates); // number

// Functional transformations with type safety
const doubled = coordinates.pipe(
  Tuple.mapBoth({
    onFirst: (x) => x * 2,   // x is guaranteed to be number
    onSecond: (y) => y * 2   // y is guaranteed to be number
  })
);

Key Concepts

Fixed-Length Structure: Tuples have a known, compile-time length with type guarantees for each position.

Heterogeneous Types: Each position can hold a different type, unlike arrays which are homogeneous.

Type Preservation: All operations maintain precise type information through the transformation chain.

Functional Operations: Immutable transformations that compose naturally with other Effect operations.

Basic Usage Patterns

Pattern 1: Creating Tuples

import { Tuple } from "effect"

// Creating tuples with make
const point2D = Tuple.make(10, 20);          // [number, number]
const point3D = Tuple.make(10, 20, 30);      // [number, number, number]
const userRecord = Tuple.make("Alice", 30, "Engineer"); // [string, number, string]

// Creating tuples from existing arrays (with type assertion)
const coordinates = [100, 200] as const;     // readonly [100, 200]
const config = ["prod", true, 3000] as const; // readonly ["prod", true, 3000]

Pattern 2: Accessing Elements

import { Tuple } from "effect"

const userInfo = Tuple.make("Bob", 25, true);

// Safe access for pairs
const name = Tuple.getFirst(userInfo);  // Type error: userInfo is not a pair
const age = Tuple.getSecond(userInfo);  // Type error: userInfo is not a pair

// For pairs specifically
const point = Tuple.make(10, 20);
const x = Tuple.getFirst(point);   // number: 10
const y = Tuple.getSecond(point);  // number: 20

// Generic access by index
const firstElement = Tuple.at(userInfo, 0);  // string: "Bob"
const secondElement = Tuple.at(userInfo, 1); // number: 25
const thirdElement = Tuple.at(userInfo, 2);  // boolean: true

Pattern 3: Basic Transformations

import { Tuple } from "effect"

const point = Tuple.make(10, 20);

// Transform both elements of a pair
const scaled = Tuple.mapBoth(point, {
  onFirst: (x) => x * 2,
  onSecond: (y) => y * 2
}); // [20, 40]

// Transform only first element
const adjustedX = Tuple.mapFirst(point, (x) => x + 5); // [15, 20]

// Transform only second element  
const adjustedY = Tuple.mapSecond(point, (y) => y - 3); // [10, 17]

// Swap elements
const swapped = Tuple.swap(point); // [20, 10]

Real-World Examples

Example 1: Coordinate System Processing

A graphics application that processes 2D coordinates with type-safe transformations:

import { Tuple, Effect, Array as Arr } from "effect"

// Domain types
type Point2D = readonly [number, number]
type BoundingBox = readonly [Point2D, Point2D] // [topLeft, bottomRight]

// Create coordinate processing functions
const createPoint = (x: number, y: number): Point2D => Tuple.make(x, y)

const translatePoint = (point: Point2D, dx: number, dy: number): Point2D =>
  Tuple.mapBoth(point, {
    onFirst: (x) => x + dx,
    onSecond: (y) => y + dy
  })

const scalePoint = (point: Point2D, factor: number): Point2D =>
  Tuple.mapBoth(point, {
    onFirst: (x) => x * factor,
    onSecond: (y) => y * factor
  })

const rotatePoint = (point: Point2D, angleRad: number): Point2D => {
  const x = Tuple.getFirst(point)
  const y = Tuple.getSecond(point)
  const cos = Math.cos(angleRad)
  const sin = Math.sin(angleRad)
  
  return Tuple.make(
    x * cos - y * sin,
    x * sin + y * cos
  )
}

// Complex transformation pipeline
const processGraphicsPoints = (points: Point2D[]) => Effect.gen(function* () {
  const logger = yield* Effect.log
  
  yield* logger("Processing graphics points transformation")
  
  // Apply series of transformations
  const transformed = points.pipe(
    Arr.map(point => point.pipe(
      p => translatePoint(p, 10, 10),  // Move all points
      p => scalePoint(p, 1.5),         // Scale up
      p => rotatePoint(p, Math.PI / 4) // Rotate 45 degrees
    ))
  )
  
  // Calculate bounding box
  const xs = transformed.map(Tuple.getFirst)
  const ys = transformed.map(Tuple.getSecond)
  const boundingBox: BoundingBox = Tuple.make(
    Tuple.make(Math.min(...xs), Math.min(...ys)),
    Tuple.make(Math.max(...xs), Math.max(...ys))
  )
  
  yield* logger(`Bounding box: ${JSON.stringify(boundingBox)}`)
  
  return { points: transformed, boundingBox }
}).pipe(
  Effect.withSpan("graphics.process_points")
)

// Usage
const originalPoints: Point2D[] = [
  createPoint(0, 0),
  createPoint(100, 0),
  createPoint(100, 100),
  createPoint(0, 100)
]

const program = processGraphicsPoints(originalPoints)

Example 2: Database Query Results with Metadata

A database service that returns query results with execution metadata:

import { Tuple, Effect, Array as Arr } from "effect"

// Domain types
interface User {
  id: number
  name: string
  email: string
}

interface QueryMetadata {
  executionTimeMs: number
  rowsAffected: number
  queryId: string
}

// Database result as tuple: [data, metadata]
type QueryResult<T> = readonly [T[], QueryMetadata]

// Database service
class DatabaseService extends Effect.Tag("DatabaseService")<
  DatabaseService,
  {
    executeQuery: <T>(query: string) => Effect.Effect<QueryResult<T>, Error>
    executeTransaction: <T>(queries: string[]) => Effect.Effect<QueryResult<T>[], Error>
  }
>() {}

// Helper functions for working with query results
const extractData = <T>(result: QueryResult<T>): T[] => 
  Tuple.getFirst(result)

const extractMetadata = <T>(result: QueryResult<T>): QueryMetadata => 
  Tuple.getSecond(result)

const combineResults = <T>(results: QueryResult<T>[]): QueryResult<T> => {
  const allData = results.flatMap(extractData)
  const totalTime = results.reduce((sum, result) => 
    sum + extractMetadata(result).executionTimeMs, 0)
  const totalRows = results.reduce((sum, result) => 
    sum + extractMetadata(result).rowsAffected, 0)
  
  const combinedMetadata: QueryMetadata = {
    executionTimeMs: totalTime,
    rowsAffected: totalRows,
    queryId: `batch_${Date.now()}`
  }
  
  return Tuple.make(allData, combinedMetadata)
}

// Business logic using tuples
const getUsersWithAnalytics = (minAge: number) => Effect.gen(function* () {
  const db = yield* DatabaseService
  
  // Execute query and get tuple result
  const queryResult = yield* db.executeQuery<User>(
    `SELECT * FROM users WHERE age >= ${minAge}`
  )
  
  // Extract data and metadata safely
  const users = extractData(queryResult)
  const metadata = extractMetadata(queryResult)
  
  // Transform data while preserving metadata
  const enhancedResult = Tuple.mapBoth(queryResult, {
    onFirst: (users: User[]) => users.map(user => ({
      ...user,
      emailDomain: user.email.split('@')[1]
    })),
    onSecond: (meta) => ({
      ...meta,
      performanceRating: meta.executionTimeMs < 100 ? 'fast' : 'slow'
    })
  })
  
  return enhancedResult
}).pipe(
  Effect.withSpan("database.get_users_with_analytics"),
  Effect.provideService(DatabaseService, {
    executeQuery: <T>(query: string) => Effect.succeed(
      Tuple.make(
        [] as T[], 
        { executionTimeMs: 45, rowsAffected: 10, queryId: `query_${Date.now()}` }
      )
    ),
    executeTransaction: <T>(queries: string[]) => Effect.succeed([])
  })
)

// Usage with error handling
const program = getUsersWithAnalytics(18).pipe(
  Effect.map(result => ({
    users: extractData(result),
    performance: extractMetadata(result)
  })),
  Effect.tapError(error => Effect.logError(`Database error: ${error.message}`))
)

Example 3: Configuration Management with Validation

A configuration system that pairs values with validation results:

import { Tuple, Effect, Either, Array as Arr } from "effect"

// Configuration types
interface DatabaseConfig {
  host: string
  port: number
  database: string
}

interface ServerConfig {
  port: number
  host: string
  ssl: boolean
}

// Validation result as tuple: [value, isValid]
type ValidationResult<T> = readonly [T, boolean]
type ConfigWithValidation<T> = readonly [T, ValidationResult<string>[]]

// Validation functions
const validateDatabaseConfig = (config: DatabaseConfig): ValidationResult<string>[] => [
  Tuple.make("host", config.host.length > 0),
  Tuple.make("port", config.port > 0 && config.port < 65536),
  Tuple.make("database", config.database.length > 0)
]

const validateServerConfig = (config: ServerConfig): ValidationResult<string>[] => [
  Tuple.make("port", config.port > 0 && config.port < 65536),
  Tuple.make("host", config.host.length > 0),
  Tuple.make("ssl", typeof config.ssl === 'boolean')
]

// Configuration service
class ConfigService extends Effect.Tag("ConfigService")<
  ConfigService,
  {
    loadDatabaseConfig: () => Effect.Effect<ConfigWithValidation<DatabaseConfig>, Error>
    loadServerConfig: () => Effect.Effect<ConfigWithValidation<ServerConfig>, Error>
    validateAndMerge: <T>(configs: ConfigWithValidation<T>[]) => Effect.Effect<T[], Error>
  }
>() {}

// Helper functions
const isConfigValid = <T>(configResult: ConfigWithValidation<T>): boolean => {
  const validations = Tuple.getSecond(configResult)
  return validations.every(validation => Tuple.getSecond(validation))
}

const getValidationErrors = <T>(configResult: ConfigWithValidation<T>): string[] => {
  const validations = Tuple.getSecond(configResult)
  return validations
    .filter(validation => !Tuple.getSecond(validation))
    .map(validation => `Invalid ${Tuple.getFirst(validation)}`)
}

const extractConfig = <T>(configResult: ConfigWithValidation<T>): T =>
  Tuple.getFirst(configResult)

// Application startup with configuration
const initializeApplication = () => Effect.gen(function* () {
  const configService = yield* ConfigService
  const logger = yield* Effect.log
  
  yield* logger("Loading application configuration...")
  
  // Load configurations
  const dbConfigResult = yield* configService.loadDatabaseConfig()
  const serverConfigResult = yield* configService.loadServerConfig()
  
  // Validate configurations
  const dbValid = isConfigValid(dbConfigResult)
  const serverValid = isConfigValid(serverConfigResult)
  
  if (!dbValid) {
    const errors = getValidationErrors(dbConfigResult)
    yield* Effect.fail(new Error(`Database config invalid: ${errors.join(', ')}`))
  }
  
  if (!serverValid) {
    const errors = getValidationErrors(serverConfigResult)
    yield* Effect.fail(new Error(`Server config invalid: ${errors.join(', ')}`))
  }
  
  // Extract validated configurations
  const dbConfig = extractConfig(dbConfigResult)
  const serverConfig = extractConfig(serverConfigResult)
  
  yield* logger("All configurations validated successfully")
  
  return {
    database: dbConfig,
    server: serverConfig,
    ready: true
  }
}).pipe(
  Effect.withSpan("app.initialize"),
  Effect.provideService(ConfigService, {
    loadDatabaseConfig: () => Effect.succeed(
      Tuple.make(
        { host: "localhost", port: 5432, database: "myapp" },
        validateDatabaseConfig({ host: "localhost", port: 5432, database: "myapp" })
      )
    ),
    loadServerConfig: () => Effect.succeed(
      Tuple.make(
        { host: "0.0.0.0", port: 3000, ssl: true },
        validateServerConfig({ host: "0.0.0.0", port: 3000, ssl: true })
      )
    ),
    validateAndMerge: (configs) => Effect.succeed(configs.map(extractConfig))
  })
)

// Usage
const program = initializeApplication().pipe(
  Effect.tapError(error => Effect.logError(`Startup failed: ${error.message}`))
)

Advanced Features Deep Dive

Feature 1: Homomorphic Mapping

The map function treats tuples homomorphically, applying the same transformation to all elements while preserving the tuple structure:

Basic Homomorphic Mapping

import { Tuple } from "effect"

// Transform all elements uniformly
const mixedTuple = Tuple.make("hello", 42, true)

// Convert all elements to strings
const stringified = Tuple.map(mixedTuple, (element) => 
  element.toString().toUpperCase()
) // ["HELLO", "42", "TRUE"]

// Apply same operation to each element
const numbers = Tuple.make(1, 2, 3, 4, 5)
const doubled = Tuple.map(numbers, (n) => n * 2) // [2, 4, 6, 8, 10]

Real-World Homomorphic Example

import { Tuple, Effect } from "effect"

// Process configuration values uniformly
const processConfigValues = (config: readonly [string, string, string]) => Effect.gen(function* () {
  const logger = yield* Effect.log
  
  yield* logger("Processing configuration values...")
  
  // Apply same validation and transformation to all config values
  const processed = Tuple.map(config, (value) => {
    // Uniform processing: trim, validate, and normalize
    const trimmed = value.trim()
    const isValid = trimmed.length > 0
    const normalized = trimmed.toLowerCase()
    
    return {
      original: value,
      processed: normalized,
      valid: isValid
    }
  })
  
  yield* logger(`Processed ${processed.length} config values`)
  
  return processed
})

// Usage
const config = Tuple.make("  DATABASE_URL  ", "PORT", "  SSL_ENABLED  ")
const program = processConfigValues(config)

Feature 2: Type Guards and Runtime Validation

Effect Tuple provides type guards for runtime validation of tuple structure:

Basic Type Guard Usage

import { Tuple } from "effect"

// Check if array is exactly a tuple of specific length
function processPair(input: unknown[]) {
  if (Tuple.isTupleOf(input, 2)) {
    // TypeScript now knows input is [unknown, unknown]
    const first = Tuple.getFirst(input)
    const second = Tuple.getSecond(input)
    return Tuple.make(first, second)
  }
  throw new Error("Input must be a pair")
}

// Check if array has at least N elements
function processAtLeastThree(input: unknown[]) {
  if (Tuple.isTupleOfAtLeast(input, 3)) {
    // TypeScript knows input has at least 3 elements
    const first = input[0]
    const second = input[1] 
    const third = input[2]
    const rest = input.slice(3) // remaining elements
    
    return { first, second, third, rest }
  }
  throw new Error("Input must have at least 3 elements")
}

Advanced Type Guard Pattern

import { Tuple, Effect, Either } from "effect"

// Safe tuple processing with validation
class TupleProcessor extends Effect.Tag("TupleProcessor")<
  TupleProcessor,
  {
    processCoordinates: (input: unknown) => Effect.Effect<readonly [number, number], Error>
    processUserData: (input: unknown) => Effect.Effect<readonly [string, number, boolean], Error>
  }
>() {}

const createTupleProcessor = (): TupleProcessor => ({
  processCoordinates: (input: unknown) => Effect.gen(function* () {
    // Validate input structure
    if (!Array.isArray(input)) {
      yield* Effect.fail(new Error("Input must be an array"))
    }
    
    if (!Tuple.isTupleOf(input, 2)) {
      yield* Effect.fail(new Error("Input must be a pair"))
    }
    
    // Validate element types
    const [x, y] = input
    if (typeof x !== 'number' || typeof y !== 'number') {
      yield* Effect.fail(new Error("Both elements must be numbers"))
    }
    
    return Tuple.make(x, y)
  }),
  
  processUserData: (input: unknown) => Effect.gen(function* () {
    if (!Array.isArray(input)) {
      yield* Effect.fail(new Error("Input must be an array"))
    }
    
    if (!Tuple.isTupleOf(input, 3)) {
      yield* Effect.fail(new Error("Input must have exactly 3 elements"))
    }
    
    const [name, age, active] = input
    
    if (typeof name !== 'string') {
      yield* Effect.fail(new Error("First element must be a string"))
    }
    
    if (typeof age !== 'number') {
      yield* Effect.fail(new Error("Second element must be a number"))
    }
    
    if (typeof active !== 'boolean') {
      yield* Effect.fail(new Error("Third element must be a boolean"))
    }
    
    return Tuple.make(name, age, active)
  })
})

// Usage with validation
const validateAndProcess = (rawData: unknown) => Effect.gen(function* () {
  const processor = yield* TupleProcessor
  
  const coordinates = yield* processor.processCoordinates([10, 20])
  const userData = yield* processor.processUserData(["Alice", 30, true])
  
  return {
    position: coordinates,
    user: userData
  }
}).pipe(
  Effect.provideService(TupleProcessor, createTupleProcessor()),
  Effect.catchAll(error => Effect.logError(`Validation failed: ${error.message}`).pipe(
    Effect.as(null)
  ))
)

Feature 3: Equivalence and Ordering

Tuples support deep comparison and ordering operations:

Equivalence for Tuples

import { Tuple, Equivalence } from "effect"

// Create equivalence for different tuple types
const pointEquivalence = Tuple.getEquivalence(
  Equivalence.number, // First element comparison
  Equivalence.number  // Second element comparison
)

const point1 = Tuple.make(10, 20)
const point2 = Tuple.make(10, 20)
const point3 = Tuple.make(15, 25)

console.log(pointEquivalence(point1, point2)) // true
console.log(pointEquivalence(point1, point3)) // false

// Complex tuple equivalence
const userEquivalence = Tuple.getEquivalence(
  Equivalence.string,  // name
  Equivalence.number,  // age
  Equivalence.boolean  // active
)

const user1 = Tuple.make("Alice", 30, true)
const user2 = Tuple.make("Alice", 30, true)
console.log(userEquivalence(user1, user2)) // true

Ordering for Tuples

import { Tuple, Order } from "effect"

// Create ordering for tuples (lexicographic order)
const pointOrder = Tuple.getOrder(
  Order.number, // First compare by first element
  Order.number  // Then by second element
)

const points = [
  Tuple.make(10, 30),
  Tuple.make(5, 20),
  Tuple.make(10, 25),
  Tuple.make(15, 10)
]

// Sort points using tuple ordering
const sortedPoints = points.sort(pointOrder)
// Result: [[5, 20], [10, 25], [10, 30], [15, 10]]

// Custom ordering example
const userOrder = Tuple.getOrder(
  Order.string,   // Sort by name first
  Order.number    // Then by age
)

const users = [
  Tuple.make("Bob", 25),
  Tuple.make("Alice", 30),
  Tuple.make("Alice", 20),
  Tuple.make("Charlie", 35)
]

const sortedUsers = users.sort(userOrder)
// Result: [["Alice", 20], ["Alice", 30], ["Bob", 25], ["Charlie", 35]]

Practical Patterns & Best Practices

Pattern 1: Tuple-Based State Management

import { Tuple, Effect, Ref } from "effect"

// State as tuple: [current value, previous value]
type StateHistory<T> = readonly [T, T | null]

// Create stateful operations with history
const createStatefulCounter = (initialValue: number) => Effect.gen(function* () {
  const stateRef = yield* Ref.make<StateHistory<number>>(
    Tuple.make(initialValue, null)
  )
  
  const increment = () => Ref.modify(stateRef, (state) => {
    const current = Tuple.getFirst(state)
    const newState = Tuple.make(current + 1, current)
    return Tuple.make(current + 1, newState)
  })
  
  const decrement = () => Ref.modify(stateRef, (state) => {
    const current = Tuple.getFirst(state)
    const newState = Tuple.make(current - 1, current)
    return Tuple.make(current - 1, newState)
  })
  
  const getState = () => Ref.get(stateRef)
  
  const canUndo = () => Ref.get(stateRef).pipe(
    Effect.map(state => Tuple.getSecond(state) !== null)
  )
  
  const undo = () => Ref.modify(stateRef, (state) => {
    const previous = Tuple.getSecond(state)
    if (previous === null) {
      return Tuple.make(false, state) // Can't undo
    }
    const newState = Tuple.make(previous, null)
    return Tuple.make(true, newState)
  })
  
  return { increment, decrement, getState, canUndo, undo }
})

// Usage
const counterProgram = Effect.gen(function* () {
  const counter = yield* createStatefulCounter(0)
  
  yield* counter.increment() // 1
  yield* counter.increment() // 2
  yield* counter.increment() // 3
  
  const state1 = yield* counter.getState() // [3, 2]
  const canUndo = yield* counter.canUndo() // true
  
  const undoSuccess = yield* counter.undo() // true
  const state2 = yield* counter.getState() // [2, null]
  
  return { state1, state2, undoSuccess }
})

Pattern 2: Result and Error Pairing

import { Tuple, Effect, Either } from "effect"

// Operation result as tuple: [result, error]
type OperationResult<T, E> = readonly [T | null, E | null]

// Helper functions for result tuples
const success = <T>(value: T): OperationResult<T, never> => 
  Tuple.make(value, null)

const failure = <E>(error: E): OperationResult<never, E> => 
  Tuple.make(null, error)

const isSuccess = <T, E>(result: OperationResult<T, E>): result is readonly [T, null] =>
  Tuple.getSecond(result) === null

const isFailure = <T, E>(result: OperationResult<T, E>): result is readonly [null, E] =>
  Tuple.getFirst(result) === null

const extractValue = <T, E>(result: OperationResult<T, E>): T => {
  if (!isSuccess(result)) {
    throw new Error("Cannot extract value from failed operation")
  }
  return Tuple.getFirst(result)
}

const extractError = <T, E>(result: OperationResult<T, E>): E => {
  if (!isFailure(result)) {
    throw new Error("Cannot extract error from successful operation")
  }
  return Tuple.getSecond(result)
}

// Service using result tuples
class ApiService extends Effect.Tag("ApiService")<
  ApiService,
  {
    fetchUser: (id: string) => Effect.Effect<OperationResult<User, string>>
    updateUser: (user: User) => Effect.Effect<OperationResult<User, string>>
  }
>() {}

interface User {
  id: string
  name: string
  email: string
}

// Business logic with result tuples
const processUserUpdate = (userId: string, updates: Partial<User>) => Effect.gen(function* () {
  const api = yield* ApiService
  const logger = yield* Effect.log
  
  // Fetch user
  const fetchResult = yield* api.fetchUser(userId)
  
  if (isFailure(fetchResult)) {
    const error = extractError(fetchResult)
    yield* logger(`Failed to fetch user: ${error}`)
    return failure(`User fetch failed: ${error}`)
  }
  
  const user = extractValue(fetchResult)
  yield* logger(`Fetched user: ${user.name}`)
  
  // Update user
  const updatedUser = { ...user, ...updates }
  const updateResult = yield* api.updateUser(updatedUser)
  
  if (isFailure(updateResult)) {
    const error = extractError(updateResult)
    yield* logger(`Failed to update user: ${error}`)
    return failure(`User update failed: ${error}`)
  }
  
  const finalUser = extractValue(updateResult)
  yield* logger(`Successfully updated user: ${finalUser.name}`)
  
  return success(finalUser)
}).pipe(
  Effect.withSpan("user.process_update"),
  Effect.provideService(ApiService, {
    fetchUser: (id) => Effect.succeed(
      success({ id, name: "Alice", email: "alice@example.com" })
    ),
    updateUser: (user) => Effect.succeed(success(user))
  })
)

// Usage
const program = processUserUpdate("123", { name: "Alice Smith" })

Pattern 3: Caching with Metadata

import { Tuple, Effect, Ref, DateTime } from "effect"

// Cache entry as tuple: [value, metadata]
type CacheEntry<T> = readonly [T, CacheMetadata]

interface CacheMetadata {
  timestamp: DateTime.DateTime
  hits: number
  ttl: number
}

// Cache service using tuples
class CacheService extends Effect.Tag("CacheService")<
  CacheService,
  {
    get: <T>(key: string) => Effect.Effect<CacheEntry<T> | null, Error>
    set: <T>(key: string, value: T, ttlMs: number) => Effect.Effect<void, Error>
    invalidate: (key: string) => Effect.Effect<void, Error>
    getStats: (key: string) => Effect.Effect<CacheMetadata | null, Error>
  }
>() {}

const createInMemoryCache = () => Effect.gen(function* () {
  const storage = yield* Ref.make<Map<string, CacheEntry<any>>>(new Map())
  
  const isExpired = (entry: CacheEntry<any>, now: DateTime.DateTime): boolean => {
    const metadata = Tuple.getSecond(entry)
    const expiryTime = DateTime.add(metadata.timestamp, metadata.ttl)
    return DateTime.greaterThan(now, expiryTime)
  }
  
  const get = <T>(key: string) => Effect.gen(function* () {
    const store = yield* Ref.get(storage)
    const entry = store.get(key) as CacheEntry<T> | undefined
    
    if (!entry) {
      return null
    }
    
    const now = yield* DateTime.now
    
    if (isExpired(entry, now)) {
      // Remove expired entry
      yield* Ref.update(storage, (store) => {
        const newStore = new Map(store)
        newStore.delete(key)
        return newStore
      })
      return null
    }
    
    // Update hit count
    const updatedEntry = Tuple.mapSecond(entry, (metadata) => ({
      ...metadata,
      hits: metadata.hits + 1
    }))
    
    yield* Ref.update(storage, (store) => {
      const newStore = new Map(store)
      newStore.set(key, updatedEntry)
      return newStore
    })
    
    return updatedEntry
  })
  
  const set = <T>(key: string, value: T, ttlMs: number) => Effect.gen(function* () {
    const now = yield* DateTime.now
    const metadata: CacheMetadata = {
      timestamp: now,
      hits: 0,
      ttl: ttlMs
    }
    
    const entry = Tuple.make(value, metadata)
    
    yield* Ref.update(storage, (store) => {
      const newStore = new Map(store)
      newStore.set(key, entry)
      return newStore
    })
  })
  
  const invalidate = (key: string) => Effect.gen(function* () {
    yield* Ref.update(storage, (store) => {
      const newStore = new Map(store)
      newStore.delete(key)
      return newStore
    })
  })
  
  const getStats = (key: string) => Effect.gen(function* () {
    const store = yield* Ref.get(storage)
    const entry = store.get(key)
    
    if (!entry) {
      return null
    }
    
    return Tuple.getSecond(entry)
  })
  
  return { get, set, invalidate, getStats }
})

// Usage with caching
const cachedUserService = Effect.gen(function* () {
  const cache = yield* CacheService
  const logger = yield* Effect.log
  
  const getUserWithCache = (userId: string) => Effect.gen(function* () {
    const cacheKey = `user:${userId}`
    
    // Try cache first
    const cached = yield* cache.get<User>(cacheKey)
    
    if (cached) {
      const user = Tuple.getFirst(cached)
      const metadata = Tuple.getSecond(cached)
      
      yield* logger(`Cache hit for user ${userId} (${metadata.hits} total hits)`)
      return user
    }
    
    // Cache miss - fetch from source
    yield* logger(`Cache miss for user ${userId}`)
    const user: User = { id: userId, name: "Alice", email: "alice@example.com" }
    
    // Cache for 5 minutes
    yield* cache.set(cacheKey, user, 5 * 60 * 1000)
    
    return user
  })
  
  return { getUserWithCache }
}).pipe(
  Effect.provideService(CacheService, yield* createInMemoryCache())
)

// Program
const program = cachedUserService.pipe(
  Effect.flatMap(service => service.getUserWithCache("123")),
  Effect.flatMap(user => Effect.log(`Retrieved user: ${user.name}`))
)

Integration Examples

Integration with Schema Validation

import { Tuple, Schema, Effect } from "effect"

// Define schemas for tuple elements
const CoordinateSchema = Schema.Struct({
  x: Schema.Number,
  y: Schema.Number
})

const UserSchema = Schema.Struct({
  id: Schema.String,
  name: Schema.String,
  age: Schema.Number
})

// Schema for tuples
const PointTupleSchema = Schema.Tuple(Schema.Number, Schema.Number)
const UserDataTupleSchema = Schema.Tuple(Schema.String, Schema.Number, Schema.Boolean)

// Service integrating tuples with schema validation
class ValidationService extends Effect.Tag("ValidationService")<
  ValidationService,
  {
    validatePoint: (input: unknown) => Effect.Effect<readonly [number, number], Error>
    validateUserData: (input: unknown) => Effect.Effect<readonly [string, number, boolean], Error>
    validateAndTransform: <T>(schema: Schema.Schema<T>, input: unknown) => Effect.Effect<T, Error>
  }
>() {}

const createValidationService = (): ValidationService => ({
  validatePoint: (input) => Effect.gen(function* () {
    const validated = yield* Schema.decodeUnknown(PointTupleSchema)(input)
    return validated
  }),
  
  validateUserData: (input) => Effect.gen(function* () {
    const validated = yield* Schema.decodeUnknown(UserDataTupleSchema)(input)
    return validated
  }),
  
  validateAndTransform: (schema, input) => Effect.gen(function* () {
    const validated = yield* Schema.decodeUnknown(schema)(input)
    return validated
  })
})

// Processing service using validated tuples
const processValidatedData = (rawData: {
  points: unknown[]
  users: unknown[]
}) => Effect.gen(function* () {
  const validator = yield* ValidationService
  const logger = yield* Effect.log
  
  yield* logger("Processing validated tuple data...")
  
  // Validate all points
  const validatedPoints = yield* Effect.all(
    rawData.points.map(point => validator.validatePoint(point))
  )
  
  // Validate all user data
  const validatedUsers = yield* Effect.all(
    rawData.users.map(user => validator.validateUserData(user))
  )
  
  // Transform validated tuples
  const processedPoints = validatedPoints.map(point => 
    Tuple.mapBoth(point, {
      onFirst: (x) => x * 2,
      onSecond: (y) => y * 2
    })
  )
  
  const processedUsers = validatedUsers.map(user =>
    Tuple.mapFirst(user, (name) => name.toUpperCase())
  )
  
  yield* logger(`Processed ${processedPoints.length} points and ${processedUsers.length} users`)
  
  return {
    points: processedPoints,
    users: processedUsers
  }
}).pipe(
  Effect.withSpan("data.process_validated"),
  Effect.provideService(ValidationService, createValidationService())
)

// Usage
const program = processValidatedData({
  points: [[10, 20], [30, 40], [50, 60]],
  users: [["Alice", 30, true], ["Bob", 25, false]]
}).pipe(
  Effect.catchAll(error => Effect.logError(`Validation failed: ${error.message}`))
)

Testing Strategies

import { Tuple, Effect, TestContext } from "effect"

// Test utilities for tuple operations
const TupleTestUtils = {
  // Generate test tuples
  generatePoints: (count: number): readonly [number, number][] =>
    Array.from({ length: count }, (_, i) => 
      Tuple.make(i * 10, i * 20)
    ),
  
  // Assertion helpers
  assertTupleEquals: <T extends ReadonlyArray<any>>(actual: T, expected: T) => 
    Effect.gen(function* () {
      const logger = yield* Effect.log
      
      if (actual.length !== expected.length) {
        yield* Effect.fail(new Error(
          `Tuple length mismatch: expected ${expected.length}, got ${actual.length}`
        ))
      }
      
      for (let i = 0; i < actual.length; i++) {
        if (actual[i] !== expected[i]) {
          yield* Effect.fail(new Error(
            `Tuple element ${i} mismatch: expected ${expected[i]}, got ${actual[i]}`
          ))
        }
      }
      
      yield* logger(`Tuple assertion passed: ${JSON.stringify(actual)}`)
    }),
  
  // Mock tuple transformations
  mockTransformation: <T, R>(transform: (input: T) => R) => 
    (input: T): Effect.Effect<R, never> => Effect.succeed(transform(input))
}

// Test suite for tuple operations
const tupleOperationTests = Effect.gen(function* () {
  const logger = yield* Effect.log
  
  yield* logger("Running tuple operation tests...")
  
  // Test 1: Basic tuple creation and access
  yield* logger("Test 1: Basic operations")
  const point = Tuple.make(10, 20)
  const x = Tuple.getFirst(point)
  const y = Tuple.getSecond(point)
  
  yield* TupleTestUtils.assertTupleEquals([x, y], [10, 20])
  
  // Test 2: Tuple transformations
  yield* logger("Test 2: Transformations")
  const doubled = Tuple.mapBoth(point, {
    onFirst: (x) => x * 2,
    onSecond: (y) => y * 2
  })
  
  yield* TupleTestUtils.assertTupleEquals(doubled, [20, 40])
  
  // Test 3: Tuple swapping
  yield* logger("Test 3: Swapping")
  const swapped = Tuple.swap(point)
  yield* TupleTestUtils.assertTupleEquals(swapped, [20, 10])
  
  // Test 4: Homomorphic mapping
  yield* logger("Test 4: Homomorphic mapping")
  const triple = Tuple.make(1, 2, 3)
  const tripled = Tuple.map(triple, (n) => n * 3)
  yield* TupleTestUtils.assertTupleEquals(tripled, [3, 6, 9])
  
  yield* logger("All tuple tests passed!")
  
  return "success"
})

// Integration test with mocked services
const integrationTest = Effect.gen(function* () {
  const logger = yield* Effect.log
  
  yield* logger("Running integration tests...")
  
  // Mock data
  const testPoints = TupleTestUtils.generatePoints(3)
  
  // Test with mocked transformation
  const transformedPoints = yield* Effect.all(
    testPoints.map(point => 
      TupleTestUtils.mockTransformation(
        (p: readonly [number, number]) => Tuple.mapBoth(p, {
          onFirst: (x) => x + 1,
          onSecond: (y) => y + 1
        })
      )(point)
    )
  )
  
  // Verify results
  const expected = [
    Tuple.make(1, 1),
    Tuple.make(11, 21),
    Tuple.make(21, 41)
  ]
  
  for (let i = 0; i < transformedPoints.length; i++) {
    yield* TupleTestUtils.assertTupleEquals(transformedPoints[i], expected[i])
  }
  
  yield* logger("Integration tests passed!")
  
  return "success"
})

// Test runner
const runTests = Effect.gen(function* () {
  yield* tupleOperationTests
  yield* integrationTest
  
  return "All tests completed successfully"
})

// Usage
const testProgram = runTests.pipe(
  Effect.catchAll(error => Effect.logError(`Test failed: ${error.message}`))
)

Conclusion

Effect's Tuple module provides type-safe, functional operations for fixed-length heterogeneous data structures, enabling elegant solutions for coordinate systems, configuration management, and result pairing patterns.

Key benefits:

  • Type Safety: Compile-time guarantees for tuple structure and element types
  • Functional Operations: Immutable transformations that compose naturally with Effect operations
  • Runtime Validation: Type guards for safe tuple processing with unknown data
  • Performance: Efficient operations with minimal overhead compared to objects
  • Ergonomics: Clean syntax for common tuple patterns and transformations

Use Effect Tuple when you need to work with fixed-size collections of heterogeneous data, require compile-time type guarantees, or want to leverage functional programming patterns for data transformation pipelines.