0
# Raise DSL
1
2
The Raise DSL provides modern typed error handling with short-circuiting, error recovery, and accumulation patterns. It allows you to work with logical failures in a structured way, replacing exception-based error handling with typed errors.
3
4
## Core Interface
5
6
```kotlin { .api }
7
interface Raise<in Error> {
8
// Raise a logical failure and short-circuit
9
fun raise(r: Error): Nothing
10
11
// Bind operations for extracting success values
12
fun <A> Either<Error, A>.bind(): A
13
fun <A> Option<A>.bind(): A // raises None for None case
14
fun <A> EagerEffect<Error, A>.bind(): A
15
suspend fun <A> Effect<Error, A>.bind(): A
16
}
17
18
// DSL marker
19
@DslMarker
20
annotation class RaiseDSL
21
```
22
23
## DSL Builders
24
25
```kotlin { .api }
26
// Build Either using Raise DSL
27
fun <E, A> either(block: Raise<E>.() -> A): Either<E, A>
28
29
// Build Option using Raise DSL (raises None)
30
fun <A> option(block: Raise<None>.() -> A): Option<A>
31
32
// Error recovery
33
fun <E, A> recover(block: Raise<E>.() -> A, recover: (E) -> A): A
34
35
// Error transformation
36
fun <E, EE, A> recover(block: Raise<E>.() -> A, transform: Raise<EE>.(E) -> A): Either<EE, A>
37
```
38
39
## Validation Functions
40
41
```kotlin { .api }
42
// In Raise context
43
fun Raise<Error>.ensure(condition: Boolean, raise: () -> Error)
44
fun <A> Raise<Error>.ensureNotNull(value: A?, raise: () -> Error): A
45
```
46
47
## Error Accumulation
48
49
```kotlin { .api }
50
// Accumulate errors from multiple operations
51
fun <Error, A, B, Z> mapOrAccumulate(
52
a: () -> A,
53
b: () -> B,
54
transform: (A, B) -> Z
55
): Either<NonEmptyList<Error>, Z>
56
57
// With custom error combining
58
fun <Error, A, B, Z> mapOrAccumulate(
59
combine: (Error, Error) -> Error,
60
a: () -> A,
61
b: () -> B,
62
transform: (A, B) -> Z
63
): Either<Error, Z>
64
```
65
66
## Exception Handling
67
68
```kotlin { .api }
69
// Catch exceptions in Raise context
70
fun <E, A> catch(
71
raise: (Throwable) -> E,
72
block: () -> A
73
): Either<E, A>
74
75
// Catch specific exceptions
76
fun <E, T : Throwable, A> catch(
77
raise: (T) -> E,
78
block: () -> A
79
): Either<E, A>
80
```
81
82
## Usage Examples
83
84
### Basic Error Handling
85
86
```kotlin
87
import arrow.core.raise.*
88
import arrow.core.*
89
90
// Define error types
91
sealed class DivisionError {
92
object DivisionByZero : DivisionError()
93
data class InvalidInput(val input: String) : DivisionError()
94
}
95
96
// Function using Raise DSL
97
fun Raise<DivisionError>.safeDivide(x: String, y: String): Double {
98
val xNum = x.toDoubleOrNull()
99
?: raise(DivisionError.InvalidInput(x))
100
val yNum = y.toDoubleOrNull()
101
?: raise(DivisionError.InvalidInput(y))
102
103
ensure(yNum != 0.0) { DivisionError.DivisionByZero }
104
105
return xNum / yNum
106
}
107
108
// Use with either builder
109
val result1 = either { safeDivide("10", "2") } // Right(5.0)
110
val result2 = either { safeDivide("10", "0") } // Left(DivisionByZero)
111
val result3 = either { safeDivide("abc", "2") } // Left(InvalidInput("abc"))
112
```
113
114
### Error Recovery
115
116
```kotlin
117
import arrow.core.raise.*
118
import arrow.core.*
119
120
fun Raise<String>.parseAge(input: String): Int {
121
val age = input.toIntOrNull() ?: raise("Invalid number format")
122
ensure(age >= 0) { "Age cannot be negative" }
123
ensure(age <= 150) { "Age seems unrealistic" }
124
return age
125
}
126
127
// Recover with fallback value
128
val age1 = recover({ parseAge("25") }) { error ->
129
println("Error: $error")
130
0 // fallback
131
} // Result: 25
132
133
val age2 = recover({ parseAge("invalid") }) { error ->
134
println("Error: $error")
135
18 // default adult age
136
} // Result: 18
137
```
138
139
### Binding Operations
140
141
```kotlin
142
import arrow.core.raise.*
143
import arrow.core.*
144
145
data class User(val id: Int, val name: String, val email: String)
146
147
fun findUser(id: Int): Either<String, User> =
148
if (id > 0) User(id, "Alice", "alice@example.com").right()
149
else "Invalid user ID".left()
150
151
fun validateEmail(email: String): Option<String> =
152
if (email.contains('@')) email.some() else none()
153
154
// Combine operations using bind
155
fun Raise<String>.processUser(id: Int): String {
156
val user = findUser(id).bind() // Extracts User or raises String error
157
val validEmail = validateEmail(user.email).bind() // Raises None if invalid
158
return "User ${user.name} has valid email: $validEmail"
159
}
160
161
val result = either { processUser(1) }
162
// Right("User Alice has valid email: alice@example.com")
163
```
164
165
### Error Accumulation
166
167
```kotlin
168
import arrow.core.raise.*
169
import arrow.core.*
170
171
data class ValidationError(val field: String, val message: String)
172
data class UserForm(val name: String, val email: String, val age: Int)
173
174
fun Raise<ValidationError>.validateName(name: String): String {
175
ensure(name.isNotBlank()) { ValidationError("name", "Name is required") }
176
ensure(name.length >= 2) { ValidationError("name", "Name too short") }
177
return name
178
}
179
180
fun Raise<ValidationError>.validateEmail(email: String): String {
181
ensure(email.isNotBlank()) { ValidationError("email", "Email is required") }
182
ensure(email.contains('@')) { ValidationError("email", "Invalid email") }
183
return email
184
}
185
186
fun Raise<ValidationError>.validateAge(age: Int): Int {
187
ensure(age >= 18) { ValidationError("age", "Must be 18 or older") }
188
ensure(age <= 100) { ValidationError("age", "Age seems unrealistic") }
189
return age
190
}
191
192
// Accumulate all validation errors
193
fun validateUserForm(name: String, email: String, age: Int): Either<NonEmptyList<ValidationError>, UserForm> =
194
mapOrAccumulate(
195
{ validateName(name) },
196
{ validateEmail(email) },
197
{ validateAge(age) }
198
) { validName, validEmail, validAge ->
199
UserForm(validName, validEmail, validAge)
200
}
201
202
val valid = validateUserForm("Alice", "alice@example.com", 25)
203
// Right(UserForm("Alice", "alice@example.com", 25))
204
205
val invalid = validateUserForm("", "invalid-email", 16)
206
// Left(NonEmptyList(
207
// ValidationError("name", "Name is required"),
208
// ValidationError("email", "Invalid email"),
209
// ValidationError("age", "Must be 18 or older")
210
// ))
211
```
212
213
### Exception Integration
214
215
```kotlin
216
import arrow.core.raise.*
217
import arrow.core.*
218
219
sealed class FileError {
220
data class FileNotFound(val path: String) : FileError()
221
data class PermissionDenied(val path: String) : FileError()
222
data class IOError(val message: String) : FileError()
223
}
224
225
fun Raise<FileError>.readFile(path: String): String {
226
return catch(
227
raise = { throwable ->
228
when (throwable) {
229
is java.io.FileNotFoundException ->
230
FileError.FileNotFound(path)
231
is java.io.IOException ->
232
FileError.IOError(throwable.message ?: "Unknown IO error")
233
else -> FileError.IOError("Unexpected error: ${throwable.message}")
234
}
235
}
236
) {
237
// This could throw various exceptions
238
java.io.File(path).readText()
239
}.bind()
240
}
241
242
val content = either { readFile("config.txt") }
243
```
244
245
### Complex Workflows
246
247
```kotlin
248
import arrow.core.raise.*
249
import arrow.core.*
250
251
// Multi-step process with different error types
252
sealed class ProcessingError {
253
data class ParseError(val input: String) : ProcessingError()
254
data class ValidationError(val message: String) : ProcessingError()
255
data class TransformationError(val stage: String) : ProcessingError()
256
}
257
258
fun Raise<ProcessingError>.parseInput(input: String): Map<String, Any> {
259
ensure(input.isNotBlank()) { ProcessingError.ParseError("Empty input") }
260
// Simulate parsing JSON-like input
261
return mapOf("data" to input)
262
}
263
264
fun Raise<ProcessingError>.validateData(data: Map<String, Any>): Map<String, Any> {
265
ensure(data.containsKey("data")) {
266
ProcessingError.ValidationError("Missing data field")
267
}
268
return data
269
}
270
271
fun Raise<ProcessingError>.transformData(data: Map<String, Any>): String {
272
val value = data["data"] as? String
273
?: raise(ProcessingError.TransformationError("Data conversion"))
274
return value.uppercase()
275
}
276
277
// Chain operations in Raise context
278
fun Raise<ProcessingError>.processWorkflow(input: String): String {
279
val parsed = parseInput(input)
280
val validated = validateData(parsed)
281
return transformData(validated)
282
}
283
284
// Execute with error recovery
285
val workflow1 = recover({ processWorkflow("hello world") }) { error ->
286
when (error) {
287
is ProcessingError.ParseError -> "PARSE_ERROR"
288
is ProcessingError.ValidationError -> "VALIDATION_ERROR"
289
is ProcessingError.TransformationError -> "TRANSFORM_ERROR"
290
}
291
} // Result: "HELLO WORLD"
292
293
val workflow2 = recover({ processWorkflow("") }) { error ->
294
"ERROR: $error"
295
} // Result: "ERROR: ParseError(Empty input)"
296
```
297
298
### Integration with Option
299
300
```kotlin
301
import arrow.core.raise.*
302
import arrow.core.*
303
304
fun findUserById(id: Int): Option<String> =
305
if (id in 1..10) "user_$id".some() else none()
306
307
// Using Option with Raise DSL
308
fun Raise<String>.processUserChain(id: Int): String {
309
val user = findUserById(id).bind() // Raises "None" if Option is None
310
return "Processing: $user"
311
}
312
313
// Handle None as string error
314
val result1 = recover({ processUserChain(5) }) {
315
"User not found"
316
} // Result: "Processing: user_5"
317
318
val result2 = recover({ processUserChain(15) }) {
319
"User not found"
320
} // Result: "User not found"
321
```