Functional Programming Guide
W3MSG SDK is built on functional programming principles, providing a robust, composable, and type-safe foundation for Web3 messaging. This guide explores the architectural patterns that make the SDK both powerful and maintainable.
๐ฏ Core Principlesโ
1. Pure Functionsโ
Functions with no side effects that always return the same output for the same input:
// โ
Pure function - predictable and testable
const validateMessage = (message: Message): ValidationResult => {
const errors = [
!message.to && 'Recipient required',
!message.content?.trim() && 'Content required',
message.content && message.content.length > 1000 && 'Message too long'
].filter(Boolean) as string[];
return {
isValid: errors.length === 0,
errors: errors
};
};
// โ Impure function - has side effects
const validateMessageBad = (message: Message): boolean => {
if (!message.to) {
console.log('Missing recipient'); // Side effect!
return false;
}
return true;
};
2. Immutable Dataโ
State updates always return new objects instead of modifying existing ones:
// โ
Immutable update pattern
const updateSDKConfig = (
current: SDKConfig,
updates: Partial<SDKConfig>
): SDKConfig => ({
...current,
...updates,
// Nested objects must also be immutable
retryConfig: current.retryConfig ? {
...current.retryConfig,
...updates.retryConfig
} : updates.retryConfig
});
// โ Mutable update - modifies original
const updateSDKConfigBad = (config: SDKConfig, updates: Partial<SDKConfig>) => {
Object.assign(config, updates); // Mutates original!
return config;
};
3. Function Compositionโ
Building complex operations from simple, composable functions:
// Individual pure functions
const parseMessage = (raw: string): ParsedMessage => ({ /* ... */ });
const validateParsedMessage = (msg: ParsedMessage): ValidationResult => ({ /* ... */ });
const encryptMessage = (msg: ParsedMessage): EncryptedMessage => ({ /* ... */ });
// Composed pipeline
const processMessage = (raw: string): Result<EncryptedMessage, Error> =>
Result.success(raw)
.map(parseMessage)
.andThen(msg =>
validateParsedMessage(msg).isValid
? Result.success(msg)
: Result.failure(new ValidationError(validateParsedMessage(msg).errors.join(', ')))
)
.map(encryptMessage);
๐ง Monadic Error Handlingโ
Result<T, E> Typeโ
The Result type represents operations that can succeed or fail:
// Success case
const successResult = Result.success('Hello World');
console.log(successResult.isSuccess()); // true
// Failure case
const failureResult = Result.failure(new Error('Something went wrong'));
console.log(failureResult.isFailure()); // true
// Safe operations with map
const processedResult = successResult
.map(text => text.toUpperCase())
.map(text => `${text}!`);
// Chain operations that might fail
const chainedResult = successResult
.andThen(text =>
text.length > 0
? Result.success(text.split(' '))
: Result.failure(new Error('Empty string'))
)
.map(words => words.length);
Either<L, R> Typeโ
The Either type represents values that can be one of two types:
// Protocol selection with Either
const selectProtocol = (messageType: string): Either<ProtocolError, Protocol> => {
switch (messageType) {
case 'chat':
return Either.right(xmtpProtocol);
case 'notification':
return Either.right(pushProtocol);
default:
return Either.left(new UnsupportedMessageTypeError(messageType));
}
};
// Handle both cases safely
const handleProtocol = selectProtocol('chat').fold(
error => console.error('Protocol selection failed:', error.message),
protocol => console.log('Using protocol:', protocol.name)
);
Optional<T> Typeโ
The Optional type provides null-safe operations:
// Safe property access
const getWalletAddress = (sdk: W3MSGSDK): Optional<string> =>
Optional.fromNullable(sdk.getWalletAddress());
// Chain optional operations
const formattedAddress = getWalletAddress(sdk)
.map(addr => addr.toLowerCase())
.map(addr => `${addr.slice(0, 6)}...${addr.slice(-4)}`)
.getOrElse('No address');
โก Async Functional Patternsโ
Result with Promisesโ
Convert async operations to Result types:
// Convert Promise to Result
const connectWalletSafe = async (): Promise<Result<string, Error>> => {
return Result.fromAsync(async () => {
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
return signer.getAddress();
});
};
// Chain async operations
const walletConnectionFlow = async (): Promise<Result<void, Error>> => {
const addressResult = await connectWalletSafe();
return addressResult.andThen(async address => {
const initResult = await Result.fromAsync(() => sdk.initialize());
return initResult.andThen(async () => {
return Result.fromAsync(() => sdk.connect());
});
});
};
Resilient Async Operationsโ
Higher-order functions for robust async operations:
// Timeout wrapper
const withTimeout = <T>(
operation: () => Promise<T>,
timeoutMs: number
): Promise<T> => {
return Promise.race([
operation(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new TimeoutError(`Operation timed out after ${timeoutMs}ms`)), timeoutMs)
)
]);
};
// Retry wrapper with exponential backoff
const withRetry = <T>(
operation: () => Promise<T>,
config: { maxAttempts: number; backoffFactor: number; jitter?: boolean }
): Promise<T> => {
const { maxAttempts, backoffFactor, jitter = true } = config;
const attempt = async (attemptNumber: number): Promise<T> => {
try {
return await operation();
} catch (error) {
if (attemptNumber >= maxAttempts) throw error;
const delay = Math.pow(backoffFactor, attemptNumber) * 1000;
const jitteredDelay = jitter ? delay * (0.5 + Math.random() * 0.5) : delay;
await new Promise(resolve => setTimeout(resolve, jitteredDelay));
return attempt(attemptNumber + 1);
}
};
return attempt(1);
};
// Combined resilience
const resilientOperation = <T>(
operation: () => Promise<T>,
config: { timeout: number; maxAttempts: number; backoffFactor: number }
): Promise<T> =>
withRetry(
() => withTimeout(operation, config.timeout),
{ maxAttempts: config.maxAttempts, backoffFactor: config.backoffFactor }
);
๐๏ธ Factory Functions and Higher-Order Functionsโ
Factory Functionsโ
Create objects with consistent initialization:
// Circuit breaker factory
const createCircuitBreaker = (config: CircuitBreakerConfig): CircuitBreaker => {
let state: CircuitBreakerState = 'closed';
let failureCount = 0;
let lastFailureTime: number | null = null;
return {
execute: async <T>(operation: () => Promise<T>): Promise<T> => {
if (state === 'open') {
const timeSinceFailure = Date.now() - (lastFailureTime || 0);
if (timeSinceFailure < config.timeout) {
throw new CircuitOpenError('Circuit breaker is open');
}
state = 'half-open';
}
try {
const result = await operation();
if (state === 'half-open') {
state = 'closed';
failureCount = 0;
}
return result;
} catch (error) {
failureCount++;
lastFailureTime = Date.now();
if (failureCount >= config.failureThreshold) {
state = 'open';
}
throw error;
}
},
getState: () => state,
getFailureCount: () => failureCount
};
};
Higher-Order Functionsโ
Functions that operate on other functions:
// Caching higher-order function
const withCaching = <T extends unknown[], R>(
fn: (...args: T) => R,
keyFn: (...args: T) => string,
ttl = 300000 // 5 minutes
) => {
const cache = new Map<string, { value: R; expiry: number }>();
return (...args: T): R => {
const key = keyFn(...args);
const cached = cache.get(key);
if (cached && Date.now() < cached.expiry) {
return cached.value;
}
const result = fn(...args);
cache.set(key, { value: result, expiry: Date.now() + ttl });
return result;
};
};
// Usage
const cachedVerification = withCaching(
(credentialId: string) => verifyCredential(credentialId),
(credentialId) => `vc_${credentialId}`,
600000 // 10 minutes
);
// Performance monitoring higher-order function
const withPerformanceMonitoring = <T extends unknown[], R>(
fn: (...args: T) => Promise<R>,
operationName: string
) => async (...args: T): Promise<R> => {
const start = performance.now();
const operationId = crypto.randomUUID();
console.debug(`[Performance] Starting ${operationName} (${operationId})`);
try {
const result = await fn(...args);
const duration = performance.now() - start;
console.debug(`[Performance] ${operationName} completed in ${duration.toFixed(2)}ms (${operationId})`);
// Record telemetry
sdkPerformance.recordOperation(operationName, duration, true);
return result;
} catch (error) {
const duration = performance.now() - start;
console.debug(`[Performance] ${operationName} failed after ${duration.toFixed(2)}ms (${operationId})`, error);
// Record failure telemetry
sdkPerformance.recordOperation(operationName, duration, false, error);
throw error;
}
};
๐ Advanced TypeScript Featuresโ
Branded Typesโ
Create type-safe identifiers:
// Branded types for enhanced type safety
type WalletAddress = string & { readonly __brand: 'WalletAddress' };
type APIKey = string & { readonly __brand: 'APIKey' };
type MessageID = string & { readonly __brand: 'MessageID' };
// Factory functions with validation
const createWalletAddress = (address: string): WalletAddress => {
if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
throw new Error('Invalid wallet address format');
}
return address as WalletAddress;
};
const createAPIKey = (key: string): APIKey => {
if (key.length < 32) {
throw new Error('API key too short');
}
return key as APIKey;
};
// Type-safe usage
const config: SDKConfig = {
apiKey: createAPIKey('your-very-long-api-key-here-at-least-32-chars'),
walletAddress: createWalletAddress('0x742d35Cc6634C0532925a3b8D900b5dF5D7F3E8a')
};
Utility Typesโ
Advanced TypeScript patterns for better type safety:
// Conditional types for protocol features
type ProtocolFeatures<T extends Protocol> =
T extends XMTPProtocol
? 'chat' | 'conversations' | 'encryption'
: T extends PushProtocol
? 'notifications' | 'channels' | 'subscription'
: never;
// Template literal types for message formats
type MessageFormat<P extends string> = `${P}_message_${string}`;
type XMTPMessageID = MessageFormat<'xmtp'>; // 'xmtp_message_string'
type PushMessageID = MessageFormat<'push'>; // 'push_message_string'
// Mapped types for configuration
type RequiredConfig<T> = {
[K in keyof T]-?: T[K];
};
type PartialExcept<T, K extends keyof T> = Partial<T> & Pick<T, K>;
// Usage in SDK config
type SDKInitConfig = PartialExcept<SDKConfig, 'apiKey'>;
Discriminated Unionsโ
Type-safe error handling and state management:
// Discriminated union for different error types
type ProtocolError =
| { type: 'NETWORK_ERROR'; code: number; message: string; retryable: true }
| { type: 'AUTHENTICATION_ERROR'; message: string; retryable: false }
| { type: 'RATE_LIMITED'; retryAfter: number; retryable: true }
| { type: 'UNKNOWN_ERROR'; error: unknown; retryable: false };
// Type-safe error handling
const handleProtocolError = (error: ProtocolError): void => {
switch (error.type) {
case 'NETWORK_ERROR':
console.log(`Network error (${error.code}): ${error.message}`);
if (error.retryable) {
scheduleRetry();
}
break;
case 'AUTHENTICATION_ERROR':
console.log(`Auth error: ${error.message}`);
// Type system knows retryable is false here
redirectToLogin();
break;
case 'RATE_LIMITED':
console.log(`Rate limited, retry after ${error.retryAfter}ms`);
setTimeout(retry, error.retryAfter);
break;
case 'UNKNOWN_ERROR':
console.error('Unknown error:', error.error);
break;
}
};
๐จ Functional State Managementโ
Event-Driven Architectureโ
Functional event handling with immutable state:
// Event system with functional patterns
type AppState = {
readonly connectionState: 'disconnected' | 'connecting' | 'connected';
readonly messages: readonly Message[];
readonly protocols: readonly Protocol[];
};
type AppEvent =
| { type: 'CONNECTION_STARTED' }
| { type: 'CONNECTION_COMPLETED'; address: WalletAddress }
| { type: 'MESSAGE_RECEIVED'; message: Message }
| { type: 'PROTOCOL_ADDED'; protocol: Protocol };
// Pure state reducer
const appReducer = (state: AppState, event: AppEvent): AppState => {
switch (event.type) {
case 'CONNECTION_STARTED':
return { ...state, connectionState: 'connecting' };
case 'CONNECTION_COMPLETED':
return { ...state, connectionState: 'connected' };
case 'MESSAGE_RECEIVED':
return {
...state,
messages: [...state.messages, event.message]
};
case 'PROTOCOL_ADDED':
return {
...state,
protocols: [...state.protocols, event.protocol]
};
default:
return state;
}
};
// Functional event emitter
const createEventStore = <S, E>(
initialState: S,
reducer: (state: S, event: E) => S
) => {
let currentState = initialState;
const listeners: ((state: S) => void)[] = [];
return {
getState: () => currentState,
dispatch: (event: E) => {
currentState = reducer(currentState, event);
listeners.forEach(listener => listener(currentState));
},
subscribe: (listener: (state: S) => void) => {
listeners.push(listener);
return () => {
const index = listeners.indexOf(listener);
if (index >= 0) listeners.splice(index, 1);
};
}
};
};
๐งช Testing Functional Codeโ
Property-Based Testingโ
Test functions with generated inputs:
// Property-based test for message validation
const validateMessageProperty = (message: Message): boolean => {
const result = validateMessage(message);
// Properties that should always hold
return (
// If validation passes, errors should be empty
(result.isValid && result.errors.length === 0) ||
// If validation fails, there should be at least one error
(!result.isValid && result.errors.length > 0)
);
};
// Test with many random inputs
for (let i = 0; i < 1000; i++) {
const randomMessage = generateRandomMessage();
if (!validateMessageProperty(randomMessage)) {
throw new Error(`Property violated for message: ${JSON.stringify(randomMessage)}`);
}
}
Pure Function Testingโ
Test pure functions with predictable inputs and outputs:
describe('Pure Functions', () => {
test('processMessage should be deterministic', () => {
const input = { to: 'user', content: 'hello' };
const result1 = processMessage(input);
const result2 = processMessage(input);
// Same input should produce same output
expect(result1).toEqual(result2);
});
test('updateConfig should not mutate original', () => {
const original = { retryAttempts: 3, timeout: 5000 };
const updates = { retryAttempts: 5 };
const result = updateConfig(original, updates);
// Original should be unchanged
expect(original.retryAttempts).toBe(3);
// Result should have updates
expect(result.retryAttempts).toBe(5);
expect(result.timeout).toBe(5000);
});
});
๐ Benefits of Functional Approachโ
1. Predictabilityโ
Pure functions always produce the same output for the same input, making code easier to reason about and debug.
2. Testabilityโ
Pure functions are easy to test in isolation without complex setup or mocking.
3. Composabilityโ
Small, focused functions can be combined to create complex behaviors.
4. Type Safetyโ
TypeScript's type system works exceptionally well with functional patterns, catching errors at compile time.
5. Performanceโ
Immutable data structures and pure functions enable better optimization and caching strategies.
6. Concurrencyโ
Pure functions are inherently safe for concurrent execution since they don't modify shared state.
Ready to dive deeper?
Explore our Performance Guide to see how functional programming enables better optimization, or check out the API Reference for detailed examples of functional patterns in action.