Merge pull request #9528 from Roasbeef/res-opt

fn: implement ResultOpt type for operations with optional values
This commit is contained in:
Olaoluwa Osuntokun
2025-06-18 18:22:11 -07:00
committed by GitHub
2 changed files with 200 additions and 0 deletions

64
fn/result_opt.go Normal file
View File

@@ -0,0 +1,64 @@
package fn
// ResultOpt represents an operation that may either fail (with an error) or
// succeed with an optional final value.
type ResultOpt[T any] struct {
Result[Option[T]]
}
// OkOpt constructs a successful ResultOpt with a present value.
func OkOpt[T any](val T) ResultOpt[T] {
return ResultOpt[T]{Ok(Some(val))}
}
// NoneOpt constructs a successful ResultOpt with no final value.
func NoneOpt[T any]() ResultOpt[T] {
return ResultOpt[T]{Ok(None[T]())}
}
// ErrOpt constructs a failed ResultOpt with the provided error.
func ErrOpt[T any](err error) ResultOpt[T] {
return ResultOpt[T]{Err[Option[T]](err)}
}
// MapResultOpt applies a function to the final value of a successful operation.
func MapResultOpt[T, U any](ro ResultOpt[T], f func(T) U) ResultOpt[U] {
if ro.IsErr() {
return ErrOpt[U](ro.Err())
}
opt, _ := ro.Unpack()
return ResultOpt[U]{Ok(MapOption(f)(opt))}
}
// AndThenResultOpt applies a function to the final value of a successful
// operation.
func AndThenResultOpt[T, U any](ro ResultOpt[T],
f func(T) ResultOpt[U]) ResultOpt[U] {
if ro.IsErr() {
return ErrOpt[U](ro.Err())
}
opt, _ := ro.Unpack()
if opt.IsNone() {
return NoneOpt[U]()
}
return f(opt.some)
}
// IsSome returns true if the operation succeeded and contains a final value.
func (ro ResultOpt[T]) IsSome() bool {
if ro.IsErr() {
return false
}
opt, _ := ro.Unpack()
return opt.IsSome()
}
// IsNone returns true if the operation succeeded but no final value is present.
func (ro ResultOpt[T]) IsNone() bool {
if ro.IsErr() {
return false
}
opt, _ := ro.Unpack()
return opt.IsNone()
}

136
fn/result_opt_test.go Normal file
View File

@@ -0,0 +1,136 @@
package fn
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
)
func TestOkOpt(t *testing.T) {
value := 42
resOpt := OkOpt(value)
opt, err := resOpt.Unpack()
require.NoError(t, err)
require.True(t, opt.IsSome(), "expected Option to be Some")
require.Equal(t, value, opt.UnsafeFromSome())
require.True(t, resOpt.IsSome())
require.False(t, resOpt.IsNone())
}
func TestNoneOpt(t *testing.T) {
resOpt := NoneOpt[int]()
opt, err := resOpt.Unpack()
require.NoError(t, err)
require.True(t, opt.IsNone(), "expected Option to be None")
require.True(t, resOpt.IsNone())
require.False(t, resOpt.IsSome())
}
func TestErrOpt(t *testing.T) {
errMsg := "some error"
resOpt := ErrOpt[int](errors.New(errMsg))
_, err := resOpt.Unpack()
require.Error(t, err)
require.EqualError(t, err, errMsg)
require.False(t, resOpt.IsSome())
require.False(t, resOpt.IsNone())
}
func TestMapResultOptOk(t *testing.T) {
value := 10
resOpt := OkOpt(value)
mapped := MapResultOpt(resOpt, func(i int) int {
return i * 3
})
opt, err := mapped.Unpack()
require.NoError(t, err)
require.True(t, opt.IsSome(), "expected mapped Option to be Some")
require.Equal(t, 30, opt.UnsafeFromSome())
}
func TestMapResultOptNone(t *testing.T) {
resOpt := NoneOpt[int]()
mapped := MapResultOpt(resOpt, func(i int) int {
return i * 3
})
opt, err := mapped.Unpack()
require.NoError(t, err)
require.True(t, opt.IsNone(), "expected mapped Option to remain None")
}
func TestMapResultOptErr(t *testing.T) {
errMsg := "error mapping"
resOpt := ErrOpt[int](errors.New(errMsg))
mapped := MapResultOpt(resOpt, func(i int) int {
return i * 3
})
_, err := mapped.Unpack()
require.Error(t, err)
require.EqualError(t, err, errMsg)
}
func incrementOpt(x int) ResultOpt[int] {
return OkOpt(x + 1)
}
func TestAndThenResultOptOk(t *testing.T) {
resOpt := OkOpt(5)
chained := AndThenResultOpt(resOpt, incrementOpt)
opt, err := chained.Unpack()
require.NoError(t, err)
require.True(t, opt.IsSome(), "expected chained Option to be Some")
require.Equal(t, 6, opt.UnsafeFromSome())
}
func TestAndThenResultOptNone(t *testing.T) {
resOpt := NoneOpt[int]()
chained := AndThenResultOpt(resOpt, incrementOpt)
opt, err := chained.Unpack()
require.NoError(t, err)
require.True(t, opt.IsNone(), "expected chained result to remain None")
}
func TestAndThenResultOptErr(t *testing.T) {
errMsg := "error in initial result"
resOpt := ErrOpt[int](errors.New(errMsg))
chained := AndThenResultOpt(resOpt, incrementOpt)
_, err := chained.Unpack()
require.Error(t, err)
require.EqualError(t, err, errMsg)
}
func maybeEvenOpt(x int) ResultOpt[int] {
if x%2 == 0 {
return OkOpt(x / 2)
}
return NoneOpt[int]()
}
func TestAndThenResultOptProducesNone(t *testing.T) {
// Given an odd number, maybeEvenOpt returns None.
resOpt := OkOpt(5)
chained := AndThenResultOpt(resOpt, maybeEvenOpt)
opt, err := chained.Unpack()
require.NoError(t, err)
require.True(t, opt.IsNone(), "expected chained result to be None")
}
func TestMapAndThenIntegration(t *testing.T) {
resOpt := OkOpt(2)
chained := MapResultOpt(
AndThenResultOpt(resOpt, func(x int) ResultOpt[int] {
return OkOpt(x + 3)
}),
func(y int) int {
return y * 2
},
)
opt, err := chained.Unpack()
require.NoError(t, err)
require.True(
t, opt.IsSome(), "expected integrated mapping and "+
"chaining to produce Some",
)
require.Equal(t, 10, opt.UnsafeFromSome())
}