Skip to content

Commit 168d63c

Browse files
authored
Merge pull request #9528 from Roasbeef/res-opt
fn: implement ResultOpt type for operations with optional values
2 parents 31c74f2 + 0c25bd3 commit 168d63c

File tree

2 files changed

+200
-0
lines changed

2 files changed

+200
-0
lines changed

fn/result_opt.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package fn
2+
3+
// ResultOpt represents an operation that may either fail (with an error) or
4+
// succeed with an optional final value.
5+
type ResultOpt[T any] struct {
6+
Result[Option[T]]
7+
}
8+
9+
// OkOpt constructs a successful ResultOpt with a present value.
10+
func OkOpt[T any](val T) ResultOpt[T] {
11+
return ResultOpt[T]{Ok(Some(val))}
12+
}
13+
14+
// NoneOpt constructs a successful ResultOpt with no final value.
15+
func NoneOpt[T any]() ResultOpt[T] {
16+
return ResultOpt[T]{Ok(None[T]())}
17+
}
18+
19+
// ErrOpt constructs a failed ResultOpt with the provided error.
20+
func ErrOpt[T any](err error) ResultOpt[T] {
21+
return ResultOpt[T]{Err[Option[T]](err)}
22+
}
23+
24+
// MapResultOpt applies a function to the final value of a successful operation.
25+
func MapResultOpt[T, U any](ro ResultOpt[T], f func(T) U) ResultOpt[U] {
26+
if ro.IsErr() {
27+
return ErrOpt[U](ro.Err())
28+
}
29+
opt, _ := ro.Unpack()
30+
return ResultOpt[U]{Ok(MapOption(f)(opt))}
31+
}
32+
33+
// AndThenResultOpt applies a function to the final value of a successful
34+
// operation.
35+
func AndThenResultOpt[T, U any](ro ResultOpt[T],
36+
f func(T) ResultOpt[U]) ResultOpt[U] {
37+
38+
if ro.IsErr() {
39+
return ErrOpt[U](ro.Err())
40+
}
41+
opt, _ := ro.Unpack()
42+
if opt.IsNone() {
43+
return NoneOpt[U]()
44+
}
45+
return f(opt.some)
46+
}
47+
48+
// IsSome returns true if the operation succeeded and contains a final value.
49+
func (ro ResultOpt[T]) IsSome() bool {
50+
if ro.IsErr() {
51+
return false
52+
}
53+
opt, _ := ro.Unpack()
54+
return opt.IsSome()
55+
}
56+
57+
// IsNone returns true if the operation succeeded but no final value is present.
58+
func (ro ResultOpt[T]) IsNone() bool {
59+
if ro.IsErr() {
60+
return false
61+
}
62+
opt, _ := ro.Unpack()
63+
return opt.IsNone()
64+
}

fn/result_opt_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package fn
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestOkOpt(t *testing.T) {
11+
value := 42
12+
resOpt := OkOpt(value)
13+
opt, err := resOpt.Unpack()
14+
require.NoError(t, err)
15+
require.True(t, opt.IsSome(), "expected Option to be Some")
16+
require.Equal(t, value, opt.UnsafeFromSome())
17+
require.True(t, resOpt.IsSome())
18+
require.False(t, resOpt.IsNone())
19+
}
20+
21+
func TestNoneOpt(t *testing.T) {
22+
resOpt := NoneOpt[int]()
23+
opt, err := resOpt.Unpack()
24+
require.NoError(t, err)
25+
require.True(t, opt.IsNone(), "expected Option to be None")
26+
require.True(t, resOpt.IsNone())
27+
require.False(t, resOpt.IsSome())
28+
}
29+
30+
func TestErrOpt(t *testing.T) {
31+
errMsg := "some error"
32+
resOpt := ErrOpt[int](errors.New(errMsg))
33+
_, err := resOpt.Unpack()
34+
require.Error(t, err)
35+
require.EqualError(t, err, errMsg)
36+
require.False(t, resOpt.IsSome())
37+
require.False(t, resOpt.IsNone())
38+
}
39+
40+
func TestMapResultOptOk(t *testing.T) {
41+
value := 10
42+
resOpt := OkOpt(value)
43+
mapped := MapResultOpt(resOpt, func(i int) int {
44+
return i * 3
45+
})
46+
opt, err := mapped.Unpack()
47+
require.NoError(t, err)
48+
require.True(t, opt.IsSome(), "expected mapped Option to be Some")
49+
require.Equal(t, 30, opt.UnsafeFromSome())
50+
}
51+
52+
func TestMapResultOptNone(t *testing.T) {
53+
resOpt := NoneOpt[int]()
54+
mapped := MapResultOpt(resOpt, func(i int) int {
55+
return i * 3
56+
})
57+
opt, err := mapped.Unpack()
58+
require.NoError(t, err)
59+
require.True(t, opt.IsNone(), "expected mapped Option to remain None")
60+
}
61+
62+
func TestMapResultOptErr(t *testing.T) {
63+
errMsg := "error mapping"
64+
resOpt := ErrOpt[int](errors.New(errMsg))
65+
mapped := MapResultOpt(resOpt, func(i int) int {
66+
return i * 3
67+
})
68+
_, err := mapped.Unpack()
69+
require.Error(t, err)
70+
require.EqualError(t, err, errMsg)
71+
}
72+
73+
func incrementOpt(x int) ResultOpt[int] {
74+
return OkOpt(x + 1)
75+
}
76+
77+
func TestAndThenResultOptOk(t *testing.T) {
78+
resOpt := OkOpt(5)
79+
chained := AndThenResultOpt(resOpt, incrementOpt)
80+
opt, err := chained.Unpack()
81+
require.NoError(t, err)
82+
require.True(t, opt.IsSome(), "expected chained Option to be Some")
83+
require.Equal(t, 6, opt.UnsafeFromSome())
84+
}
85+
86+
func TestAndThenResultOptNone(t *testing.T) {
87+
resOpt := NoneOpt[int]()
88+
chained := AndThenResultOpt(resOpt, incrementOpt)
89+
opt, err := chained.Unpack()
90+
require.NoError(t, err)
91+
require.True(t, opt.IsNone(), "expected chained result to remain None")
92+
}
93+
94+
func TestAndThenResultOptErr(t *testing.T) {
95+
errMsg := "error in initial result"
96+
resOpt := ErrOpt[int](errors.New(errMsg))
97+
chained := AndThenResultOpt(resOpt, incrementOpt)
98+
_, err := chained.Unpack()
99+
require.Error(t, err)
100+
require.EqualError(t, err, errMsg)
101+
}
102+
103+
func maybeEvenOpt(x int) ResultOpt[int] {
104+
if x%2 == 0 {
105+
return OkOpt(x / 2)
106+
}
107+
return NoneOpt[int]()
108+
}
109+
110+
func TestAndThenResultOptProducesNone(t *testing.T) {
111+
// Given an odd number, maybeEvenOpt returns None.
112+
resOpt := OkOpt(5)
113+
chained := AndThenResultOpt(resOpt, maybeEvenOpt)
114+
opt, err := chained.Unpack()
115+
require.NoError(t, err)
116+
require.True(t, opt.IsNone(), "expected chained result to be None")
117+
}
118+
119+
func TestMapAndThenIntegration(t *testing.T) {
120+
resOpt := OkOpt(2)
121+
chained := MapResultOpt(
122+
AndThenResultOpt(resOpt, func(x int) ResultOpt[int] {
123+
return OkOpt(x + 3)
124+
}),
125+
func(y int) int {
126+
return y * 2
127+
},
128+
)
129+
opt, err := chained.Unpack()
130+
require.NoError(t, err)
131+
require.True(
132+
t, opt.IsSome(), "expected integrated mapping and "+
133+
"chaining to produce Some",
134+
)
135+
require.Equal(t, 10, opt.UnsafeFromSome())
136+
}

0 commit comments

Comments
 (0)