- Introduction & Core Concepts
- Basic Usage Patterns
- Real-World Examples
- Advanced Features Deep Dive
- Practical Patterns & Best Practices
- Integration Examples
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
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
})
);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.
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]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: trueimport { 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]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)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}`))
)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}`))
)The map function treats tuples homomorphically, applying the same transformation to all elements while preserving the tuple structure:
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]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)Effect Tuple provides type guards for runtime validation of tuple structure:
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")
}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)
))
)Tuples support deep comparison and ordering operations:
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)) // trueimport { 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]]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 }
})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" })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}`))
)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}`))
)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}`))
)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.