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? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

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

Already on GitHub? Sign in 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 👍.

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

Successfully merging this pull request may close these issues.

3 participants