Skip to content

Commit 7eb0728

Browse files
committed
fn: fix Result[T].FlatMap
For a Result[T], FlatMap should apply f when the result is Ok, and propagate the error unchanged when it's Err. The original code returns r on Ok and tries to use r.left when Err, which is wrong. This commit fixes that. Secondly, the group of FlatMap/AndThen and OrElse functions and methods are now properly tested with new unit tests. fixes #10401
1 parent 85a5bf2 commit 7eb0728

File tree

3 files changed

+139
-2
lines changed

3 files changed

+139
-2
lines changed

docs/release-notes/release-notes-0.20.1.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
addresses](https://github.com/lightningnetwork/lnd/pull/10341) were added to
3333
the node announcement and `getinfo` output.
3434

35+
* A bug in the [implementation of
36+
`FlatMap`](https://github.com/lightningnetwork/lnd/pull/10403) in the `fn`
37+
package has been corrected by applying the provided function when the result
38+
is `Ok` and propagate the error unchanged when it is `Err`.
39+
3540
# New Features
3641

3742
## Functional Enhancements

fn/result.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,10 @@ func FlattenResult[A any](r Result[Result[A]]) Result[A] {
149149
// success value if it exists.
150150
func (r Result[T]) FlatMap(f func(T) Result[T]) Result[T] {
151151
if r.IsOk() {
152-
return r
152+
return f(r.left)
153153
}
154154

155-
return f(r.left)
155+
return r
156156
}
157157

158158
// AndThen is an alias for FlatMap. This along with OrElse can be used to

fn/result_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,135 @@ func TestSinkOnOkContinuationCall(t *testing.T) {
9696
require.True(t, called)
9797
require.Nil(t, res)
9898
}
99+
100+
var errFlatMap = errors.New("fail")
101+
var errFlatMapOrig = errors.New("original")
102+
103+
var flatMapTestCases = []struct {
104+
name string
105+
input Result[int]
106+
fnA func(int) Result[int]
107+
fnB func(int) Result[string]
108+
expectedA Result[int]
109+
expectedB Result[string]
110+
}{
111+
{
112+
name: "Ok to Ok",
113+
input: Ok(1),
114+
fnA: func(i int) Result[int] { return Ok(i + 1) },
115+
fnB: func(i int) Result[string] {
116+
return Ok(fmt.Sprintf("%d", i+1))
117+
},
118+
expectedA: Ok(2),
119+
expectedB: Ok("2"),
120+
},
121+
{
122+
name: "Ok to Err",
123+
input: Ok(1),
124+
fnA: func(i int) Result[int] {
125+
return Err[int](errFlatMap)
126+
},
127+
fnB: func(i int) Result[string] {
128+
return Err[string](errFlatMap)
129+
},
130+
expectedA: Err[int](errFlatMap),
131+
expectedB: Err[string](errFlatMap),
132+
},
133+
{
134+
name: "Err to Err (function not called)",
135+
input: Err[int](errFlatMapOrig),
136+
fnA: func(i int) Result[int] { return Ok(i + 1) },
137+
fnB: func(i int) Result[string] {
138+
return Ok("should not happen")
139+
},
140+
expectedA: Err[int](errFlatMapOrig),
141+
expectedB: Err[string](errFlatMapOrig),
142+
},
143+
}
144+
145+
var orElseTestCases = []struct {
146+
name string
147+
input Result[int]
148+
fn func(error) Result[int]
149+
expected Result[int]
150+
}{
151+
{
152+
name: "Ok to Ok (function not called)",
153+
input: Ok(1),
154+
fn: func(err error) Result[int] { return Ok(2) },
155+
expected: Ok(1),
156+
},
157+
{
158+
name: "Err to Ok",
159+
input: Err[int](errFlatMapOrig),
160+
fn: func(err error) Result[int] { return Ok(2) },
161+
expected: Ok(2),
162+
},
163+
{
164+
name: "Err to Err",
165+
input: Err[int](errFlatMapOrig),
166+
fn: func(err error) Result[int] {
167+
return Err[int](errFlatMap)
168+
},
169+
expected: Err[int](errFlatMap),
170+
},
171+
}
172+
173+
func TestFlatMap(t *testing.T) {
174+
for _, tc := range flatMapTestCases {
175+
tc := tc
176+
t.Run(tc.name, func(t *testing.T) {
177+
t.Parallel()
178+
actual := tc.input.FlatMap(tc.fnA)
179+
require.Equal(t, tc.expectedA, actual)
180+
})
181+
}
182+
}
183+
184+
func TestAndThenMethod(t *testing.T) {
185+
// Since AndThen is just an alias for FlatMap, we can reuse the same
186+
// test cases.
187+
for _, tc := range flatMapTestCases {
188+
tc := tc
189+
t.Run(tc.name, func(t *testing.T) {
190+
t.Parallel()
191+
actual := tc.input.AndThen(tc.fnA)
192+
require.Equal(t, tc.expectedA, actual)
193+
})
194+
}
195+
}
196+
197+
func TestOrElseMethod(t *testing.T) {
198+
for _, tc := range orElseTestCases {
199+
tc := tc
200+
t.Run(tc.name, func(t *testing.T) {
201+
t.Parallel()
202+
actual := tc.input.OrElse(tc.fn)
203+
require.Equal(t, tc.expected, actual)
204+
})
205+
}
206+
}
207+
208+
func TestFlatMapResult(t *testing.T) {
209+
for _, tc := range flatMapTestCases {
210+
tc := tc
211+
t.Run(tc.name, func(t *testing.T) {
212+
t.Parallel()
213+
actual := FlatMapResult(tc.input, tc.fnB)
214+
require.Equal(t, tc.expectedB, actual)
215+
})
216+
}
217+
}
218+
219+
func TestAndThenFunc(t *testing.T) {
220+
// Since AndThen is just an alias for FlatMapResult, we can reuse the
221+
// same test cases.
222+
for _, tc := range flatMapTestCases {
223+
tc := tc
224+
t.Run(tc.name, func(t *testing.T) {
225+
t.Parallel()
226+
actual := AndThen(tc.input, tc.fnB)
227+
require.Equal(t, tc.expectedB, actual)
228+
})
229+
}
230+
}

0 commit comments

Comments
 (0)