Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fn: implement ResultOpt type for operations with optional values #9528

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,4 @@ coverage.txt
# Release build directory (to avoid build.vcs.modified Golang build tag to be
# set to true by having untracked files in the working directory).
/lnd-*/
.aider*
64 changes: 64 additions & 0 deletions fn/result_opt.go
Original file line number Diff line number Diff line change
@@ -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 changes: 136 additions & 0 deletions fn/result_opt_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
Loading