Skip to content

feat(parZip): add parZip functions for combining results of 2 to 5 computations in parallel #122

New issue

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

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

Already on GitHub? # to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

hoc081098
Copy link

@hoc081098 hoc081098 commented Apr 27, 2025

This pull request introduces a new parZip utility to the kotlin-result-coroutines library, which allows running multiple computations in parallel and combining their results. It also includes comprehensive test cases to validate the functionality of parZip for various scenarios.

  • Inspired by arrow-fx-coroutines parZip.

    Regarding the name parZip, I chose it by following the naming convention of the Arrow.kt library. If you have a better suggestion, I will update this PR accordingly 🙏.

  • Added parZip functions for combining results of 2 to 5 computations in parallel. If any computation fails, the others are cancelled, and the error is returned.

  • Added ParZipTest class with test cases to verify the behavior of parZip for 2 to 5 computations.

Example

data class Movie(val id: Int, val title: String)

suspend fun getFavoriteMovies(): Result<List<Movie>, String> {
    delay(100) // simulate network delay
    return Ok(
        listOf(
            Movie(1, "Inception"),
            Movie(2, "The Matrix")
        )
    )
}

suspend fun getPopularMovies(): Result<List<Movie>, String> {
    delay(100) // simulate network delay
    return Ok(
        listOf(
            Movie(3, "Avengers: Endgame"),
            Movie(4, "Titanic")
        )
    )
}

suspend fun getTopRatedMovies(): Result<List<Movie>, String> {
    delay(100) // simulate network delay
    return Ok(
        listOf(
            Movie(5, "The Shawshank Redemption"),
            Movie(6, "The Godfather")
        )
    )
}

// --------------------------------------------------------

data class HomePageMovies(
    val favoriteMovies: List<Movie>,
    val popularMovies: List<Movie>,
    val topRatedMovies: List<Movie>
)

suspend fun getHomePageMoviesParZip(): Result<HomePageMovies, String> =
    withContext(Dispatchers.IO) {
        parZip(
            { getFavoriteMovies() },
            { getPopularMovies() },
            { getTopRatedMovies() },
        ) { favoriteMovies, popularMovies, topRatedMovies ->
            HomePageMovies(
                favoriteMovies = favoriteMovies,
                popularMovies = popularMovies,
                topRatedMovies = topRatedMovies
            )
        }
    }
// Compare getHomePageMoviesParZip with getHomePageMoviesSequentially:
// The execution time of getHomePageMoviesParZip is significantly less than that of getHomePageMoviesSequentially.
// -> it will improve the app performance and user experience.

suspend fun getHomePageMoviesSequentially(): Result<HomePageMovies, String> =
    withContext(Dispatchers.IO) {
        coroutineBinding {
            HomePageMovies(
                favoriteMovies = getFavoriteMovies().bind(),
                popularMovies = getPopularMovies().bind(),
                topRatedMovies = getTopRatedMovies().bind(),
            )
        }
    }

@hoangchungk53qx1
Copy link
Contributor

nice,
This operator is indeed necessary, but can we add a context parameter ctx: CoroutineContext = EmptyCoroutineContext, to adjust its behavior?

@michaelbull
Copy link
Owner

Thanks for this - I like this idea.

I am slightly uncomfortable with the implementation, as it seems to share a large amount of code with coroutineBinding itself, notably the creation of a coroutineScope, a custom ParZipException, etc.

I think I would prefer if the parZip implementation leveraged as much of the existing coroutineBinding function as possible, to reduce maintenance overhead and avoid introducing more custom exceptions.

I can imagine it will end up looking like the body (below) is wrapped in a coroutineBinding:

            val values = producers
                .map { producer -> async { producer().getOrThrow(::ParZipException) } }
                .awaitAll()

Can you give this a go and let me know if the existing coroutineBinding doesn't work for you?

@hoc081098
Copy link
Author

hoc081098 commented Apr 28, 2025

Can you give this a go and let me know if the existing coroutineBinding doesn't work for you?

Thanks. I've just updated the PR following your suggestion 👍.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants