Skip to content

Commit

Permalink
Add NonEmptyMap (#2141)
Browse files Browse the repository at this point in the history
* Add NonEmptyMap

* Add more docs

* Move to newtype encoding

* Rename NonEmptyMap#get to not conflict with Reducible.get

* Fix 2.11 and 2.10 compilation

* Fix 2.10 as well

* Remove semilattice instance

* Extra tests

* Add groupByNem to NonEmptyList

* Hide more newtype machinery

* Move newtype to its own file

* Address feedback

* Remove context bounds

* Make newtype private

* keys now returns a NonEmptySet
  • Loading branch information
Luka Jacobowitz authored and kailuowang committed Mar 16, 2018
1 parent 246b0e0 commit 55e0902
Show file tree
Hide file tree
Showing 6 changed files with 566 additions and 0 deletions.
13 changes: 13 additions & 0 deletions core/src/main/scala/cats/data/Newtype2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cats
package data

/**
* Helper trait for `newtype`s with two type parameters.
* These allow you to create a zero-allocation wrapper around a specific type.
* Similar to `AnyVal` value classes, but never have any runtime overhead.
*/
private[data] trait Newtype2 { self =>
private[data] type Base
private[data] trait Tag extends Any
type Type[A, +B] <: Base with Tag
}
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
320 changes: 320 additions & 0 deletions core/src/main/scala/cats/data/NonEmptyMapImpl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
/*
* 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._

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, A](head: (K, A), tail: SortedMap[K, A])(implicit K: Order[K]): NonEmptyMap[K, A] =
create(SortedMap(head)(K.toOrdering) ++ tail)


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

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

implicit def catsNonEmptyMapOps[K, A](value: Type[K, A]): NonEmptyMapOps[K, A] =
new NonEmptyMapOps(value)


}

sealed class NonEmptyMapOps[K, A](val value: NonEmptyMap[K, A]) {
/**
* 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: NonEmptySet[K] = NonEmptySet.fromSetUnsafe(toSortedMap.keySet)

/**
* Returns the first key-value pair of this map.
*/
def head: (K, A) = toSortedMap.head
/**
* Returns the last key-value pair of this map.
*/
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 `fa` and combine it 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
}
}

4 changes: 4 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,10 @@ package object data {
def NonEmptyStream[A](head: A, tail: A*): NonEmptyStream[A] =
OneAnd(head, tail.toStream)


type NonEmptyMap[K, +A] = NonEmptyMapImpl.Type[K, A]
val NonEmptyMap = NonEmptyMapImpl

type NonEmptySet[A] = NonEmptySetImpl.Type[A]
val NonEmptySet = NonEmptySetImpl

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 @@ -80,6 +80,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

0 comments on commit 55e0902

Please sign in to comment.