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

Missing NonEmpty Collection Helper methods #4070

Open
zarthross opened this issue Dec 3, 2021 · 4 comments
Open

Missing NonEmpty Collection Helper methods #4070

zarthross opened this issue Dec 3, 2021 · 4 comments

Comments

@zarthross
Copy link
Contributor

zarthross commented Dec 3, 2021

In the Cats library there are several aliases for various NonEmpty types and methods, for example EitherNec and 1.leftNec.
However, there are a lot of inconsistencies with when a *Nec or *Nel or other non-empty collection version of a method exists.

We have the following non-empty collection types.

  • NonEmptyChain
  • NonEmptyLazyList
  • NonEmptyList
  • NonEmptyMap (Doesn't seem to be used for any 'left' sides so not included below)
  • NonEmptySeq
  • NonEmptySet
  • NonEmptyVector

See also PR: 3998 For a WIP to add a few more methods not listed here.

Below is an accounting of the missing methods. I would be happy to do a PR to add the below methods, but before I did,
I wanted to make sure there isn't some underlying principle, or typeclass, or something we could use to optimize this to a degree
so less of these helper methods will be added, while still granting their utility.

Some Ideas besides just adding the methods:

  • Adding toNe* methods to the Bifunctor syntax/ops.
  • Add a OneOf typeclass (sort of thing) of the form trait OneOf[F[_], A]{ def one(a: A): F[A]} and make a add a generic leftLift method on Bifunctor.
    • OneOf differs from Pure in AlleyCats, because it allows you to say:

      • implicit def oneOfNes[A: Order]: OneOf[NonEmptySet, A] = a => NonEmptySet[F, A].one(a)
      • implicit def oneOfApplicative[F[_]: Applicative, A]: OneOf[F, A] = a => Applicative[F].pure(a)
    • either.leftLift[NonEmptyLazyList]: EitherNell[A, B]

    • Added bonus, this works with Non-NonEmpty lists as well!

      • either.leftLift[List]: Either[List[A], B]

Either

Type Aliases

// Exists:
type EitherNel[+E, +A] = Either[NonEmptyList[E], A]
type EitherNec[+E, +A] = Either[NonEmptyChain[E], A]
type EitherNes[E, +A] = Either[NonEmptySet[E], A]

// Missing:
type EitherNeSeq[+E, +A] = Either[NonEmptySeq[E], A] // NeSeq isn't a great name, happy to hear ideas for other aliases.
type EitherNev[+E, +A] = Either[NonEmptyVector[E], A]
type EitherNell[+E, +A] = Either[NonEmptyLazyList[E], A] // Scala 2.13+ Only

Object Ops

final class EitherObjectOps(private val either: Either.type) extends AnyVal {
    // Exists:
    def leftNec[A, B](a: A): EitherNec[A, B] 
    def leftNel[A, B](a: A): EitherNel[A, B] 
    def leftNes[A, B](a: A)(implicit O: Order[A]): EitherNes[A, B]

    def rightNec[A, B](b: B): EitherNec[A, B]
    def rightNel[A, B](b: B): EitherNel[A, B]
    def rightNes[A, B](b: B)(implicit O: Order[B]): EitherNes[A, B] 

    // Missing:
    def leftNell[A, B](a: A): EitherNell[A, B]  // Scala 2.13+ Only 
    def leftNeSeq[A, B](a: A): EitherNeSeq[A, B]
    def leftNeV[A, B](a: A): EitherNeV[A, B]

    def rightNell[A, B](b: B): EitherNell[A, B]  // Scala 2.13+ Only 
    def rightNeSeq[A, B](b: B): EitherNeSeq[A, B]
    def rightNeV[A, B](b: B): EitherNeV[A, B]
}

Ops

final class EitherOps[A, B](private val eab: Either[A, B]) extends AnyVal {
    // Exists:
    def toValidatedNel[AA >: A]: ValidatedNel[AA, B]

    def toEitherNel[AA >: A]: EitherNel[AA, B]
    def toEitherNec[AA >: A]: EitherNec[AA, B]
    def toEitherNes[AA >: A](implicit O: Order[AA]): EitherNes[AA, B]

    // Missing:
    def toValidatedNec[AA >: A]: ValidatedNec[AA, B]
    def toValidatedNell[AA >: A]: ValidatedNell[AA, B]  // Scala 2.13+ Only 
    def toValidatedNes[AA >: A](implicit O: Order[AA]): ValidatedNes[AA, B]
    def toValidatedNeSeq[AA >: A]: ValidatedNeSeq[AA, B]
    def toValidatedNev[AA >: A]: ValidatedNev[AA, B]

    def toEitherNell[AA >: A]: EitherNell[AA, B]  // Scala 2.13+ Only 
    def toEitherNeSeq[AA >: A]: EitherNeSeq[AA, B]
    def toEitherNev[AA >: A]: EitherNev[AA, B]
}

Id Ops

class EitherIdOps[A](private val value: A) extends AnyVal {
    // Exist:
    def toValidatedNec: ValidatedNec[A, B]
    
    def leftNec[B]: EitherNec[A, B]
    def leftNel[B]: EitherNel[A, B]

    def rightNec[B]: EitherNec[B, A]
    def rightNel[B]: EitherNel[B, A]

    // Missing:
    def toValidatedNec: ValidatedNec[A, B]
    def toValidatedNell: ValidatedNell[A, B]  // Scala 2.13+ Only 
    def toValidatedNes(implicit O: Order[A]): ValidatedNes[A, B]
    def toValidatedNeSeq: ValidatedNeSeq[A, B]
    def toValidatedNev: ValidatedNev[A, B]

    def leftNell[B]: EitherNell[A, B]  // Scala 2.13+ Only 
    def leftNes[B](implicit O: Order[A]): EitherNes[A, B]
    def leftNeSeq[B]: EitherNeSeq[A, B]
    def leftNev[B]: EitherNev[A, B]
    
    def rightNell[B]: EitherNell[B, A]  // Scala 2.13+ Only 
    def rightNes[B](implicit O: Order[A]): EitherNes[B, A]
    def rightNeSeq[B]: EitherNeSeq[B, A]
    def rightNev[B]: EitherNev[B, A]
}

EitherT

Type Aliases

// Exists: NONE!

// Missing:
type EitherTNec[F, A, B] = EitherT[F, NonEmptyChain[A], B]
type EitherTNel[F, A, B] = EitherT[F, NonEmptyList[A], B]
type EitherTNell[F, A, B] = EitherT[F, NonEmptyLazyList[A], B] // Scala 2.13+ Only
type EitherTNes[F, A, B] = EitherT[F, NonEmptySet[A], B]
type EitherTNeSeq[F, A, B] = EitherT[F, NonEmptySeq[A], B]
type EitherTNev[F, A, B] = EitherT[F, NonEmptyVector[A], B]

Ops

final case class EitherT[F[_], A, B](value: F[Either[A, B]]) {
    // Exists:
    def toValidatedNel(implicit F: Functor[F]): F[ValidatedNel[A, B]]
    def toValidatedNec(implicit F: Functor[F]): F[ValidatedNec[A, B]]

    def toNestedValidatedNel(implicit F: Functor[F]): Nested[F, ValidatedNel[A, *], B]
    def toNestedValidatedNec(implicit F: Functor[F]): Nested[F, ValidatedNec[A, *], B]

    // Missing:
    def toValidatedNell(implicit F: Functor[F]): F[ValidatedNell[A, B]]  // Scala 2.13+ Only 
    def toValidatedNes(implicit F: Functor[F], O: Order[A]): F[ValidatedNes[A, B]]
    def toValidatedNeSeq(implicit F: Functor[F]): F[ValidatedNeSeq[A, B]]
    def toValidatedNev(implicit F: Functor[F]): F[ValidatedNev[A, B]]

    def toNestedValidatedNell(implicit F: Functor[F]): Nested[F, ValidatedNell[A, *], B]  // Scala 2.13+ Only 
    def toNestedValidatedNes(implicit F: Functor[F], O: Order[A]): Nested[F, ValidatedNes[A, *], B]
    def toNestedValidatedNeSeq(implicit F: Functor[F]): Nested[F, ValidatedNeSeq[A, *], B]
    def toNestedValidatedNev(implicit F: Functor[F]): Nested[F, ValidatedNev[A, *], B]
}

Ior

Type Aliases

// Exists:
type IorNel[+B, +A] = Ior[NonEmptyList[B], A]
type IorNec[+B, +A] = Ior[NonEmptyChain[B], A]
type IorNes[B, +A] = Ior[NonEmptySet[B], A]

// Missing:
type IorNeSeq[+B, +A] = Ior[NonEmptySeq[B], A]
type IorNev[+B, +A] = Ior[NonEmptyVector[B], A]
type IorNell[+B, +A] = Ior[NonEmptyLazyList[B], A] // Scala 2.13+ Only 

Object Ops

object Ior {
    // Exists:
    def bothNec[A, B](a: A, b: B): IorNec[A, B] 
    def bothNel[A, B](a: A, b: B): IorNel[A, B] 
    
    def leftNec[A, B](a: A): IorNec[A, B]
    def leftNel[A, B](a: A): IorNel[A, B]

    // Missing:
    def bothNell[A, B](a: A, b: B): IorNell[A, B] 
    def bothNes[A, B](a: A, b: B)(implicit O: Order[A]): IorNes[A, B] 
    def bothNeSeq[A, B](a: A, b: B): IorNeSeq[A, B] 
    def bothNev[A, B](a: A, b: B): IorNev[A, B] 

    def leftNell[A, B](a: A): IorNell[A, B]
    def leftNes[A, B](a: A)(implicit O: Order[A]): IorNes[A, B]
    def leftNeSeq[A, B](a: A): IorNeSeq[A, B]
    def leftNev[A, B](a: A): IorNev[A, B]

    def rightNec[A, B](b: B): IorNec[A, B]
    def rightNel[A, B](b: B): IorNel[A, B]
    def rightNell[A, B](a: A): IorNell[A, B]
    def rightNes[A, B](a: A)(implicit O: Order[A]): IorNes[A, B]
    def rightNeSeq[A, B](a: A): IorNeSeq[A, B]
    def rightNev[A, B](a: A): IorNev[A, B]
}

Ops

sealed abstract class Ior[+A, +B] extends Product with Serializable {
    // Exists:
    final def toIorNes[AA >: A](implicit O: Order[AA]): IorNes[AA, B]
    final def toIorNec[AA >: A]: IorNec[AA, B]
    final def toIorNel[AA >: A]: IorNel[AA, B]

    // Missing:
    final def toIorNell[AA >: A]: IorNell[AA, B]
    final def toIorNeSeq[AA >: A]: IorNeSeq[AA, B]
    final def toIorNev[AA >: A]: IorNev[AA, B]
}

Id Ops

final class IorIdOps[A](private val a: A) extends AnyVal {
    // Exists: NONE!

    // Missing:
    def bothNec[B](b: B): IorNec[A, B] 
    def bothNel[B](b: B): IorNel[A, B] 
    def bothNell[B](b: B): IorNell[A, B] 
    def bothNes[B](b: B)(implicit O: Order[A]): IorNes[A, B] 
    def bothNeSeq[B](b: B): IorNeSeq[A, B] 
    def bothNev[B](b: B): IorNev[A, B] 

    def leftNec[B]: IorNec[A, B]
    def leftNel[B]: IorNel[A, B]
    def leftNell[B]: IorNell[A, B]
    def leftNes[B](implicit O: Order[A]): IorNes[A, B]
    def leftNeSeq[B]: IorNeSeq[A, B]
    def leftNev[B]: IorNev[A, B]

    def rightNec[B]: IorNec[B, A]
    def rightNel[B]: IorNel[B, A]
    def rightNell[B]: IorNell[B, A]
    def rightNes[B](implicit O: Order[B]): IorNes[B, A]
    def rightNeSeA[B]: IorNeSeq[B, A]
    def rightNev[B]: IorNev[B, A]
}

IorT

Missing all instances Ne* methods and Aliases

Validated

Type Aliases

// Exists:
type ValidatedNel[+E, +A] = Validated[NonEmptyList[E], A]
type ValidatedNec[+E, +A] = Validated[NonEmptyChain[E], A]

// Missing:
type ValidatedNes[E, +A] = Validated[NonEmptySet[E], A]
type ValidatedNeSeq[+E, +A] = Validated[NonEmptySeq[E], A]
type ValidatedNev[+E, +A] = Validated[NonEmptyVector[E], A]
type ValidatedNell[+E, +A] = Validated[NonEmptyLazyList[E], A] // Scala 2.13+ Only

Object Ops

object Validated {
    // Exists:
    def condNec[A, B](test: Boolean, b: => B, a: => A): ValidatedNec[A, B]
    def condNel[E, A](test: Boolean, a: => A, e: => E): ValidatedNel[E, A]

    def invalidNec[A, B](a: A): ValidatedNec[A, B]
    def invalidNel[E, A](e: E): ValidatedNel[E, A]

    def validNec[A, B](b: B): ValidatedNec[A, B]
    def validNel[E, A](a: A): ValidatedNel[E, A]

    // Missing:
    def condNes[A, B](test: Boolean, b: => B, a: => A)(implicit O: Order[A]): ValidatedNes[A, B]
    def condNeSeq[A, B](test: Boolean, b: => B, a: => A): ValidatedNeSeq[A, B]
    def condNev[A, B](test: Boolean, b: => B, a: => A): ValidatedNev[A, B]
    def condNell[A, B](test: Boolean, b: => B, a: => A): ValidatedNell[A, B]

    def invalidNes[A, B](a: A)(implicit O: Order[A]): ValidatedNes[A, B]
    def invalidNeSeq[A, B](a: A): ValidatedNeSeq[A, B]
    def invalidNev[A, B](a: A): ValidatedNev[A, B]
    def invalidNell[A, B](a: A): ValidatedNell[A, B]

    def validNes[A, B](b: B)(implicit O: Order[A]): ValidatedNes[A, B]
    def validNeSeq[A, B](b: B): ValidatedNeSeq[A, B]
    def validNev[A, B](b: B): ValidatedNev[A, B]
    def validNell[A, B](b: B): ValidatedNell[A, B]
}

Ops

sealed abstract class Validated[+E, +A] extends Product with Serializable {
    // Exists:
    def toValidatedNel[EE >: E, AA >: A]: ValidatedNel[EE, AA]
    def toValidatedNec[EE >: E, AA >: A]: ValidatedNec[EE, AA]

    // Missing:
    def toValidatedNes[EE >: E, AA >: A](implicit O: Order[A]): ValidatedNes[EE, AA]
    def toValidatedNeSeq[EE >: E, AA >: A]: ValidatedNeSeq[EE, AA]
    def toValidatedNev[EE >: E, AA >: A]: ValidatedNev[EE, AA]
    def toValidatedNell[EE >: E, AA >: A]: ValidatedNell[EE, AA]
}

Id Ops

final class ValidatedIdSyntax[A](private val a: A) extends AnyVal {
    // Exists:
    def invalidNec[B]: ValidatedNec[A, B]
    def invalidNel[B]: ValidatedNel[A, B]

    def validNec[B]: ValidatedNec[B, A]
    def validNel[B]: ValidatedNel[B, A]

    // Missing:
    def invalidNes[B](implicit O: Order[A]): ValidatedNes[A, B]
    def invalidNeSeq[B]: ValidatedNeSeq[A, B]
    def invalidNev[B]: ValidatedNev[A, B]
    def invalidNell[B]: ValidatedNell[A, B]
    
    def validNes[B](implicit O: Order[B]): ValidatedNes[B, A]
    def validNeSeq[B]: ValidatedNeSeq[B, A]
    def validNev[B]: ValidatedNev[B, A]
    def validNell[B]: ValidatedNell[B, A]
}

List

Ops

final class ListOps[A](private val la: List[A]) extends AnyVal {
    // Exists:
    def groupByNec[B](f: A => B)(implicit B: Order[B]): SortedMap[B, NonEmptyChain[A]]
    def groupByNel[B](f: A => B)(implicit B: Order[B]): SortedMap[B, NonEmptyList[A]]
    
    def groupByNelA[F[_], B](f: A => F[B])(implicit F: Applicative[F], B: Order[B]): F[SortedMap[B, NonEmptyList[A]]]

    // Included for completeness, but probably doesn't need other *Ne* methods.
    def scanLeftNel[B](b: B)(f: (B, A) => B): NonEmptyList[B]
    def scanRightNel[B](b: B)(f: (A, B) => B): NonEmptyList[B]
    def toNel: Option[NonEmptyList[A]] = NonEmptyList.fromList(la)

    // Missing:
    def groupByNes[B](f: A => B)(implicit B: Order[B], A: Order[A]): SortedMap[B, NonEmptySet[A]]
    def groupByNeSeq[B](f: A => B)(implicit B: Order[B]): SortedMap[B, NonEmptySeq[A]]
    def groupByNev[B](f: A => B)(implicit B: Order[B]): SortedMap[B, NonEmptyVector[A]]
    def groupByNell[B](f: A => B)(implicit B: Order[B]): SortedMap[B, NonEmptyLazyList[A]]

    def groupByNesA[F[_], B](f: A => F[B])(implicit F: Applicative[F], B: Order[B]): F[SortedMap[B, NonEmptySet[A]]]
    def groupByNeSeqA[F[_], B](f: A => F[B])(implicit F: Applicative[F], B: Order[B]): F[SortedMap[B, NonEmptySeq[A]]]
    def groupByNevA[F[_], B](f: A => F[B])(implicit F: Applicative[F], B: Order[B]): F[SortedMap[B, NonEmptyVector[A]]]
    def groupByNellA[F[_], B](f: A => F[B])(implicit F: Applicative[F], B: Order[B]): F[SortedMap[B, NonEmptyLazyList[A]]]
}

Option

Ops

final class OptionOps[A](private val oa: Option[A]) extends AnyVal {
    // Exists:
    def toInvalidNec[B](b: => B): ValidatedNec[A, B]
    def toInvalidNel[B](b: => B): ValidatedNel[A, B]

    def toLeftNec[B](b: => B): EitherNec[A, B]
    def toLeftNel[B](b: => B): EitherNel[A, B]

    def toRightNec[B](b: => B): EitherNec[B, A] 
    def toRightNel[B](b: => B): EitherNel[B, A]

    def toValidNec[B](b: => B): ValidatedNec[B, A]
    def toValidNel[B](b: => B): ValidatedNel[B, A]

    // Missing:
    def toLeftNesA[B](b: => B)(implicit O: Order[A]): EitherNesA[A, B]
    def toLeftNeSeqA[B](b: => B): EitherNeSeqA[A, B]
    def toLeftNevA[B](b: => B): EitherNevA[A, B]
    def toLeftNellA[B](b: => B): EitherNellA[A, B]
    
    def toRightNesA[B](b: => B)(implicit O: Order[B]): EitherNesA[B, A] 
    def toRightNeSeqA[B](b: => B): EitherNeSeqA[B, A] 
    def toRightNevA[B](b: => B): EitherNevA[B, A] 
    def toRightNellA[B](b: => B): EitherNellA[B, A] 
    
    def toInvalidNesA[B](b: => B(implicit O: Order[A])): ValidatedNesA[A, B]
    def toInvalidNeSeqA[B](b: => B): ValidatedNeSeqA[A, B]
    def toInvalidNevA[B](b: => B): ValidatedNevA[A, B]
    def toInvalidNellA[B](b: => B): ValidatedNellA[A, B]

    def toValidNesA[B](b: => B)(implicit O: Order[B]): ValidatedNesA[B, A]
    def toValidNeSeqA[B](b: => B): ValidatedNeSeqA[B, A]
    def toValidNevA[B](b: => B): ValidatedNevA[B, A]
    def toValidNellA[B](b: => B): ValidatedNellA[B, A]
}

NonEmptyLazyList

Ops

class NonEmptyLazyListOps[A](private val value: NonEmptyLazyList[A])
    extends AnyVal
    with NonEmptyCollection[A, LazyList, NonEmptyLazyList] {
    // Exists:
     final def toNonEmptyVector: NonEmptyVector[A]
     final def toNonEmptyList: NonEmptyList[A]
     final def toNem[T, U](implicit ev: A <:< (T, U), order: Order[T]): NonEmptyMap[T, U]
     final def toNes[B >: A](implicit order: Order[B]): NonEmptySet[B]
     final def toNev[B >: A]: NonEmptyVector[B]

    // Missing:
    final def toNeSeq[B >: A]: NonEmptySeq[B]
}

Reducible

Ops

@typeclass trait Reducible[F[_]] extends Foldable[F] { self =>
    // Exists:
    def maximumByNel[A, B: Order](fa: F[A])(f: A => B): NonEmptyList[A]
    def maximumNel[A](fa: F[A])(implicit A: Order[A]): NonEmptyList[A] 
    def minimumByNel[A, B: Order](fa: F[A])(f: A => B): NonEmptyList[A]
    def minimumNel[A](fa: F[A])(implicit A: Order[A]): NonEmptyList[A] 
    def toNonEmptyList[A](fa: F[A]): NonEmptyList[A]

    // Missing:
    // All variations of above for 
    // Nec, Nev, Nes, NeSeq, NeLL
}

Syntax

trait Ops[F[_], A] extends Serializable {
    type TypeClassType <: Reducible[F]
    def self: F[A]
    
    // Exists:
    def minimumNel(implicit A: Order[A]): NonEmptyList[A]
    def maximumNel(implicit A: Order[A]): NonEmptyList[A]
    def minimumByNel[B](f: A => B)(implicit ev$1: Order[B]): NonEmptyList[A]
    def maximumByNel[B](f: A => B)(implicit ev$1: Order[B]): NonEmptyList[A]
    
    // Missing:
    // All variations of above for 
    // Nec, Nev, Nes, NeSeq, NeLL
}
@satorg
Copy link
Contributor

satorg commented Dec 3, 2021

The reason why #3998 got stuck on my side was a very valid concern from @johnynek on whether we should keep stuffing collection wrappers with ad-hoc methods or try to generalize some (or most) of them for appropriate typeclasses (Traverse, NonEmptyTraverse, Foldable, Reducible, etc).

I agree with Oscar that generalization is better so I am not confident that it makes sense to merge #3998 until we elaborate some more generic solution.

@zarthross
Copy link
Contributor Author

@satorg Agreed. That's why I wanted to list out all the methods that we had now, and open a discussion on how we can improve the current situation. These methods wouldn't exist if there wasn't some utility, so I think any general replacement needs to be nearly as convenient. I proposed 2 solutions but those only cover a subset of the missing methods.

There is likely a way to split these missing methods into different categories and address the individually. But figured a single list to get started would help show the entire picture.

@satorg
Copy link
Contributor

satorg commented Dec 3, 2021

I guess there's a typo in the description: ValidatedNel is enlisted as "missing", but in fact it is not:
https://github.com/typelevel/cats/blob/main/core/src/main/scala/cats/data/package.scala#L5

@zarthross
Copy link
Contributor Author

@satorg Thanks, corrected.

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

No branches or pull requests

2 participants