Skip to content

Commit 2e0d3bc

Browse files
feat(reader): add comprehensive documentation for Reader monad (#208)
Added detailed descriptions and examples to the Reader monad source code, including usage of various methods and static functions. This includes creation, transformation, environment manipulation, combination of Readers, and advanced features such as memoization, async integration, and side effects. Updated test specifications to reflect the new functionalities and documentations. Additionally, renamed `pubcli_api.spec.ts` files to `public_api.spec.ts` across relevant directories for consistency in specification naming.
1 parent e7837e2 commit 2e0d3bc

File tree

9 files changed

+1568
-29
lines changed

9 files changed

+1568
-29
lines changed

src/reader/README.md

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# Reader Monad
2+
3+
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.
4+
5+
## Core Concept
6+
7+
A Reader monad is essentially a function that:
8+
9+
1. Takes an environment/configuration as input
10+
2. Performs a computation using that environment
11+
3. Returns a result
12+
13+
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.
14+
15+
## Key Benefits
16+
17+
- **Dependency Injection**: Pass dependencies around implicitly without global state
18+
- **Testability**: Easily swap environments for testing
19+
- **Composability**: Combine small, focused Readers into complex operations
20+
- **Type Safety**: Environment requirements are encoded in the type system
21+
- **Pure Functional**: No side effects or hidden dependencies
22+
- **Lazy Evaluation**: Operations are only run when the final Reader is executed
23+
24+
## Basic Usage
25+
26+
```typescript
27+
import { reader, asks } from 'typescript-monads'
28+
29+
// Create a Reader that extracts a value from the environment
30+
const getApiUrl = asks<AppConfig, string>(config => config.apiUrl)
31+
32+
// Create a Reader that uses the environment to format a URL
33+
const getFullUrl = reader<AppConfig, string>(config =>
34+
`${config.apiUrl}/users?token=${config.authToken}`
35+
)
36+
37+
// Run the Reader with a configuration
38+
const url = getFullUrl.run({
39+
apiUrl: 'https://api.example.com',
40+
authToken: '12345'
41+
}) // "https://api.example.com/users?token=12345"
42+
```
43+
44+
## Core Operations
45+
46+
### Creation
47+
48+
```typescript
49+
// Create from a function that uses the environment
50+
const greeting = reader<{name: string}, string>(env => `Hello, ${env.name}!`)
51+
52+
// Create a Reader that always returns a constant value (ignores environment)
53+
const constant = readerOf<any, number>(42)
54+
55+
// Create a Reader that returns the entire environment
56+
const getEnv = ask<AppConfig>()
57+
58+
// Create a Reader that extracts a specific value from the environment
59+
const getTimeout = asks<AppConfig, number>(config => config.timeout)
60+
```
61+
62+
### Transformation
63+
64+
```typescript
65+
// Map the output value
66+
const getApiUrl = asks<Config, string>(c => c.apiUrl)
67+
const getApiUrlUpper = getApiUrl.map(url => url.toUpperCase())
68+
69+
// Chain Readers
70+
const getUser = asks<Config, User>(c => c.currentUser)
71+
const getPermissions = (user: User) =>
72+
asks<Config, string[]>(c => c.permissionsDb.getPermissionsFor(user.id))
73+
74+
// Combined operation: get user and their permissions
75+
const getUserPermissions = getUser.flatMap(getPermissions)
76+
```
77+
78+
### Environment Manipulation
79+
80+
```typescript
81+
// Create a Reader that works with a specific config type
82+
const getDatabaseUrl = reader<DbConfig, string>(db =>
83+
`postgres://${db.host}:${db.port}/${db.name}`
84+
)
85+
86+
// Adapt it to work with a different environment type
87+
const getDbFromAppConfig = getDatabaseUrl.local<AppConfig>(app => app.database)
88+
89+
// Now it works with AppConfig
90+
const url = getDbFromAppConfig.run({
91+
database: { host: 'localhost', port: 5432, name: 'myapp' },
92+
// other app config...
93+
})
94+
```
95+
96+
### Combining Readers
97+
98+
```typescript
99+
// Combine multiple Readers into an array of results
100+
const getName = asks<User, string>(u => u.name)
101+
const getAge = asks<User, number>(u => u.age)
102+
const getEmail = asks<User, string>(u => u.email)
103+
104+
const getUserInfo = sequence([getName, getAge, getEmail])
105+
// getUserInfo.run(user) returns [name, age, email]
106+
107+
// Combine multiple Readers with a mapping function
108+
const getUserSummary = combine(
109+
[getName, getAge, getEmail],
110+
(name, age, email) => `${name} (${age}) - ${email}`
111+
)
112+
// getUserSummary.run(user) returns "Alice (30) - alice@example.com"
113+
114+
// Combine two Readers with a binary function
115+
const greeting = asks<Config, string>(c => c.greeting)
116+
const username = asks<Config, string>(c => c.username)
117+
118+
const personalizedGreeting = greeting.zipWith(
119+
username,
120+
(greet, name) => `${greet}, ${name}!`
121+
)
122+
// personalizedGreeting.run({greeting: "Hello", username: "Bob"}) returns "Hello, Bob!"
123+
```
124+
125+
## Advanced Features
126+
127+
### Side Effects
128+
129+
```typescript
130+
// Execute a side effect without changing the Reader value
131+
const loggedApiUrl = getApiUrl.tap(url => console.log(`Using API URL: ${url}`))
132+
133+
// Chain Readers for sequencing operations
134+
const logAndGetUser = loggerReader.andThen(getUserReader)
135+
```
136+
137+
### Environment-Aware Transformations
138+
139+
```typescript
140+
// Transform using both environment and current value
141+
const getTemplate = asks<MessageConfig, string>(c => c.template)
142+
const getMessage = getTemplate.withEnv(
143+
(config, template) => template.replace('{user}', config.currentUser)
144+
)
145+
```
146+
147+
### Filtering and Multiple Transformations
148+
149+
```typescript
150+
// Filter values based on a predicate
151+
const getAge = asks<Person, number>(p => p.age)
152+
const getValidAge = getAge.filter(
153+
age => age >= 0 && age <= 120,
154+
0 // Default for invalid ages
155+
)
156+
157+
// Apply multiple transformations to the same value
158+
const getUserStats = getUser.fanout(
159+
user => user.loginCount,
160+
user => user.lastActive,
161+
user => user.preferences.theme
162+
)
163+
// Returns [loginCount, lastActive, theme]
164+
```
165+
166+
### Async Integration and Performance
167+
168+
```typescript
169+
// Convert a Reader to a Promise-returning function
170+
const processConfig = asks<Config, Result>(c => computeResult(c))
171+
const processAsync = processConfig.toPromise()
172+
173+
// Later in async code
174+
const result = await processAsync(myConfig)
175+
176+
// Cache expensive Reader operations
177+
const expensiveReader = reader<Config, Result>(c => expensiveComputation(c)).memoize()
178+
```
179+
180+
## Real-World Examples
181+
182+
### Dependency Injection
183+
184+
```typescript
185+
// Define dependencies interface
186+
interface AppDependencies {
187+
logger: Logger
188+
database: Database
189+
apiClient: ApiClient
190+
}
191+
192+
// Create Readers for each dependency
193+
const getLogger = asks<AppDependencies, Logger>(deps => deps.logger)
194+
const getDb = asks<AppDependencies, Database>(deps => deps.database)
195+
const getApiClient = asks<AppDependencies, ApiClient>(deps => deps.apiClient)
196+
197+
// Create business logic using dependencies
198+
const getUserById = (id: string) => combine(
199+
[getDb, getLogger],
200+
(db, logger) => {
201+
logger.info(`Fetching user ${id}`)
202+
return db.users.findById(id)
203+
}
204+
)
205+
206+
// Configure the real dependencies
207+
const dependencies: AppDependencies = {
208+
logger: new ConsoleLogger(),
209+
database: new PostgresDatabase(dbConfig),
210+
apiClient: new HttpApiClient(apiConfig)
211+
}
212+
213+
// Run the Reader with the dependencies
214+
const user = getUserById('123').run(dependencies)
215+
```
216+
217+
### Configuration Management
218+
219+
```typescript
220+
// Different sections of configuration
221+
const getDbConfig = asks<AppConfig, DbConfig>(c => c.database)
222+
const getApiConfig = asks<AppConfig, ApiConfig>(c => c.api)
223+
const getEnvironment = asks<AppConfig, string>(c => c.environment)
224+
225+
// Create environment-specific database URL
226+
const getDatabaseUrl = combine(
227+
[getDbConfig, getEnvironment],
228+
(db, env) => {
229+
const { host, port, name, user, password } = db
230+
const dbName = env === 'test' ? `${name}_test` : name
231+
return `postgres://${user}:${password}@${host}:${port}/${dbName}`
232+
}
233+
)
234+
```
235+
236+
## Benefits Over Direct Approaches
237+
238+
| Problem | Traditional Approach | Reader Monad Solution |
239+
|---------|---------------------|------------------------|
240+
| Dependency Injection | Constructor injection, service locators | Implicit dependencies via the environment |
241+
| Configuration | Passing config objects, globals | Environment access in pure functions |
242+
| Testing | Mocking, dependency overrides | Simply passing different environments |
243+
| Composition | Complex chaining with explicit parameters | Clean composition with flatMap and combine |
244+
| Reuse | Duplicating config parameters | Single environment shared by multiple Readers |

src/reader/pubcli_api.spec.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/reader/public_api.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { reader, readerOf, ask, asks, sequence, traverse, combine, Reader } from './public_api'
2+
3+
describe('Reader monad public API', () => {
4+
it('should export and create instances correctly', () => {
5+
expect(reader(() => { return 1 })).toBeInstanceOf(Reader)
6+
})
7+
8+
it('should export all factory functions', () => {
9+
expect(reader).toBeDefined()
10+
expect(readerOf).toBeDefined()
11+
expect(ask).toBeDefined()
12+
expect(asks).toBeDefined()
13+
expect(sequence).toBeDefined()
14+
expect(traverse).toBeDefined()
15+
expect(combine).toBeDefined()
16+
})
17+
18+
it('should export the Reader class', () => {
19+
expect(Reader).toBeDefined()
20+
expect(Reader.of).toBeDefined()
21+
expect(Reader.ask).toBeDefined()
22+
expect(Reader.asks).toBeDefined()
23+
expect(Reader.sequence).toBeDefined()
24+
expect(Reader.traverse).toBeDefined()
25+
expect(Reader.combine).toBeDefined()
26+
})
27+
28+
it('should create and run Readers correctly', () => {
29+
const r1 = reader<{name: string}, string>(env => `Hello, ${env.name}!`)
30+
const r2 = asks<{name: string}, string>(env => env.name)
31+
const r3 = readerOf<{name: string}, number>(42)
32+
33+
expect(r1.run({name: 'Alice'})).toBe('Hello, Alice!')
34+
expect(r2.run({name: 'Bob'})).toBe('Bob')
35+
expect(r3.run({name: 'Charlie'})).toBe(42)
36+
})
37+
})

0 commit comments

Comments
 (0)