cats icon indicating copy to clipboard operation
cats copied to clipboard

Missing NonEmpty Collection Helper methods

Open zarthross opened this issue 4 years ago • 4 comments

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
}

zarthross avatar Dec 03 '21 21:12 zarthross

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.

satorg avatar Dec 03 '21 22:12 satorg

@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.

zarthross avatar Dec 03 '21 22:12 zarthross

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

satorg avatar Dec 03 '21 23:12 satorg

@satorg Thanks, corrected.

zarthross avatar Dec 03 '21 23:12 zarthross