Skip to content

Commit dfc0c4b

Browse files
authored
perf(array): pre-size Ap and Flatten results (#190)
* perf(array): pre-size Ap and Flatten results array.Ap and array.Flatten both routed through MonadChain, which grows the result from nil and (for Ap) allocates an intermediate slice per function. Their output lengths are known up front -- len(fab)*len(fa) for the applicative cartesian product and the sum of inner lengths for flatten -- so build the result in a single pre-sized allocation. Benchmarks (added): BenchmarkAp 6 -> 1 allocs/op (-83%), 155 -> 66 ns/op BenchmarkFlatten 3 -> 1 allocs/op (-67%), 51 -> 24 ns/op Signed-off-by: Nishant Mehta <nishantmehta.n@gmail.com> * perf(array): use slices.Concat and slices.Grow for Flatten and Ap Address review feedback by replacing the hand-rolled pre-sizing with the idiomatic standard library helpers (Go 1.21+, compatible with the module's 1.24 requirement): - Flatten delegates to slices.Concat, which sizes the result in a single allocation. - MonadAp allocates its result once via slices.Grow, the same idiom slices.Concat uses internally. Both return a nil slice for an empty result, which is a valid representation of the empty array. The doc comments now state this, and the TestNilSlice_* assertions are relaxed to check only that the result is empty. Benchmarks are unchanged at a single allocation per call: BenchmarkAp 240 B/op, 1 allocs/op BenchmarkFlatten 128 B/op, 1 allocs/op Signed-off-by: Nishant Mehta <nishantmehta.n@gmail.com> --------- Signed-off-by: Nishant Mehta <nishantmehta.n@gmail.com>
1 parent 8396c01 commit dfc0c4b

4 files changed

Lines changed: 107 additions & 12 deletions

File tree

v2/array/ap_bench_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2023 - 2025 IBM Corp.
2+
// All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package array
17+
18+
import (
19+
"testing"
20+
)
21+
22+
var apSink []int
23+
24+
// BenchmarkAp exercises the array applicative (cartesian product of a slice of
25+
// functions over a slice of values).
26+
func BenchmarkAp(b *testing.B) {
27+
fab := []func(int) int{
28+
func(x int) int { return x + 1 },
29+
func(x int) int { return x * 2 },
30+
func(x int) int { return x - 1 },
31+
}
32+
fa := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
33+
34+
b.ResetTimer()
35+
for i := 0; i < b.N; i++ {
36+
apSink = MonadAp[int, int](fab, fa)
37+
}
38+
}

v2/array/array_nil_test.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -265,22 +265,22 @@ func TestNilSlice_MonadAp(t *testing.T) {
265265
var nilFuncs []func(int) string
266266
var nilValues []int
267267

268+
// nil is a valid representation of the empty array, so these cases only
269+
// assert that an empty result is produced.
270+
268271
// nil functions, nil values
269272
result1 := MonadAp(nilFuncs, nilValues)
270-
assert.NotNil(t, result1, "MonadAp should return non-nil slice")
271-
assert.Equal(t, 0, len(result1), "MonadAp should return empty slice for nil inputs")
273+
assert.Empty(t, result1, "MonadAp should return an empty slice for nil inputs")
272274

273275
// nil functions, non-nil values
274276
nonNilValues := []int{1, 2, 3}
275277
result2 := MonadAp(nilFuncs, nonNilValues)
276-
assert.NotNil(t, result2, "MonadAp should return non-nil slice")
277-
assert.Equal(t, 0, len(result2), "MonadAp should return empty slice when functions are nil")
278+
assert.Empty(t, result2, "MonadAp should return an empty slice when functions are nil")
278279

279280
// non-nil functions, nil values
280281
nonNilFuncs := []func(int) string{func(v int) string { return fmt.Sprintf("%d", v) }}
281282
result3 := MonadAp(nonNilFuncs, nilValues)
282-
assert.NotNil(t, result3, "MonadAp should return non-nil slice")
283-
assert.Equal(t, 0, len(result3), "MonadAp should return empty slice when values are nil")
283+
assert.Empty(t, result3, "MonadAp should return an empty slice when values are nil")
284284
}
285285

286286
// TestNilSlice_Ap verifies that Ap handles nil slices correctly
@@ -290,8 +290,8 @@ func TestNilSlice_Ap(t *testing.T) {
290290

291291
var nilFuncs []func(int) string
292292
result := ap(nilFuncs)
293-
assert.NotNil(t, result, "Ap should return non-nil slice")
294-
assert.Equal(t, 0, len(result), "Ap should return empty slice for nil inputs")
293+
// nil is a valid representation of the empty array.
294+
assert.Empty(t, result, "Ap should return an empty slice for nil inputs")
295295
}
296296

297297
// TestNilSlice_Head verifies that Head handles nil slices correctly
@@ -326,8 +326,9 @@ func TestNilSlice_Tail(t *testing.T) {
326326
func TestNilSlice_Flatten(t *testing.T) {
327327
var nilSlice [][]int
328328
result := Flatten(nilSlice)
329-
assert.NotNil(t, result, "Flatten should return non-nil slice")
330-
assert.Equal(t, 0, len(result), "Flatten should return empty slice for nil input")
329+
// nil is a valid representation of the empty array, so only the emptiness
330+
// of the result is asserted.
331+
assert.Empty(t, result, "Flatten should return an empty slice for nil input")
331332
}
332333

333334
// TestNilSlice_Lookup verifies that Lookup handles nil slices correctly

v2/array/flatten_bench_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2023 - 2025 IBM Corp.
2+
// All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package array
17+
18+
import (
19+
"testing"
20+
)
21+
22+
var flattenSink []int
23+
24+
// BenchmarkFlatten exercises flattening a slice of slices into one slice.
25+
func BenchmarkFlatten(b *testing.B) {
26+
mma := [][]int{
27+
{1, 2, 3},
28+
{4, 5, 6, 7},
29+
{8, 9},
30+
{10, 11, 12, 13, 14},
31+
{15},
32+
}
33+
34+
b.ResetTimer()
35+
for i := 0; i < b.N; i++ {
36+
flattenSink = Flatten(mma)
37+
}
38+
}

v2/array/generic/array.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package generic
1717

1818
import (
19+
"slices"
20+
1921
F "github.com/IBM/fp-go/v2/function"
2022
"github.com/IBM/fp-go/v2/internal/array"
2123
FC "github.com/IBM/fp-go/v2/internal/functor"
@@ -222,8 +224,12 @@ func ChainOptionK[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA)
222224
)
223225
}
224226

227+
// Flatten concatenates the inner slices into a single slice. It uses the
228+
// idiomatic [slices.Concat], which sizes the result in one allocation instead
229+
// of growing it via append. For an empty or nil input a nil slice is returned,
230+
// which is a valid representation of the empty array.
225231
func Flatten[GAA ~[]GA, GA ~[]A, A any](mma GAA) GA {
226-
return MonadChain(mma, F.Identity[GA])
232+
return slices.Concat(mma...)
227233
}
228234

229235
func FilterMap[GA ~[]A, GB ~[]B, A, B any](f func(A) O.Option[B]) func(GA) GB {
@@ -263,8 +269,20 @@ func Chain[AS ~[]A, BS ~[]B, A, B any](f func(A) BS) func(AS) BS {
263269
return F.Bind2nd(MonadChain[AS, BS, A, B], f)
264270
}
265271

272+
// MonadAp computes the applicative product: f(a) for each f in fab and each a
273+
// in fa. Its length is known up front (len(fab)*len(fa)), so the result is
274+
// allocated once via [slices.Grow] - the same idiom used by [slices.Concat] -
275+
// instead of reallocating as it grows. For an empty result a nil slice is
276+
// returned, which is a valid representation of the empty array.
266277
func MonadAp[BS ~[]B, ABS ~[]func(A) B, AS ~[]A, B, A any](fab ABS, fa AS) BS {
267-
return MonadChain(fab, F.Bind1st(MonadMap[AS, BS, A, B], fa))
278+
var result BS
279+
result = slices.Grow(result, len(fab)*len(fa))
280+
for _, f := range fab {
281+
for _, a := range fa {
282+
result = append(result, f(a))
283+
}
284+
}
285+
return result
268286
}
269287

270288
func Ap[BS ~[]B, ABS ~[]func(A) B, AS ~[]A, B, A any](fa AS) func(ABS) BS {

0 commit comments

Comments
 (0)