Hone logo
Hone
Problems

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:

  1. Be Generic: It must be parameterized by two types: T (for the success value type) and E (for the error value type).
  2. 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 type E).
  3. Provide Constructors:
    • A way to create an "Ok" instance with a T value.
    • A way to create an "Err" instance with an E value.
  4. Provide Inspection Methods:
    • is_ok(): Returns true if the Result is in the "Ok" state, false otherwise.
    • is_err(): Returns true if the Result is in the "Err" state, false otherwise.
  5. Provide Value Access Methods:
    • unwrap_or(default_value: T): If the Result is "Ok", return its contained T value. If it's "Err", return the default_value provided.
    • unwrap_err_or(default_error: E): If the Result is "Err", return its contained E value. If it's "Ok", return the default_error provided.
    • (Optional, but recommended for completeness) map(transform_function): If the Result is "Ok", apply transform_function to its contained value and return a new Result containing the transformed value. If "Err", return the original Err unchanged. The transform_function should take a T and return U, resulting in Result<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 Result type 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 Result type itself is the error handling mechanism.
  • The internal state of a Result instance should be immutable after creation.
  • Memory usage should be efficient, avoiding unnecessary allocations when switching states. (e.g., it shouldn't hold both a T and E value simultaneously, only one or the other).

Notes

  • Think about how to represent the two distinct states (Ok and Err) within your chosen language's type system. Discriminated unions, enums with associated values, or sealed classes are common patterns.
  • Consider making your Result type easy to compose with other functions that also return Result types. While map is a good start, functions like and_then (often called flat_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 map can be added incrementally.
Loading editor...
plaintext