kotlin-result icon indicating copy to clipboard operation
kotlin-result copied to clipboard

Implement traverse Inspired by Haskell for Safe Transformations

Open hoangchungk53qx1 opened this issue 7 months ago • 4 comments

This pull request introduces a new extension function, traverse, inspired by the traverse function in Haskell's Data.Traversable module. Link : https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Traversable.html#t:Traversable

The goal is to provide a safe and declarative way to transform collections while handling potential errors gracefully.

functional programming, traverse allows us to apply a function that returns a Result (or any monadic type) to each element of a collection and collect the results into a single Result containing the transformed collection. If any transformation fails, the entire operation short-circuits, returning the first error encountered. This is particularly useful for scenarios where we need to ensure all transformations succeed or fail early on the first error.

hoangchungk53qx1 avatar Apr 06 '25 20:04 hoangchungk53qx1

Thanks for this. I like this idea a lot!

I think we could simplify its implementation, as it shares a lot of structure with fold. It looks to me like traverse could call fold with list::add as the second argument.

michaelbull avatar Apr 06 '25 22:04 michaelbull

@michaelbull "Yes, I know that fold can be used to do the same thing, but I want to write an operator that is more expressive in other functional languages."

hoangchungk53qx1 avatar Apr 07 '25 09:04 hoangchungk53qx1

I'm not suggesting that fold does the same thing, just that its implementation is largely shared between fold and traverse, so the new traverse function could call fold in its implementation to avoid duplication.

michaelbull avatar Apr 07 '25 15:04 michaelbull

@michaelbull That's a good idea. I've just edited it, please check it again.

hoangchungk53qx1 avatar Apr 07 '25 16:04 hoangchungk53qx1

Hi @hoangchungk53qx1, apologies for taking so long to get back to you on this.

The more I've studied this implementation, the more I am convinced that the mapResult function on Iterable already solves this problem.

Please can you let me know there's something I am missing with regards to this function in comparison to mapResult?

michaelbull avatar Aug 03 '25 12:08 michaelbull

Hi @hoangchungk53qx1, apologies for taking so long to get back to you on this.

The more I've studied this implementation, the more I am convinced that the mapResult function on Iterable already solves this problem.

Please can you let me know there's something I am missing with regards to this function in comparison to mapResult?

In functional programming, while operators are reusable, their meanings can vary. Additionally, the concept of Traversable should be clearly understood and distinguished from other ideas.

hoangchungk53qx1 avatar Aug 04 '25 01:08 hoangchungk53qx1

The implementation of mapResult seems identical to traverse as you first proposed it:

mapResult:

public inline fun <V, E, U> Iterable<V>.mapResult(
    transform: (V) -> Result<U, E>,
): Result<List<U>, E> {
    val values = map { element ->
        val transformed = transform(element)

        when {
            transformed.isOk -> transformed.value
            else -> return transformed.asErr()
        }
    }

    return Ok(values)
}

Your traverse:

public fun <V, E, U> Iterable<V>.traverse(
    transform: (V) -> Result<U, E>
): Result<List<U>, E> {
    val results = mutableListOf<U>()
    for (item in this) {
        val result = transform(item)
        val element  = when {
            result.isOk -> result.value
            else -> return Err(result.error)
        }
        results.add(element)
    }
    return Ok(results)
}

michaelbull avatar Aug 04 '25 01:08 michaelbull

The implementation of mapResult seems identical to traverse as you first proposed it:

mapResult:

public inline fun <V, E, U> Iterable<V>.mapResult(
    transform: (V) -> Result<U, E>,
): Result<List<U>, E> {
    val values = map { element ->
        val transformed = transform(element)

        when {
            transformed.isOk -> transformed.value
            else -> return transformed.asErr()
        }
    }

    return Ok(values)
}

Your traverse:

public fun <V, E, U> Iterable<V>.traverse(
    transform: (V) -> Result<U, E>
): Result<List<U>, E> {
    val results = mutableListOf<U>()
    for (item in this) {
        val result = transform(item)
        val element  = when {
            result.isOk -> result.value
            else -> return Err(result.error)
        }
        results.add(element)
    }
    return Ok(results)
}

Ohh, It seems that I haven't fully read mapResult, because I only gradually converted from Haskell.

hoangchungk53qx1 avatar Aug 04 '25 02:08 hoangchungk53qx1

mapResult can do this, i will close this MR

hoangchungk53qx1 avatar Aug 04 '25 02:08 hoangchungk53qx1

👍

michaelbull avatar Aug 04 '25 02:08 michaelbull