lo icon indicating copy to clipboard operation
lo copied to clipboard

Error handling variants for iteratees

Open Southclaws opened this issue 2 years ago • 9 comments

Quite frequently, I want to do something like lo.Map(slice, Transformer) but Transformer returns (R, error).

I can write a function literal and handle the error, but it's still not ergonomic as there's no proper way to return the error.

So, I propose (and I can submit a PR for this) a variant of Map (and maybe others that make sense) that looks like this:

func Map[T any, R any](collection []T, iteratee func(T, int) (R, error)) ([]R, error)

Where, if the iteratee returns an error, the entire iteration stops, returns an empty list and an error (or maybe returns what it has so far)

Southclaws avatar Mar 30 '22 11:03 Southclaws

https://github.com/samber/lo/pull/43

NilsJPWerner avatar Mar 31 '22 17:03 NilsJPWerner

We also want this for GroupBy and other lo functions.

Workaround:

        var groupByError error
	entityIDsByType := lo.GroupBy(input.EntityIds, func(entityID entity.EntityID) entity.EntityType {
		entityType, err := entity.EntityTypeFromEntity(entityID)
		if err != nil {
			groupByError = err
		}
		return entityType
	})
	if groupByError != nil {
		return nil, groupByError
	}

What we would like to write:

	entityIDsByType, err := lo.GroupBy(input.EntityIds, func(entityID entity.EntityID) (entity.EntityType, err) {
		return entity.EntityTypeFromEntity(entityID)
	})
	if err != nil {
		return nil, err
	}

finnigantime avatar Jul 20 '22 20:07 finnigantime

here's what we use when we need error handling:


type (
	MapIteratee[T any, R any]    func(T) R
	MapErrIteratee[T any, R any] func(T) (R, error)
)

// Map is stolen from samber/lo but improved with errpr handling and no i arg.
func Map[T any, R any](
	collection []T,
	iteratee MapIteratee[T, R],
) []R {
	result := make([]R, len(collection))

	for i, item := range collection {
		result[i] = iteratee(item)
	}

	return result
}

// MapErr is Map but handles errors.
func MapErr[T any, R any](
	collection []T,
	iteratee MapErrIteratee[T, R],
) ([]R, error) {
	result := make([]R, len(collection))

	for i, item := range collection {
		var err error
		result[i], err = iteratee(item)
		if err != nil {
			return nil, err
		}
	}

	return result, nil
}


func Filter[V any](
	collection []V,
	predicate func(V) bool,
) []V {
	result := []V{}

	for _, item := range collection {
		if predicate(item) {
			result = append(result, item)
		}
	}

	return result
}

func FilterErr[V any](
	collection []V,
	predicate func(V) (bool, error),
) ([]V, error) {
	result := []V{}

	for _, item := range collection {
		ok, err := predicate(item)
		if err != nil {
			return nil, err
		}

		if ok {
			result = append(result, item)
		}
	}

	return result, nil
}


// Reduce is stolen from samber/lo but improved with proper error handling and
// without the useless `i` argument.
//
// Most of the time you just want to Reduce(arr, DataTransformer) instead of
// constructing a whole new function and ignoring the `i int` parameter.
//
// Also, supports returning errors so you can break out of the iteration.
func Reduce[T any, R any](
	collection []T,
	accumulator ReduceIteratee[T, R],
	initial R,
) R {
	for _, item := range collection {
		initial = accumulator(initial, item)
	}

	return initial
}

func ReduceErr[T any, R any, E error](
	collection []T,
	accumulator ReduceErrIteratee[T, R, E],
	initial R,
) (R, error) {
	for _, item := range collection {
		var err error
		initial, err = accumulator(initial, item)
		if err != nil {
			return initial, err
		}
	}

	return initial, nil
}

Southclaws avatar Jul 21 '22 16:07 Southclaws

@Southclaws this looks great - it would be nice if this were a part of lo - or published as a package somewhere.

wrouesnel avatar Aug 06 '22 02:08 wrouesnel

@Southclaws this looks great - it would be nice if this were a part of lo - or published as a package somewhere.

made a quick package, but it would be great to have these error variants as part of lo: https://github.com/Southclaws/dt/tree/main

Southclaws avatar Aug 07 '22 19:08 Southclaws

Another naming option is just the E suffix, like what Cobra uses.

jeremybeard avatar Oct 17 '22 18:10 jeremybeard

I'd like to follow up on this discussion and suggest my own variation (which is very similar to @Southclaws's suggestion):

// MapErr is similar to lo.Map, but handles error in iteratee function
func MapErr[T any, R any](collection []T, iteratee func(T, int) (R, error)) ([]R, error) {
	result := make([]R, len(collection))

	for i, item := range collection {
		res, err := iteratee(item, i)
		if err != nil {
			return nil, err
		}
		result[i] = res
	}

	return result, nil
}

If accepted I'd like to suggest my own contribution (similar to my implementation in https://github.com/otterize/lox) and cover further iterator cases (supporting FilterErr, ReduceErr, etc).

@samber would love your thoughts on this.

amitlicht avatar Nov 14 '22 17:11 amitlicht

Hi, what's the status here? I'm, of course, willing to help if needed but there's been already several PRs open to implement this (https://github.com/samber/lo/pull/292, https://github.com/samber/lo/pull/43).

I think it's quite common to have fallible iteratees in Go and a MapErr function was the only utility function I was unable to replace with lo. My implementation is actually identical to the @amitlicht's one.

Thank you.

simek-m avatar Mar 07 '23 10:03 simek-m