From 0c25bd3f1953d8e2e07ac73e48d0f53d5cfa2b4a Mon Sep 17 00:00:00 2001 From: "Olaoluwa Osuntokun (aider)" Date: Tue, 18 Feb 2025 15:25:25 -0800 Subject: [PATCH] fn: implement ResultOpt type for operations with optional values This commit introduces the ResultOpt type, which represents an operation that can either succeed with an optional final value or fail with an error. The .gitignore file is also updated to exclude specific files related to the `aider` tool. --- fn/result_opt.go | 64 ++++++++++++++++++++ fn/result_opt_test.go | 136 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 fn/result_opt.go create mode 100644 fn/result_opt_test.go diff --git a/fn/result_opt.go b/fn/result_opt.go new file mode 100644 index 000000000..bb2fa0d14 --- /dev/null +++ b/fn/result_opt.go @@ -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() +} diff --git a/fn/result_opt_test.go b/fn/result_opt_test.go new file mode 100644 index 000000000..2c17fe3b2 --- /dev/null +++ b/fn/result_opt_test.go @@ -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()) +}