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 NonEmptyMap #2141

Merged
merged 16 commits into from
Mar 16, 2018
15 changes: 15 additions & 0 deletions core/src/main/scala/cats/data/NonEmptyList.scala
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,21 @@ final case class NonEmptyList[+A](head: A, tail: List[A]) {
case (k, v) => (k, NonEmptyList.fromListUnsafe(v.result))
} : TreeMap[B, NonEmptyList[A]]
}

/**
* Groups elements inside this `NonEmptyList` according to the `Order`
* of the keys produced by the given mapping function.
*
* {{{
* scala> import cats.data._
* scala> import cats.instances.boolean._
* scala> val nel = NonEmptyList.of(12, -2, 3, -5)
* scala> nel.groupByNem(_ >= 0)
* res0: NonEmptyMap[Boolean, NonEmptyList[Int]] = Map(false -> NonEmptyList(-2, -5), true -> NonEmptyList(12, 3))
* }}}
*/
def groupByNem[B](f: A => B)(implicit B: Order[B]): NonEmptyMap[B, NonEmptyList[A]] =
NonEmptyMap.fromMapUnsafe(groupBy(f))
}

object NonEmptyList extends NonEmptyListInstances {
Expand Down
327 changes: 327 additions & 0 deletions core/src/main/scala/cats/data/NonEmptyMapImpl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
/*
* Copyright (c) 2018 Luka Jacobowitz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package cats
package data

import cats.instances.sortedMap._
import cats.kernel._
import cats.{Always, Apply, Eval, Foldable, Functor, Later, NonEmptyTraverse, Now, SemigroupK, Show}

import scala.collection.immutable._

trait Newtype2 { self =>
private[data] type Base
private[data] trait Tag extends Any
private[cats] type Type[A, +B] <: Base with Tag
}


private[data] object NonEmptyMapImpl extends NonEmptyMapInstances with Newtype2 {

private[cats] def create[K, A](m: SortedMap[K, A]): Type[K, A] =
m.asInstanceOf[Type[K, A]]

private[cats] def unwrap[K, A](m: Type[K, A]): SortedMap[K, A] =
m.asInstanceOf[SortedMap[K, A]]

def fromMap[K: Order, A](as: SortedMap[K, A]): Option[NonEmptyMap[K, A]] =
if (as.nonEmpty) Option(create(as)) else None

def fromMapUnsafe[K: Order, A](m: SortedMap[K, A]): NonEmptyMap[K, A] =
if (m.nonEmpty) create(m)
else throw new IllegalArgumentException("Cannot create NonEmptyMap from empty map")

def apply[K: Order, A](head: (K, A), tail: SortedMap[K, A]): NonEmptyMap[K, A] =
create(SortedMap(head)(Order[K].toOrdering) ++ tail)
Copy link
Contributor

Choose a reason for hiding this comment

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

This and I think some other methods don't follow the convention that we kind of settled on in the early days of Cats here. Admittedly, that never got added to the guidelines, and I'm not sure whether we have been consistent about that in other places.

Copy link
Member Author

Choose a reason for hiding this comment

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

I personally don't have a strong opinion on this and didn't know we had such a convention 😄, you're right we should maybe reopen discussion and add it to the guideline, but for now I'll just change it, so we can avoid calling apply on the companion 👍 Thanks!



def of[K: Order, A](a: (K, A), as: (K, A)*): NonEmptyMap[K, A] =
create(SortedMap(as: _*)(Order[K].toOrdering) + a)

def one[K: Order, A](k: K, a: A): NonEmptyMap[K, A] =
create(SortedMap((k, a))(Order[K].toOrdering))

implicit def catsNonEmptyMapOps[K, A](value: Type[K, A]): NonEmptyMapOps[K, A] =
Copy link
Contributor

Choose a reason for hiding this comment

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

This is supposedly a public method, but its return type is private[data].

new NonEmptyMapOps(value)


Copy link
Member

Choose a reason for hiding this comment

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

Nitpicky, I know, but there are a lot of these double empty lines around.

}

private[data] sealed class NonEmptyMapOps[K, A](val value: NonEmptyMapImpl.Type[K, A]) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe private val value?

/**
* Converts this map to a `SortedMap`.
*/
def toSortedMap: SortedMap[K, A] = NonEmptyMapImpl.unwrap(value)

private implicit val ordering: Ordering[K] = toSortedMap.ordering
private implicit val order: Order[K] = Order.fromOrdering

/**
* Alias for [[concat]]
*/
def ++(as: NonEmptyMap[K, A]): NonEmptyMap[K, A] = concat(as)
/**
* Appends this NEM to another NEM, producing a new `NonEmptyMap`.
*/
def concat(as: NonEmptyMap[K, A]): NonEmptyMap[K, A] = NonEmptyMapImpl.create(toSortedMap ++ as.toSortedMap)

/**
* Removes a key from this map, returning a new SortedMap.
*/
def -(key: K): SortedMap[K, A] = toSortedMap - key


/**
* Adds a key-value pair to this map, returning a new `NonEmptyMap`.
* */
def add(ka: (K, A)): NonEmptyMap[K, A] = NonEmptyMapImpl.create(toSortedMap + ka)

/**
* Applies f to all the elements
*/
def map[B](f: A ⇒ B): NonEmptyMap[K, B] =
NonEmptyMapImpl.create(Functor[SortedMap[K, ?]].map(toSortedMap)(f))

/**
* Optionally returns the value associated with the given key.
*/
def lookup(k: K): Option[A] = toSortedMap.get(k)

/**
* Returns a `SortedSet` containing all the keys of this map.
*/
def keys: SortedSet[K] = toSortedMap.keySet
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this be NonEmpty(Sorted)Set?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, as mentioned before here :) #2141 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

2141 merged :)


/**
* Returns the first key-value pair of this map.
*/
def head: (K, A) = toSortedMap.head
/**
* Returns the first key-value pair of this map.
Copy link
Contributor

Choose a reason for hiding this comment

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

first -> last

*/
def last: (K, A) = toSortedMap.last

/**
* Returns all the key-value pairs, except for the first.
*/
def tail: SortedMap[K, A] = toSortedMap.tail

/**
* Alias for [[lookup]]
*/
def apply(key: K): Option[A] = lookup(key)

/**
* Checks whether this map contains a binding for the given key.
*/
def contains(key: K): Boolean = toSortedMap.contains(key)


/**
* Tests whether a predicate holds for all elements of this map.
*/
def forall(p: A ⇒ Boolean): Boolean = toSortedMap.forall { case (_, a) => p(a) }

/**
* Tests whether a predicate holds for at least one element of this map.
*/
def exists(f: A ⇒ Boolean): Boolean = toSortedMap.exists { case (_, a) => f(a) }

/**
* Returns the first value along with its key, that matches the given predicate.
*/
def find(f: A ⇒ Boolean): Option[(K, A)] = toSortedMap.find { case (_, a) => f(a) }

/**
* Filters all elements of this map that do not satisfy the given predicate.
*/
def filter(p: A ⇒ Boolean): SortedMap[K, A] = toSortedMap.filter { case (_, a) => p(a) }

/**
* Filters all elements of this map that satisfy the given predicate.
*/
def filterNot(p: A ⇒ Boolean): SortedMap[K, A] = filter(t => !p(t))


/**
* Left-associative fold using f.
*/
def foldLeft[B](b: B)(f: (B, A) => B): B =
toSortedMap.foldLeft(b)((b, t) => f(b, t._2))

/**
* Right-associative fold using f.
*/
def foldRight[B](lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] =
Foldable[SortedMap[K, ?]].foldRight(toSortedMap, lb)(f)

/**
* Left-associative reduce using f.
*/
def reduceLeft(f: (A, A) => A): A =
reduceLeftTo(identity)(f)


/**
* Apply `f` to the "initial element" of this map and lazily combine it
Copy link
Contributor

Choose a reason for hiding this comment

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

What does lazily mean in this case?

Copy link
Member Author

Choose a reason for hiding this comment

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

A mistake 😄

* with every other value using the given function `g`.
*/
def reduceLeftTo[B](f: A => B)(g: (B, A) => B): B =
tail.foldLeft(f(head._2))((b, a) => g(b, a._2))

/**
* Right-associative reduce using f.
*/
def reduceRight(f: (A, Eval[A]) => Eval[A]): Eval[A] =
reduceRightTo(identity)(f)

/**
* Apply `f` to the "initial element" of this map and lazily combine it
* with every other value using the given function `g`.
*/
def reduceRightTo[B](f: A => B)(g: (A, Eval[B]) => Eval[B]): Eval[B] =
Always((head, tail)).flatMap { case ((_, a), ga) =>
Foldable[SortedMap[K, ?]].reduceRightToOption(ga)(f)(g).flatMap {
case Some(b) => g(a, Now(b))
case None => Later(f(a))
}
}


/**
* Reduce using the Semigroup of A
*/
def reduce(implicit S: Semigroup[A]): A =
reduceLeft(S.combine)

private def reduceRightToOptionWithKey[V, B](fa: SortedMap[K, V])(f: (K, V) => B)(g: ((K, V), Eval[B]) => Eval[B]): Eval[Option[B]] =
Foldable.iterateRight(fa.toIterable, Now(Option.empty[B])) { (a, lb) =>
lb.flatMap {
case Some(b) => g(a, Now(b)).map(Some(_))
case None => Later(Some(f.tupled(a)))
}
}

/**
* Given a function which returns a G effect, thread this effect
* through the running of this function on all the values in this map,
* returning an NonEmptyMap[K, B] in a G context.
*/
def nonEmptyTraverse[G[_], B](f: A => G[B])(implicit G: Apply[G]): G[NonEmptyMap[K, B]] =
reduceRightToOptionWithKey[A, G[SortedMap[K, B]]](tail)({ case (k, a) =>
G.map(f(a))(b => SortedMap.empty[K, B] + ((k, b)))
}) { (t, lglb) =>
G.map2Eval(f(t._2), lglb)((b, bs) => bs + ((t._1, b)))
}.map {
case None => G.map(f(head._2))(a => NonEmptyMapImpl.one(head._1, a))
case Some(gtail) => G.map2(f(head._2), gtail)((a, bs) => NonEmptyMapImpl((head._1, a), bs))
}.value

/**
* Typesafe stringification method.
*
* This method is similar to .toString except that it stringifies
* values according to Show[_] instances, rather than using the
* universal .toString method.
*/
def show(implicit A: Show[A], K: Show[K]): String =
s"NonEmpty${Show[SortedMap[K, A]].show(toSortedMap)}"

/**
* Typesafe equality operator.
*
* This method is similar to == except that it only allows two
* NonEmptySet[A] values to be compared to each other, and uses
* equality provided by Eq[_] instances, rather than using the
* universal equality provided by .equals.
*/
def ===(that: NonEmptyMap[K, A])(implicit A: Eq[A]): Boolean =
Eq[SortedMap[K, A]].eqv(toSortedMap, that.toSortedMap)

/**
* Returns the amount of key-value pars in this map.
*/
def length: Int = toSortedMap.size

}

private[data] sealed abstract class NonEmptyMapInstances {


implicit def catsDataInstancesForNonEmptyMap[K: Order]: SemigroupK[NonEmptyMap[K, ?]] with NonEmptyTraverse[NonEmptyMap[K, ?]] =
new SemigroupK[NonEmptyMap[K, ?]] with NonEmptyTraverse[NonEmptyMap[K, ?]] {

override def map[A, B](fa: NonEmptyMap[K, A])(f: A => B): NonEmptyMap[K, B] =
fa.map(f)

def combineK[A](a: NonEmptyMap[K, A], b: NonEmptyMap[K, A]): NonEmptyMap[K, A] =
a ++ b

override def size[A](fa: NonEmptyMap[K, A]): Long = fa.length.toLong

override def reduceLeft[A](fa: NonEmptyMap[K, A])(f: (A, A) => A): A =
fa.reduceLeft(f)

override def reduce[A](fa: NonEmptyMap[K, A])(implicit A: Semigroup[A]): A =
fa.reduce

def reduceLeftTo[A, B](fa: NonEmptyMap[K, A])(f: A => B)(g: (B, A) => B): B = fa.reduceLeftTo(f)(g)

def reduceRightTo[A, B](fa: NonEmptyMap[K, A])(f: A => B)(g: (A, Eval[B]) => Eval[B]): Eval[B] =
fa.reduceRightTo(f)(g)

def nonEmptyTraverse[G[_], A, B](fa: NonEmptyMap[K, A])(f: A => G[B])(implicit G: Apply[G]) =
fa nonEmptyTraverse f

override def foldLeft[A, B](fa: NonEmptyMap[K, A], b: B)(f: (B, A) => B): B =
fa.foldLeft(b)(f)

override def foldRight[A, B](fa: NonEmptyMap[K, A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] =
fa.foldRight(lb)(f)

override def foldMap[A, B](fa: NonEmptyMap[K, A])(f: A => B)(implicit B: Monoid[B]): B =
fa.foldLeft(B.empty)((b, a) => B.combine(b, f(a)))

override def fold[A](fa: NonEmptyMap[K, A])(implicit A: Monoid[A]): A =
fa.reduce

override def find[A](fa: NonEmptyMap[K, A])(f: A => Boolean): Option[A] =
fa.find(f).map(_._2)

override def forall[A](fa: NonEmptyMap[K, A])(p: A => Boolean): Boolean =
fa.forall(p)

override def exists[A](fa: NonEmptyMap[K, A])(p: A => Boolean): Boolean =
fa.exists(p)

override def toNonEmptyList[A](fa: NonEmptyMap[K, A]): NonEmptyList[A] =
NonEmptyList(fa.head._2, fa.tail.toList.map(_._2))
}

implicit def catsDataEqForNonEmptyMap[K: Order, A: Eq]: Eq[NonEmptyMap[K, A]] =
new Eq[NonEmptyMap[K, A]]{
def eqv(x: NonEmptyMap[K, A], y: NonEmptyMap[K, A]): Boolean = x === y
}

implicit def catsDataShowForNonEmptyMap[K: Show, A: Show]: Show[NonEmptyMap[K, A]] =
Show.show[NonEmptyMap[K, A]](_.show)

implicit def catsDataBandForNonEmptyMap[K, A]: Band[NonEmptyMap[K, A]] = new Band[NonEmptyMap[K, A]] {
def combine(x: NonEmptyMap[K, A], y: NonEmptyMap[K, A]): NonEmptyMap[K, A] = x ++ y
}
}

3 changes: 3 additions & 0 deletions core/src/main/scala/cats/data/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ package object data {
def NonEmptyStream[A](head: A, tail: A*): NonEmptyStream[A] =
OneAnd(head, tail.toStream)

type NonEmptyMap[K, +A] = NonEmptyMapImpl.Type[K, A]
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be called NonEmptySortedMap? There might be in the future a hashtable-based non-empty map ...

val NonEmptyMap = NonEmptyMapImpl
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the inferred type here? I'd guess NonEmptyMapImpl.type, but that cannot be, since it's private[data] ...

Copy link
Member Author

Choose a reason for hiding this comment

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

It is its own type aka, cats.data.NonEmptyMap.type :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, sure ... but that's not what I asked. The inferred type seems to be this:

private[this] val NonEmptyMap: cats.data.NonEmptyMapImpl.type = NonEmptyMapImpl;
<stable> <accessor> def NonEmptyMap: cats.data.NonEmptyMapImpl.type = `package`.this.NonEmptyMap;

Which means that we have a private[data] type on a public API.


type ReaderT[F[_], A, B] = Kleisli[F, A, B]
val ReaderT = Kleisli

Expand Down
10 changes: 10 additions & 0 deletions laws/src/main/scala/cats/laws/discipline/Arbitrary.scala
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ object arbitrary extends ArbitraryInstances0 {
implicit def catsLawsArbitraryForZipNonEmptyList[A](implicit A: Arbitrary[A]): Arbitrary[ZipNonEmptyList[A]] =
Arbitrary(implicitly[Arbitrary[NonEmptyList[A]]].arbitrary.map(nel => new ZipNonEmptyList(nel)))

implicit def arbNonEmptyMap[K: Order, A](implicit A: Arbitrary[A], K: Arbitrary[K]): Arbitrary[NonEmptyMap[K, A]] =
Arbitrary(for {
fa <- implicitly[Arbitrary[SortedMap[K, A]]].arbitrary
k <- K.arbitrary
a <- A.arbitrary
} yield NonEmptyMap((k, a), fa))

implicit def cogenNonEmptyMap[K: Order : Cogen, A: Order : Cogen]: Cogen[NonEmptyMap[K, A]] =
Cogen[SortedMap[K, A]].contramap(_.toSortedMap)

implicit def catsLawsArbitraryForEitherT[F[_], A, B](implicit F: Arbitrary[F[Either[A, B]]]): Arbitrary[EitherT[F, A, B]] =
Arbitrary(F.arbitrary.map(EitherT(_)))

Expand Down
Loading