Implementing a Generic Result<T, E> Type for Robust Error Handling
Traditional error handling often relies on exceptions, which can make control flow harder to reason about and force callers to guess what errors might occur. The Result type offers a more explicit and functional approach to error management, enabling functions to clearly declare that they might either succeed with a value or fail with an error. This challenge asks you to design and implement such a Result type.
Problem Description
Your task is to implement a generic Result type that can encapsulate the outcome of an operation. This type should clearly distinguish between a successful result containing a value and a failed result containing an error.
Specifically, your Result type should:
- Be Generic: It must be parameterized by two types:
T(for the success value type) andE(for the error value type). - Have Two States: It should internally represent either an "Ok" state (holding a value of type
T) or an "Err" state (holding an error value of typeE). - Provide Constructors:
- A way to create an "Ok" instance with a
Tvalue. - A way to create an "Err" instance with an
Evalue.
- A way to create an "Ok" instance with a
- Provide Inspection Methods:
is_ok(): Returnstrueif theResultis in the "Ok" state,falseotherwise.is_err(): Returnstrueif theResultis in the "Err" state,falseotherwise.
- Provide Value Access Methods:
unwrap_or(default_value: T): If theResultis "Ok", return its containedTvalue. If it's "Err", return thedefault_valueprovided.unwrap_err_or(default_error: E): If theResultis "Err", return its containedEvalue. If it's "Ok", return thedefault_errorprovided.- (Optional, but recommended for completeness)
map(transform_function): If theResultis "Ok", applytransform_functionto its contained value and return a newResultcontaining the transformed value. If "Err", return the originalErrunchanged. Thetransform_functionshould take aTand returnU, resulting inResult<U, E>.
Your implementation should ensure that it's impossible to access a success value from an "Err" state (without providing a default), and vice-versa, promoting type safety and explicit error handling.
Examples
Assume we have a function divide(a: Number, b: Number) -> Result<Number, String> that attempts to divide two numbers.
Example 1: Successful Operation
// Define an error type
type DivisionError = String
// Function signature
function divide(numerator: Number, denominator: Number) -> Result<Number, DivisionError>
if denominator == 0 then
return Result.Err("Cannot divide by zero")
else
return Result.Ok(numerator / denominator)
end if
end function
// Usage
result1 = divide(10, 2)
Input: result1
Output: result1.is_ok() == true
result1.is_err() == false
result1.unwrap_or(0) == 5
Explanation: The division 10 / 2 is successful, so the result is an 'Ok' variant holding the value 5.
Example 2: Failed Operation
// Usage
result2 = divide(10, 0)
Input: result2
Output: result2.is_ok() == false
result2.is_err() == true
result2.unwrap_or(0) == 0
result2.unwrap_err_or("No Error") == "Cannot divide by zero"
Explanation: Division by zero is not allowed, so the function returns an 'Err' variant containing the error message "Cannot divide by zero". `unwrap_or(0)` correctly returns the default because it's an error.
Example 3: Chaining with map
// Assume 'divide' function from above exists.
// Also assume we want to double the result if successful.
result3 = divide(20, 4) // This will be Result.Ok(5)
doubled_result = result3.map(function (value: Number) -> Number
return value * 2
end function)
Input: doubled_result
Output: doubled_result.is_ok() == true
doubled_result.unwrap_or(0) == 10
Explanation: Since `result3` was `Ok(5)`, the `map` function was applied to 5, resulting in 10. A new `Result.Ok(10)` is returned.
result4 = divide(10, 0) // This will be Result.Err("Cannot divide by zero")
doubled_error_result = result4.map(function (value: Number) -> Number
return value * 2
end function)
Input: doubled_error_result
Output: doubled_error_result.is_err() == true
doubled_error_result.unwrap_err_or("No Error") == "Cannot divide by zero"
Explanation: Since `result4` was `Err("Cannot divide by zero")`, the `map` function was *not* applied. The `Err` value is preserved and returned as `doubled_error_result`.
Constraints
- The
Resulttype must be generic, accepting any type for its success value (T) and any type for its error value (E). - Your implementation should not rely on language-specific exception handling mechanisms for its core logic; the
Resulttype itself is the error handling mechanism. - The internal state of a
Resultinstance should be immutable after creation. - Memory usage should be efficient, avoiding unnecessary allocations when switching states. (e.g., it shouldn't hold both a
TandEvalue simultaneously, only one or the other).
Notes
- Think about how to represent the two distinct states (
OkandErr) within your chosen language's type system. Discriminated unions, enums with associated values, or sealed classes are common patterns. - Consider making your
Resulttype easy to compose with other functions that also returnResulttypes. Whilemapis a good start, functions likeand_then(often calledflat_map) are powerful for chaining operations that might fail. - This pattern encourages callers to explicitly handle both success and error cases, improving code clarity and robustness.
- Focus on the core functionality first, ensuring correct state management and access. Advanced methods like
mapcan be added incrementally.