Skip to content

Commit

Permalink
Enhanced result package, rust should be proud
Browse files Browse the repository at this point in the history
  • Loading branch information
theHamdiz committed Feb 4, 2025
1 parent 0a48f29 commit 62b8329
Show file tree
Hide file tree
Showing 3 changed files with 624 additions and 3 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,12 +379,46 @@ Now go forth and fail gracefully, because that's what mature code does.
```go
import "github.com/theHamdiz/it/result"

// For the optimists
res := result.Ok("success")
if res.IsOk() {
value := res.UnwrapOr("plan B")
}

// For the realists
res := result.Try(func() string {
return maybeExplode()
}).OrElse(func() Result[string] {
return Ok("at least we tried")
})

// For the functional programming enthusiasts
result.Ok(42).
Filter(isNotTooLarge).
Map(addOne).
Match(
func(n int) { fmt.Println("Yay:", n) },
func(err error) { /* pretend this never happened */ },
)

// For those who like to live dangerously
value := result.Some(42).UnwrapOrPanic() // YOLO

// For the overachievers
pairs := result.Zip(
result.Ok("hello"),
result.Ok(42),
) // Because one value isn't complicated enough

// For when you need to fail collectively
results := result.Collect([]Result[int]{
Ok(1), Ok(2), Err[int](errors.New("oops"))
}) // Misery loves company
```
Because if you're going to handle errors, you might as well do it with style.
Now with 100% more Option types, because nil was getting lonely.
### Math - For The Algorithmically Gifted
```go
Expand Down
171 changes: 168 additions & 3 deletions result/result.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
// Package result - Because null checks are so last century
// Also yes, that's two "u"s. Deal with it.
// Package result - Because nil checks are so last century
package result

import (
"errors"
"fmt"
)

// Result represents a value that might exist
// Or might be busy generating stack traces
type Result[T any] struct {
value T // The thing you want
err error // The thing you'll probably get
}

// Pair holds two values of potentially different types
type Pair[T, U any] struct {
First T
Second U
}

// Option represents a value that may or may not exist
type Option[T any] struct {
value T
valid bool
}

// Some creates an Option containing a value
func Some[T any](value T) Option[T] {
return Option[T]{
value: value,
valid: true,
}
}

// None creates an empty Option
func None[T any]() Option[T] {
return Option[T]{
valid: false,
}
}

// NewResult creates a new coin flip of success/failure
func NewResult[T any](value T, err error) Result[T] {
return Result[T]{value: value, err: err}
Expand All @@ -22,10 +53,15 @@ func Ok[T any](value T) Result[T] {

// Err creates a Result for when reality meets expectations
func Err[T any](err error) Result[T] {
var zero T // Because null isn't painful enough
var zero T // Because nil isn't painful enough
return Result[T]{value: zero, err: err}
}

// Err returns the error if present, nil otherwise
func (r Result[T]) Err() error {
return r.err
}

// IsOk checks if your optimism was justified
func (r Result[T]) IsOk() bool {
return r.err == nil
Expand Down Expand Up @@ -133,3 +169,132 @@ func FlatMap[T, U any](r Result[T], fn func(T) Result[U]) Result[U] {
}
return fn(r.value) // Your second chance to fail
}

// Match is for when you want to handle both cases elegantly
func (r Result[T]) Match(ok func(T), err func(error)) {
if r.IsErr() {
err(r.err)
} else {
ok(r.value)
}
}

// Filter turns success into failure if a condition isn't met
func (r Result[T]) Filter(predicate func(T) bool) Result[T] {
if !r.IsOk() || !predicate(r.value) {
return Err[T](errors.New("predicate failed"))
}
return r
}

// Inspect lets you peek at success without changing it
func (r Result[T]) Inspect(fn func(T)) Result[T] {
if r.IsOk() {
fn(r.value)
}
return r
}

// InspectErr lets you peek at failure without changing it
func (r Result[T]) InspectErr(fn func(error)) Result[T] {
if r.IsErr() {
fn(r.err)
}
return r
}

// Transpose converts Result[Option[T]] to Option[Result[T]]
func Transpose[T any](r Result[Option[T]]) Option[Result[T]] {
if r.IsErr() {
return Some(Err[T](r.err))
}
opt := r.UnwrapOrPanic()
if opt.IsNone() {
return None[Result[T]]()
}
return Some(Ok(opt.UnwrapOrPanic()))
}

// Collect turns a slice of Results into a Result of slice
func Collect[T any](results []Result[T]) Result[[]T] {
values := make([]T, 0, len(results))
for _, r := range results {
if r.IsErr() {
return Err[[]T](r.err)
}
values = append(values, r.UnwrapOrPanic())
}
return Ok(values)
}

// Try converts a panic into a Result
func Try[T any](fn func() T) Result[T] {
var result T
var err error

func() {
defer func() {
if r := recover(); r != nil {
switch v := r.(type) {
case error:
err = v
case string:
err = errors.New(v)
default:
err = fmt.Errorf("panic: %v", r)
}
}
}()
result = fn()
}()

if err != nil {
return Err[T](err)
}
return Ok(result)
}

// FromOption converts an Option to a Result with custom error
func FromOption[T any](opt Option[T], err error) Result[T] {
if opt.IsNone() {
return Err[T](err)
}
return Ok(opt.UnwrapOrPanic())
}

// Zip combines two Results into one
func Zip[T, U any](r1 Result[T], r2 Result[U]) Result[Pair[T, U]] {
if r1.IsErr() {
return Err[Pair[T, U]](r1.err)
}
if r2.IsErr() {
return Err[Pair[T, U]](r2.err)
}
return Ok(Pair[T, U]{First: r1.value, Second: r2.value})
}

// IsNone returns true if the Option contains no value
func (o Option[T]) IsNone() bool {
return !o.valid
}

// IsSome returns true if the Option contains a value
func (o Option[T]) IsSome() bool {
return o.valid
}

// UnwrapOr returns the contained value or a default
func (o Option[T]) UnwrapOr(defaultValue T) T {
if o.valid {
return o.value
}
return defaultValue
}

// UnwrapOrPanic returns the contained value or panics
func (o Option[T]) UnwrapOrPanic() T {
if !o.valid {
panic("called UnwrapOrPanic on None value")
}
return o.value
}
Loading

0 comments on commit 62b8329

Please sign in to comment.