UNPKG

typescript-monads

Version:
244 lines (187 loc) 7.74 kB
# Reader Monad The Reader monad is a powerful functional programming pattern for handling dependencies and configuration. It provides a clean way to access shared environment or configuration data throughout a computation without explicitly passing it around. ## Core Concept A Reader monad is essentially a function that: 1. Takes an environment/configuration as input 2. Performs a computation using that environment 3. Returns a result The magic happens when you compose multiple Readers together - each Reader in the chain has access to the same environment, making dependency injection simple and pure. ## Key Benefits - **Dependency Injection**: Pass dependencies around implicitly without global state - **Testability**: Easily swap environments for testing - **Composability**: Combine small, focused Readers into complex operations - **Type Safety**: Environment requirements are encoded in the type system - **Pure Functional**: No side effects or hidden dependencies - **Lazy Evaluation**: Operations are only run when the final Reader is executed ## Basic Usage ```typescript import { reader, asks } from 'typescript-monads' // Create a Reader that extracts a value from the environment const getApiUrl = asks<AppConfig, string>(config => config.apiUrl) // Create a Reader that uses the environment to format a URL const getFullUrl = reader<AppConfig, string>(config => `${config.apiUrl}/users?token=${config.authToken}` ) // Run the Reader with a configuration const url = getFullUrl.run({ apiUrl: 'https://api.example.com', authToken: '12345' }) // "https://api.example.com/users?token=12345" ``` ## Core Operations ### Creation ```typescript // Create from a function that uses the environment const greeting = reader<{name: string}, string>(env => `Hello, ${env.name}!`) // Create a Reader that always returns a constant value (ignores environment) const constant = readerOf<any, number>(42) // Create a Reader that returns the entire environment const getEnv = ask<AppConfig>() // Create a Reader that extracts a specific value from the environment const getTimeout = asks<AppConfig, number>(config => config.timeout) ``` ### Transformation ```typescript // Map the output value const getApiUrl = asks<Config, string>(c => c.apiUrl) const getApiUrlUpper = getApiUrl.map(url => url.toUpperCase()) // Chain Readers const getUser = asks<Config, User>(c => c.currentUser) const getPermissions = (user: User) => asks<Config, string[]>(c => c.permissionsDb.getPermissionsFor(user.id)) // Combined operation: get user and their permissions const getUserPermissions = getUser.flatMap(getPermissions) ``` ### Environment Manipulation ```typescript // Create a Reader that works with a specific config type const getDatabaseUrl = reader<DbConfig, string>(db => `postgres://${db.host}:${db.port}/${db.name}` ) // Adapt it to work with a different environment type const getDbFromAppConfig = getDatabaseUrl.local<AppConfig>(app => app.database) // Now it works with AppConfig const url = getDbFromAppConfig.run({ database: { host: 'localhost', port: 5432, name: 'myapp' }, // other app config... }) ``` ### Combining Readers ```typescript // Combine multiple Readers into an array of results const getName = asks<User, string>(u => u.name) const getAge = asks<User, number>(u => u.age) const getEmail = asks<User, string>(u => u.email) const getUserInfo = sequence([getName, getAge, getEmail]) // getUserInfo.run(user) returns [name, age, email] // Combine multiple Readers with a mapping function const getUserSummary = combine( [getName, getAge, getEmail], (name, age, email) => `${name} (${age}) - ${email}` ) // getUserSummary.run(user) returns "Alice (30) - alice@example.com" // Combine two Readers with a binary function const greeting = asks<Config, string>(c => c.greeting) const username = asks<Config, string>(c => c.username) const personalizedGreeting = greeting.zipWith( username, (greet, name) => `${greet}, ${name}!` ) // personalizedGreeting.run({greeting: "Hello", username: "Bob"}) returns "Hello, Bob!" ``` ## Advanced Features ### Side Effects ```typescript // Execute a side effect without changing the Reader value const loggedApiUrl = getApiUrl.tap(url => console.log(`Using API URL: ${url}`)) // Chain Readers for sequencing operations const logAndGetUser = loggerReader.andThen(getUserReader) ``` ### Environment-Aware Transformations ```typescript // Transform using both environment and current value const getTemplate = asks<MessageConfig, string>(c => c.template) const getMessage = getTemplate.withEnv( (config, template) => template.replace('{user}', config.currentUser) ) ``` ### Filtering and Multiple Transformations ```typescript // Filter values based on a predicate const getAge = asks<Person, number>(p => p.age) const getValidAge = getAge.filter( age => age >= 0 && age <= 120, 0 // Default for invalid ages ) // Apply multiple transformations to the same value const getUserStats = getUser.fanout( user => user.loginCount, user => user.lastActive, user => user.preferences.theme ) // Returns [loginCount, lastActive, theme] ``` ### Async Integration and Performance ```typescript // Convert a Reader to a Promise-returning function const processConfig = asks<Config, Result>(c => computeResult(c)) const processAsync = processConfig.toPromise() // Later in async code const result = await processAsync(myConfig) // Cache expensive Reader operations const expensiveReader = reader<Config, Result>(c => expensiveComputation(c)).memoize() ``` ## Real-World Examples ### Dependency Injection ```typescript // Define dependencies interface interface AppDependencies { logger: Logger database: Database apiClient: ApiClient } // Create Readers for each dependency const getLogger = asks<AppDependencies, Logger>(deps => deps.logger) const getDb = asks<AppDependencies, Database>(deps => deps.database) const getApiClient = asks<AppDependencies, ApiClient>(deps => deps.apiClient) // Create business logic using dependencies const getUserById = (id: string) => combine( [getDb, getLogger], (db, logger) => { logger.info(`Fetching user ${id}`) return db.users.findById(id) } ) // Configure the real dependencies const dependencies: AppDependencies = { logger: new ConsoleLogger(), database: new PostgresDatabase(dbConfig), apiClient: new HttpApiClient(apiConfig) } // Run the Reader with the dependencies const user = getUserById('123').run(dependencies) ``` ### Configuration Management ```typescript // Different sections of configuration const getDbConfig = asks<AppConfig, DbConfig>(c => c.database) const getApiConfig = asks<AppConfig, ApiConfig>(c => c.api) const getEnvironment = asks<AppConfig, string>(c => c.environment) // Create environment-specific database URL const getDatabaseUrl = combine( [getDbConfig, getEnvironment], (db, env) => { const { host, port, name, user, password } = db const dbName = env === 'test' ? `${name}_test` : name return `postgres://${user}:${password}@${host}:${port}/${dbName}` } ) ``` ## Benefits Over Direct Approaches | Problem | Traditional Approach | Reader Monad Solution | |---------|---------------------|------------------------| | Dependency Injection | Constructor injection, service locators | Implicit dependencies via the environment | | Configuration | Passing config objects, globals | Environment access in pure functions | | Testing | Mocking, dependency overrides | Simply passing different environments | | Composition | Complex chaining with explicit parameters | Clean composition with flatMap and combine | | Reuse | Duplicating config parameters | Single environment shared by multiple Readers |