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

[PROPOSAL] Option API deprecation, and preparation for 2.x.x #2913

Merged
merged 33 commits into from
Feb 9, 2023

Conversation

franciscodr
Copy link
Collaborator

@franciscodr franciscodr commented Feb 2, 2023

This PR proposes a smaller API surface for Option without compromising its functionality or feature set.
The goal of these changes is to offer APIs that are more powerful and more idiomatic to use in Kotlin. Following the signatures and naming of some other well-known APIs from Kotlin Std, or KotlinX Coroutines

Option (API Size 70 -> 21)

Foldable / Traverse (API Size 18 -> 1)

Keep

  • fold

Remove

  • crosswalk: replace with map { f(it) }
  • crosswalkMap: replace with fold( { emptyMap() }, { a -> f(a).mapValues { Some(it.value) } })
  • crosswalkNull: replace with map { value -> f(value)?.let { Some(it) } }.orNull()
  • foldLeft: replace with fold
  • foldMap: replace with fold
  • reduceOrNull: replace with map { value -> operation(initial(value), value) }.orNull()
  • reduceRightEvalOrNull: replace with fold({ Eval.now(null) }, { value -> operation(value, Eval.now(initial(value))) })
  • separateEither: replace with fold({ None to None }) { either -> either.fold<Pair<Option<A>, Option<B>>>({ Some(it) to None }, { None to Some(it) }) }
  • separateValidated: replace with fold({ None to None }) { validated -> validated.fold<Pair<Option<A>, Option<B>>>({ Some(it) to None }, { None to Some(it) }) }
  • unite: replace with map { iterable -> iterable.fold(MA) }
  • uniteEither: replace with flatMap { either -> either.fold({ None }, { Some(it) }) }
  • uniteValidated: replace with flatMap { validated -> validated.fold({ None }, ::Some) }
  • All the traverse methods can be replaced by a simple fold, or even simpler by orNull, getOrElse in many cases.
    • sequence (Iterable)
    • sequenceEither
    • sequence (Either)
    • sequenceValidated
    • sequence (Validated)
    • traverse (Iterable)
    • traverse Either
    • traverse (Either)
    • traverseValidated
    • traverse (Validated)

Functor / Applicative / Monad (API Size 21 -> 10)

New

  • onSome: evaluate a fire-and-forget action if the value is defined
  • onNone: evaluate a fire-and-forget action if the value is empty

Keep

  • map: transform the value if defined
  • filter: keep the value if it holds the predicate. Otherwise, return None
  • filterNot: keep the value if it doesn't hold the predicate. Otherwise, return None
  • flatMap: transform the value if defined with Option + flatten
  • flatten: flatten nested option values
  • unzip: convert an option value containing a pair into a pair with two option values
  • widen: conveniently change the generic parameters to a superclass type

Remove

  • all: replace with fold({ false }, predicate) or map(predicate).getOrElse{ false }
  • ensure: use instead DSL + ensure, flatMap + if-else
  • exists: replace with fold({ true }, predicate) or map(predicate).getOrElse{ true }
  • findOrNull: replace with getOrNull()?.takeIf(predicate)
  • pairLeft: replace with map { left to it }
  • pairRight: replace with map { it to right }
  • replicate: replace with map { List(n) { it } } // There's an alternative with Monoid
  • ​​tap -> replace with onDefined to be more in line with Kotlin Std
  • tapNone -> replace with onEmpty to be more in line with Kotlin Std
  • unit: replace with Some(Unit)
  • void: replace with map {}

Replace

  • zip: the original zip implementation will be replaced with inline option + bind

Applicative/MonadError (API Size 5 -> 0)

Remove

  • handleError: replace with getOrElse { f(Unit) }
  • handleErrorWith: replace with orElse { f(Unit) }
  • redeem: replace with map(fb).orElse { Some(fe(Unit)) }
  • redeemWith: replace with flatMap(fb).orElse(fe)
  • rethrow: replace with flatMap { it.fold({ None }, { a -> Some(a) }) }

Align (API Size 7 -> 0)

Remove

  • align: prefer using a simple fold, or when expression
  • padZip: prefer using a simple fold, or when expression
  • salign: prefer using a simple fold, or when expression
  • unalign: prefer using a when expression

Utilities (API Size 19 -> 10)

New

  • getOrNull: extract the value or return null

Keep

  • compareTo: keep as Comparable operator
  • getOrElse: extract or provide a fallback value if empty
  • isEmpty: check if the value is not defined
  • isNotEmpty: check if the value is defined
  • orElse: return the option value if defined. Otherwise, return the provided fallback value
  • toEither: convert the option value as Either.Right if defined. Otherwise, return the provided fallback value as Either.Left
  • toList: convert the option value as a singleton list if defined. Otherwise, return an empty list
  • toMap: convert the option value containing a pair as a singleton map if defined. Otherwise, return an empty map
  • T?.toOption(): convert a nullable value into None if it's null. Otherwise, return Some containing the value

Remove

  • and: replace with flatMap { value }
  • Boolean.maybe: replace with if (this) { Some(f()) } else { None }
  • combine: will be covered by accumulating zip behavior in 2.x.x
  • combineAll: replace with fold(Monoid.option(MA))
  • filterIsInstance: replace with flatMap { when (it) { is B -> Some(it) else -> None } }
  • isDefined: duplicate - use isNotEmpty
  • mapNotNull: replace with flatMap { fromNullable(f(it)) }
  • nonEmpty: duplicate - use isNotEmpty
  • or: replace with orElse
  • orNull: replace with getOrNull

@github-actions
Copy link
Contributor

github-actions bot commented Feb 2, 2023

Kover Report

File Coverage [51.33%]
arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Either.kt 57.04%
arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Option.kt 44.30%
Total Project Coverage 43.72%

Copy link
Member

@nomisRev nomisRev left a comment

Choose a reason for hiding this comment

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

Looks good to me so far. I think it was missing some imports in the ReplaceWith 🤔

@franciscodr
Copy link
Collaborator Author

@nomisRev Do we want to deprecate the zip method and all its variants with the inline option DSL? So far, I kept the methods and only changed the implementation to use the DSL, but I saw you're deprecating the zip method for the Either type.

@nomisRev
Copy link
Member

nomisRev commented Feb 6, 2023

Do we want to deprecate the zip method and all its variants with the inline option DSL?

Yes, we agreed that fa.zip(fb) { a, b -> transform(a, b) } doesn't really offer any benefits over option { transform(fa.bind(), fb.bind()) }. The latter is only 4 characters longer than the former, and doesn't suffer from the arity-n problem.

It's an API that originally came from Apply, but offers duplication in the API as bind (or flatMap).

@nomisRev
Copy link
Member

nomisRev commented Feb 6, 2023

This decision can still be challenged of course. @myuwono would be great to get your review here when it's ready for review. He's one of the biggest users of Option in combination with Spring WebFlux, and can perhaps short-circuit some things if needed.

@franciscodr franciscodr marked this pull request as ready for review February 6, 2023 19:06
@franciscodr franciscodr requested a review from a team February 6, 2023 19:07
Copy link
Member

@serras serras left a comment

Choose a reason for hiding this comment

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

LGTM

@@ -1029,6 +1255,14 @@ public inline fun <A> Option<A>.ensure(error: () -> Unit, predicate: (A) -> Bool
/**
* Returns an Option containing all elements that are instances of specified type parameter [B].
*/
@Deprecated(
NicheAPI + "Prefer using option DSL or flatMap",
Copy link
Collaborator

Choose a reason for hiding this comment

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

This aligns with stdlib for list.filterIsInstance. let's not deprecate this.

Copy link
Member

Choose a reason for hiding this comment

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

Is this an API that is commonly used in your projects @myuwono? It's an API I personally find kind-of strange on Option, even on List I think it's not commonly used 🤔

I am having a hard time thinking of a good use-case. I hope that Option<Any> is an extremely rare case 😁
Otherwise inheritance rules should apply? 🤔

Copy link
Collaborator

@myuwono myuwono Feb 7, 2023

Choose a reason for hiding this comment

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

Similarly to Lists, this is useful for a call that returns a sealed type @nomisRev. For instance if a database call returns Option<Document> where document is a sealed class with many subtypes, and we are interested in only one of them e.g. Option<Document.ExternalMetadata>.

Copy link
Member

Choose a reason for hiding this comment

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

Right, you need it for narrow'ing. So an Option alternative to as?.
Since this only makes sense for Option, lets keep it 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the feedback, @myuwono! I have reverted the deprecation in this commit

Copy link
Member

@nomisRev nomisRev left a comment

Choose a reason for hiding this comment

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

Couple of thoughts, and nits. We should do a pass for the contracts in a subsequent PR, I think we might have missed some.

I'm going to approve, we can keep discussion open with @myuwono and make adjustments later on ☺️

For anyone reading this, anything deprecated now can be challenged before 2.x.x release. Or can be re-introduced if needed. Please provide and share use-cases, and production usage ☺️

@@ -1029,6 +1255,14 @@ public inline fun <A> Option<A>.ensure(error: () -> Unit, predicate: (A) -> Bool
/**
* Returns an Option containing all elements that are instances of specified type parameter [B].
*/
@Deprecated(
NicheAPI + "Prefer using option DSL or flatMap",
Copy link
Member

Choose a reason for hiding this comment

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

Is this an API that is commonly used in your projects @myuwono? It's an API I personally find kind-of strange on Option, even on List I think it's not commonly used 🤔

I am having a hard time thinking of a good use-case. I hope that Option<Any> is an extremely rare case 😁
Otherwise inheritance rules should apply? 🤔

@Deprecated(
NicheAPI + "Prefer Kotlin nullable syntax instead",
ReplaceWith("getOrNull()?.takeIf(predicate)")
)
public inline fun findOrNull(predicate: (A) -> Boolean): A? {
Copy link
Member

Choose a reason for hiding this comment

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

Pretty crazy.. these APIs predate the introduction of takeIf into Kotlin Std. Arrow is getting old, happy it's getting a new lack of paint 😍

@nomisRev nomisRev added the 1.2.0 Tickets belonging to 1.1.2 label Feb 7, 2023
Copy link
Member

@nomisRev nomisRev left a comment

Choose a reason for hiding this comment

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

Temporarily blocking this PR pending the ongoing conversation with @myuwono.

@@ -867,7 +867,6 @@ public final class arrow/core/NonFatalOrThrowKt {

public final class arrow/core/None : arrow/core/Option {
public static final field INSTANCE Larrow/core/None;
public fun isEmpty ()Z
Copy link
Member

Choose a reason for hiding this comment

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

@franciscodr sorry for dragging this PR on so long 🙈
We need to revert the change we made to abstract fun isEmpty in order to not break binary compatibility. The improvements we made in isSome.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No need to apologize, @nomisRev. Thanks for your thorough review of this PR 😄

Copy link
Member

@nomisRev nomisRev left a comment

Choose a reason for hiding this comment

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

Thank you @franciscodr for this awesome PR 🙏 👏 🥳 And thanks @myuwono for the great feedback and brainstorming the new API 🎉

@nomisRev nomisRev added this pull request to the merge queue Feb 9, 2023
Merged via the queue into main with commit 50c80d5 Feb 9, 2023
@franciscodr franciscodr deleted the fd-option-API branch February 9, 2023 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
1.2.0 Tickets belonging to 1.1.2
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants