Skip to content
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

Add traverse, zip and orEmpty to Option #53

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/api/lib.api
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,11 @@ public final class app/cash/quiver/extensions/OptionKt {
public static final fun forEach (Larrow/core/Option;Lkotlin/jvm/functions/Function1;)V
public static final fun ifAbsent (Larrow/core/Option;Lkotlin/jvm/functions/Function0;)V
public static final fun or (Larrow/core/Option;Larrow/core/Option;)Larrow/core/Option;
public static final fun orEmpty (Larrow/core/Option;Lkotlin/jvm/functions/Function1;)Ljava/lang/String;
public static final fun toValidatedNel (Larrow/core/Option;Lkotlin/jvm/functions/Function0;)Larrow/core/Validated;
public static final fun traverseOp (Larrow/core/Option;Lkotlin/jvm/functions/Function1;)Larrow/core/Either;
public static final fun unit (Larrow/core/Option;)Larrow/core/Option;
public static final fun zipOp (Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function2;)Larrow/core/Option;
}

public final class app/cash/quiver/extensions/ResultKt {
Expand Down
26 changes: 26 additions & 0 deletions lib/src/main/kotlin/app/cash/quiver/extensions/Option.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

package app.cash.quiver.extensions

import arrow.core.Either
import arrow.core.None
import arrow.core.Option
import arrow.core.Some
import arrow.core.ValidatedNel
import arrow.core.getOrElse
import arrow.core.nonEmptyListOf
import arrow.core.raise.option
import arrow.core.right

/**
* Takes a function to run if your Option is None. Returns Unit if your Option is Some.
Expand Down Expand Up @@ -41,3 +45,25 @@ infix fun <T> Option<T>.or(other: Option<T>): Option<T> = when (this) {
is Some -> this
is None -> other
}

/**
* Given a function that returns an Either, will turn your Option of A into an Option of B in the context of Either,
* where the Left value will always be the None.
*/
inline fun <E, A, B> Option<A>.traverseOp(fa: (A) -> Either<E, B>): Either<E, Option<B>> = fold(
{ None.right() },
{ fa(it).map(::Some) }
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unfortunate we have to use traverseOp and zipOp, that really reduces the value of back-porting these.

Copy link
Collaborator

@aparkersquare aparkersquare Sep 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

traverseEither and zipOption would be more consistent with the variants on NonEmptyList (which have recently been back-ported to quiver).

We could then add traverse and zip when arrow finally removes them?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like using straight traverse and zip is worth the wait. Let's lean on Arrow to fully remove them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I'll open a new PR with just orEmpty then if you're happy with it


/**
* Given an optional value A and B, will return you an optional C
*/
inline fun <A, B, C> Option<A>.zipOp(b: Option<B>, f: (A, B) -> C): Option<C> = option {
f(bind(), b.bind())
}

/**
* Will return an empty string if the Option supplied is None
*/
fun <T> Option<T>.orEmpty(f: (T) -> String): String = this.map(f).getOrElse { "" }

23 changes: 23 additions & 0 deletions lib/src/test/kotlin/app/cash/quiver/extensions/OptionTest.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package app.cash.quiver.extensions

import arrow.core.None
import arrow.core.right
import arrow.core.some
import io.kotest.assertions.arrow.core.shouldBeRight
import io.kotest.assertions.arrow.core.shouldBeSome
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.property.Arb
Expand Down Expand Up @@ -29,4 +32,24 @@ class OptionTest : StringSpec({
None.or(other) shouldBe other
}
}

"traverse returns an either of option" {
"apple".some().traverseOp {
it.length.right()
} shouldBeRight (5.some())
}

"zip returns an optional combination of optional values" {
5.some().zipOp("apple".some()) { a: Int, b: String ->
b.length * a
} shouldBeSome 25
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

Copy link
Collaborator

@aparkersquare aparkersquare Sep 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the indent on line 46 is actually correct (2).

But the following two tests have incorrect indent (4).


"orEmpty returns an empty string if used on a None" {
None.orEmpty { "I am an useless string " } shouldBe ""
}

"orEmpty returns the string supplied if value is Some" {
"apple".some().orEmpty { "I wanna eat $it" } shouldBe "I wanna eat apple"
}
})
Loading