non-empty newtypes (#8516)

* non-empty newtypes

* an operation

* add some map/set operations and make everything compile on 2.12 and 2.13

* +-: and :-+, with compatibility layer; docs

* move to nonempty package; add aliases for cons/snoc; fix SeqOps aliases

* ensure 2.12 aliases are inferrable

* groupBy1 and toList, use to prove uniqueSets's invariants

* prove immutability first

* matching variance in aliases

* prove the return property of uniqueSets, and use the proof

* tests for NonEmpty API

* rename sci alias to imm

* move RefinedOps to more obvious location

* more docs

CHANGELOG_BEGIN
CHANGELOG_END

* remove unused imports

* illustrate the scala.collection.Seq problem

* ideas for extension

* tests for toF

* tests for +-:

* explain difference with OneAnd
This commit is contained in:
Stephen Compall 2021-02-22 08:54:26 -05:00 committed by GitHub
parent d92f2c7003
commit 2e671e4e5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 431 additions and 19 deletions

View File

@ -34,6 +34,9 @@ da_scala_library(
runtime_deps = [ runtime_deps = [
"@maven//:ch_qos_logback_logback_classic", "@maven//:ch_qos_logback_logback_classic",
], ],
deps = [
"//libs-scala/scala-utils",
],
) )
da_scala_test( da_scala_test(
@ -44,10 +47,12 @@ da_scala_test(
"@maven//:org_scalacheck_scalacheck", "@maven//:org_scalacheck_scalacheck",
"@maven//:org_scalatest_scalatest", "@maven//:org_scalatest_scalatest",
"@maven//:org_scalatestplus_scalacheck_1_14", "@maven//:org_scalatestplus_scalacheck_1_14",
"@maven//:org_scalaz_scalaz_core",
], ],
scalacopts = lf_scalacopts, scalacopts = lf_scalacopts,
# data = ["//docs:quickstart-model.dar"], # data = ["//docs:quickstart-model.dar"],
deps = [ deps = [
":db-backend", ":db-backend",
"//libs-scala/scala-utils",
], ],
) )

View File

@ -3,13 +3,17 @@
package com.daml.http.dbbackend package com.daml.http.dbbackend
import com.daml.scalautil.nonempty
import nonempty.{NonEmpty, +-:}
import nonempty.NonEmptyReturningOps._
import doobie._ import doobie._
import doobie.implicits._ import doobie.implicits._
import scala.collection.immutable.{Iterable, Seq => ISeq}
import scalaz.{@@, Foldable, Functor, OneAnd, Tag} import scalaz.{@@, Foldable, Functor, OneAnd, Tag}
import scalaz.Id.Id import scalaz.Id.Id
import scalaz.syntax.foldable._ import scalaz.syntax.foldable._
import scalaz.syntax.functor._ import scalaz.syntax.functor._
import scalaz.syntax.std.boolean._
import scalaz.syntax.std.option._ import scalaz.syntax.std.option._
import scalaz.std.stream.unfold import scalaz.std.stream.unfold
import scalaz.std.AllInstances._ import scalaz.std.AllInstances._
@ -231,7 +235,7 @@ sealed abstract class Queries {
gvs: Get[Vector[String]], gvs: Get[Vector[String]],
pvs: Put[Vector[String]], pvs: Put[Vector[String]],
): Query0[DBContract[Unit, JsValue, JsValue, Vector[String]]] = ): Query0[DBContract[Unit, JsValue, JsValue, Vector[String]]] =
selectContractsMultiTemplate(parties, Seq((tpid, predicate)), MatchedQueryMarker.Unused) selectContractsMultiTemplate(parties, ISeq((tpid, predicate)), MatchedQueryMarker.Unused)
.map(_ copy (templateId = ())) .map(_ copy (templateId = ()))
/** Make the smallest number of queries from `queries` that still indicates /** Make the smallest number of queries from `queries` that still indicates
@ -244,7 +248,7 @@ sealed abstract class Queries {
*/ */
private[http] def selectContractsMultiTemplate[T[_], Mark]( private[http] def selectContractsMultiTemplate[T[_], Mark](
parties: OneAnd[Set, String], parties: OneAnd[Set, String],
queries: Seq[(SurrogateTpId, Fragment)], queries: ISeq[(SurrogateTpId, Fragment)],
trackMatchIndices: MatchedQueryMarker[T, Mark], trackMatchIndices: MatchedQueryMarker[T, Mark],
)(implicit )(implicit
log: LogHandler, log: LogHandler,
@ -354,18 +358,22 @@ object Queries {
oaa.copy(tail = oaa.tail.flatMap(Vector(a, _))) oaa.copy(tail = oaa.tail.flatMap(Vector(a, _)))
// Like groupBy but split into n maps where n is the longest list under groupBy. // Like groupBy but split into n maps where n is the longest list under groupBy.
// Invariant: every element of the result is non-empty private[dbbackend] def uniqueSets[A, B](iter: Iterable[(A, B)]): Seq[NonEmpty[Map[A, B]]] =
private[dbbackend] def uniqueSets[A, B](iter: Iterable[(A, B)]): Seq[Map[A, B]] = unfold(
unfold(iter.groupBy(_._1).transform((_, i) => i.toList): Map[A, List[(_, B)]]) { m => iter
// invariant: every value of m is non-empty .groupBy1(_._1)
m.nonEmpty option { .transform((_, i) => i.toList): Map[A, NonEmpty[List[(_, B)]]]
val hd = m transform { (_, abs) => ) {
val (_, b) +: _ = abs case NonEmpty(m) =>
b Some {
val hd = m transform { (_, abs) =>
val (_, b) +-: _ = abs
b
}
val tl = m collect { case (a, _ +-: NonEmpty(tl)) => (a, tl) }
(hd, tl)
} }
val tl = m collect { case (a, _ +: (tl @ (_ +: _))) => (a, tl) } case _ => None
(hd, tl)
}
} }
private[http] val Postgres: Queries = PostgresQueries private[http] val Postgres: Queries = PostgresQueries
@ -415,7 +423,7 @@ private object PostgresQueries extends Queries {
private[http] override def selectContractsMultiTemplate[T[_], Mark]( private[http] override def selectContractsMultiTemplate[T[_], Mark](
parties: OneAnd[Set, String], parties: OneAnd[Set, String],
queries: Seq[(SurrogateTpId, Fragment)], queries: ISeq[(SurrogateTpId, Fragment)],
trackMatchIndices: MatchedQueryMarker[T, Mark], trackMatchIndices: MatchedQueryMarker[T, Mark],
)(implicit )(implicit
log: LogHandler, log: LogHandler,
@ -450,8 +458,8 @@ private object PostgresQueries extends Queries {
case MatchedQueryMarker.ByInt => case MatchedQueryMarker.ByInt =>
type Ix = Int type Ix = Int
uniqueSets(queries.zipWithIndex map { case ((tpid, pred), ix) => (tpid, (pred, ix)) }).map { uniqueSets(queries.zipWithIndex map { case ((tpid, pred), ix) => (tpid, (pred, ix)) }).map {
preds: Map[SurrogateTpId, (Fragment, Ix)] => preds: NonEmpty[Map[SurrogateTpId, (Fragment, Ix)]] =>
val predHd +: predTl = preds.toVector val predHd +-: predTl = preds.toVector
val predsList = OneAnd(predHd, predTl).map { case (tpid, (predicate, _)) => val predsList = OneAnd(predHd, predTl).map { case (tpid, (predicate, _)) =>
(tpid, predicate) (tpid, predicate)
} }
@ -561,7 +569,7 @@ private object OracleQueries extends Queries {
private[http] override def selectContractsMultiTemplate[T[_], Mark]( private[http] override def selectContractsMultiTemplate[T[_], Mark](
parties: OneAnd[Set, String], parties: OneAnd[Set, String],
queries: Seq[(SurrogateTpId, Fragment)], queries: ISeq[(SurrogateTpId, Fragment)],
trackMatchIndices: MatchedQueryMarker[T, Mark], trackMatchIndices: MatchedQueryMarker[T, Mark],
)(implicit )(implicit
log: LogHandler, log: LogHandler,

View File

@ -6,6 +6,7 @@ package com.daml.http.dbbackend
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec import org.scalatest.wordspec.AnyWordSpec
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
import scala.collection.immutable.Seq
class QueriesSpec extends AnyWordSpec with Matchers with ScalaCheckDrivenPropertyChecks { class QueriesSpec extends AnyWordSpec with Matchers with ScalaCheckDrivenPropertyChecks {
"uniqueSets" should { "uniqueSets" should {

View File

@ -7,6 +7,7 @@ load(
"da_scala_test", "da_scala_test",
"lf_scalacopts", "lf_scalacopts",
) )
load("@scala_version//:index.bzl", "scala_major_version", "scala_version_suffix")
scalacopts = lf_scalacopts + [ scalacopts = lf_scalacopts + [
"-P:wartremover:traverser:org.wartremover.warts.NonUnitStatements", "-P:wartremover:traverser:org.wartremover.warts.NonUnitStatements",
@ -14,7 +15,15 @@ scalacopts = lf_scalacopts + [
da_scala_library( da_scala_library(
name = "scala-utils", name = "scala-utils",
srcs = glob(["src/main/scala/**/*.scala"]), srcs = glob(["src/main/scala/**/*.scala"]) + glob([
"src/main/{}/**/*.scala".format(scala_major_version),
]),
plugins = [
"@maven//:org_typelevel_kind_projector_{}".format(scala_version_suffix),
],
scala_deps = [
"@maven//:org_scalaz_scalaz_core",
],
scalacopts = scalacopts, scalacopts = scalacopts,
tags = ["maven_coordinates=com.daml:scala-utils:__VERSION__"], tags = ["maven_coordinates=com.daml:scala-utils:__VERSION__"],
visibility = [ visibility = [
@ -27,7 +36,11 @@ da_scala_library(
da_scala_test( da_scala_test(
name = "test", name = "test",
srcs = glob(["src/test/scala/**/*.scala"]), srcs = glob(["src/test/scala/**/*.scala"]),
plugins = [
"@maven//:org_typelevel_kind_projector_{}".format(scala_version_suffix),
],
scala_deps = [ scala_deps = [
"@maven//:com_chuusai_shapeless",
"@maven//:org_scalaz_scalaz_core", "@maven//:org_scalaz_scalaz_core",
], ],
scalacopts = scalacopts, scalacopts = scalacopts,

View File

@ -0,0 +1,11 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.scalautil.nonempty
import scala.collection.{IterableLike, SeqLike}
private[nonempty] object NonEmptyCollCompat {
type IterableOps[A, +CC[_], +C] = IterableLike[A, CC[A] with C]
type SeqOps[A, +CC[_], +C] = SeqLike[A, CC[A] with C]
}

View File

@ -0,0 +1,11 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.scalautil.nonempty
import scala.{collection => sc}
private[nonempty] object NonEmptyCollCompat {
type IterableOps[+A, +CC[_], +C] = sc.IterableOps[A, CC, C]
type SeqOps[+A, +CC[_], +C] = sc.SeqOps[A, CC, C]
}

View File

@ -0,0 +1,141 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.scalautil.nonempty
import scala.collection.{immutable => imm}, imm.Map, imm.Set
import scalaz.Id.Id
import scalaz.{Foldable, Traverse}
import scalaz.Leibniz, Leibniz.===
import scalaz.Liskov, Liskov.<~<
import NonEmptyCollCompat._
/** The visible interface of [[NonEmpty]]; use that value to access
* these members.
*/
sealed abstract class NonEmptyColl {
/** Use its alias [[com.daml.scalautil.nonempty.NonEmpty]]. */
type NonEmpty[+A]
/** Use its alias [[com.daml.scalautil.nonempty.NonEmptyF]]. */
type NonEmptyF[F[_], A] <: NonEmpty[F[A]]
private[nonempty] def substF[T[_[_]], F[_]](tf: T[F]): T[NonEmptyF[F, *]]
private[nonempty] def subst[F[_[_]]](tf: F[Id]): F[NonEmpty]
private[nonempty] def unsafeNarrow[Self](self: Self with imm.Iterable[_]): NonEmpty[Self]
/** Usable proof that [[NonEmpty]] is a subtype of its argument. (We cannot put
* this in an upper-bound, because that would prevent us from adding implicit
* methods that replace the stdlib ones.)
*/
def subtype[A]: NonEmpty[A] <~< A
/** Usable proof that [[NonEmptyF]] is actually equivalent to its [[NonEmpty]]
* parent type, not a strict subtype. (We want Scala to treat it as a strict
* subtype, usually, so that the type will be deconstructed by
* partial-unification correctly.)
*/
def equiv[F[_], A]: NonEmpty[F[A]] === NonEmptyF[F, A]
/** Check whether `self` is non-empty; if so, return it as the non-empty subtype. */
def apply[Self](self: Self with imm.Iterable[_]): Option[NonEmpty[Self]]
/** In pattern matching, think of [[NonEmpty]] as a sub-case-class of every
* [[imm.Iterable]]; matching `case NonEmpty(ne)` ''adds'' the non-empty type
* to `ne` if the pattern matches.
*
* You will get an unchecked warning if the selector is not statically of an
* immutable type. So [[scala.collection.Seq]] will not work.
*
* The type-checker will not permit you to apply this to a value that already
* has the [[NonEmpty]] type, so don't worry about redundant checks here.
*/
def unapply[Self](self: Self with imm.Iterable[_]): Option[NonEmpty[Self]] = apply(self)
}
/** If you ever have to import [[NonEmptyColl]] or anything from it, your Scala
* settings are probably wrong.
*/
object NonEmptyColl extends NonEmptyCollInstances {
private[nonempty] object Instance extends NonEmptyColl {
type NonEmpty[+A] = A
type NonEmptyF[F[_], A] = F[A]
private[nonempty] override def substF[T[_[_]], F[_]](tf: T[F]) = tf
private[nonempty] override def subst[F[_[_]]](tf: F[Id]) = tf
override def subtype[A] = Liskov.refl[A]
override def equiv[F[_], A] = Leibniz.refl
override def apply[Self](self: Self with imm.Iterable[_]) =
if (self.nonEmpty) Some(self) else None
private[nonempty] override def unsafeNarrow[Self](self: Self with imm.Iterable[_]) = self
}
implicit final class ReshapeOps[F[_], A](private val nfa: NonEmpty[F[A]]) extends AnyVal {
def toF: NonEmptyF[F, A] = NonEmpty.equiv[F, A](nfa)
}
// many of these Map and Set operations can return more specific map and set types;
// however, the way to do that is incompatible between Scala 2.12 and 2.13.
// So we won't do it at least until 2.12 support is removed
/** Operations that can ''return'' new maps. There is no reason to include any other
* kind of operation here, because they are covered by `#widen`.
*/
implicit final class MapOps[K, V](private val self: NonEmpty[Map[K, V]]) extends AnyVal {
private type ESelf = Map[K, V]
import NonEmpty.{unsafeNarrow => un}
// You can't have + because of the dumb string-converting thing in stdlib
def updated(key: K, value: V): NonEmpty[Map[K, V]] = un((self: ESelf).updated(key, value))
def ++(xs: Iterable[(K, V)]): NonEmpty[Map[K, V]] = un((self: ESelf) ++ xs)
def keySet: NonEmpty[Set[K]] = un((self: ESelf).keySet)
def transform[W](f: (K, V) => W): NonEmpty[Map[K, W]] = un((self: ESelf) transform f)
}
/** Operations that can ''return'' new sets. There is no reason to include any other
* kind of operation here, because they are covered by `#widen`.
*/
implicit final class SetOps[A](private val self: NonEmpty[Set[A]]) extends AnyVal {
private type ESelf = Set[A]
import NonEmpty.{unsafeNarrow => un}
// You can't have + because of the dumb string-converting thing in stdlib
def incl(elem: A): NonEmpty[Set[A]] = un((self: ESelf) + elem)
def ++(that: Iterable[A]): NonEmpty[Set[A]] = un((self: ESelf) ++ that)
}
implicit final class NEPreservingOps[A, C](
private val self: NonEmpty[IterableOps[A, imm.Iterable, C with imm.Iterable[A]]]
) {
import NonEmpty.{unsafeNarrow => un}
private type ESelf = IterableOps[A, imm.Iterable, C with imm.Iterable[A]]
def toList: NonEmpty[List[A]] = un((self: ESelf).toList)
def toVector: NonEmpty[Vector[A]] = un((self: ESelf).toVector)
// ideas for extension: safe head/tail (not valuable unless also using
// wartremover to disable partial Seq ops)
}
// Why not `map`? Because it's a little tricky to do portably. I suggest
// importing the appropriate Scalaz instances and using `.toF.map` if you need
// it; we can add collection-like `map` later if it seems to be really
// important.
implicit def traverse[F[_]](implicit F: Traverse[F]): Traverse[NonEmptyF[F, *]] =
NonEmpty.substF(F)
}
sealed abstract class NonEmptyCollInstances {
implicit def foldable[F[_]](implicit F: Foldable[F]): Foldable[NonEmptyF[F, *]] =
NonEmpty.substF(F)
import scala.language.implicitConversions
/** This adds the rest of the `A` API to `na`. However, because it is in the
* most distant parent class from `object NonEmptyColl`, it is tried last
* during method resolution. That's why our custom `+`, `transform`,
* `toList`, and other such methods win; their implicit conversions are
* defined '''in a subclass''' of this one.
*/
implicit def widen[A](na: NonEmpty[A]): A = NonEmpty.subtype[A](na)
implicit def widenF[F[+_], A](na: F[NonEmpty[A]]): F[A] =
Liskov.co[F, NonEmpty[A], A](NonEmpty.subtype)(na)
}

View File

@ -0,0 +1,23 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.scalautil.nonempty
import scala.collection.{immutable => imm}, imm.Map
import NonEmptyCollCompat._
/** Functions where ''the receiver'' is non-empty can be found implicitly with
* no further imports; they "just work". However, if we wish to refine a
* method on a built-in type that merely ''returns'' a nonempty-annotated type, we
* must import the contents of this object.
*/
object NonEmptyReturningOps {
implicit final class `NE Iterable Ops`[A, CC[_], C](
private val self: IterableOps[A, CC, C with imm.Iterable[A]]
) {
def groupBy1[K](f: A => K): Map[K, NonEmpty[C]] =
NonEmpty.subst[Lambda[f[_] => Map[K, f[C]]]](self groupBy f)
// ideas for extension: +-: and :-+ operators
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.scalautil.nonempty
import NonEmptyCollCompat._
/** Total version of [[+:]]. */
object +-: {
def unapply[A, CC[_], C](t: NonEmpty[SeqOps[A, CC, C]]): Some[(A, C)] =
Some((t.head, t.tail))
}
/** Total version of [[:+]]. */
object :-+ {
def unapply[A, CC[_], C](t: NonEmpty[SeqOps[A, CC, C]]): Some[(C, A)] =
Some((t.init, t.last))
}

View File

@ -0,0 +1,57 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.scalautil
package object nonempty {
val NonEmpty: NonEmptyColl = NonEmptyColl.Instance
/** A non-empty `A`. Implicitly converts to `A` in relevant contexts.
*
* Why use this instead of [[scalaz.OneAnd]?
*
* `OneAnd` is ''constructively'' non-empty; there is no way to "cheat".
* However, its focus on functorial use cases means that it is well-suited
* for uninterpreted sequences like List and Vector, but less so those where
* parts of the elements have some semantics, such as with Map and Set. For
* cases like Set you also have to do some extra work to make sure the `head`
* doesn't break the invariant of the underlying structure.
*
* By contrast, `NonEmpty` is ''nominally'' non-empty. We take care to
* define only those primitives that will preserve non-emptiness, but it is
* possible to make a mistake (for example, if you added flatMap but wrote
* the wrong signature). The benefits are that you get to treat them more
* like the underlying structure, because there is no structural difference,
* and operations on existing code will more transparently continue to work
* (and preserve the non-empty property where reasonable) than the equivalent
* port to use `OneAnd`.
*
* Using this library sensibly with Scala 2.12 requires `-Xsource:2.13` and
* `-Ypartial-unification`.
*/
type NonEmpty[+A] = NonEmpty.NonEmpty[A]
/** A subtype of `NonEmpty[F[A]]` where `A` is in position to be inferred
* properly. When attempting to fit a type to the type params `C[T]`, scalac
* will infer the following:
*
* type shape | C | T
* ----------------+----------------+-----
* NonEmpty[F[A]] | NonEmpty | F[A]
* NonEmptyF[F, A] | NonEmpty[F, *] | A
*
* If you want to `traverse` or `foldLeft` or `map` on A, the latter is far
* more convenient. So any value whose type is the former can be converted
* to the latter via the `toF` method.
*
* In fact, given any `NonEmpty[Foo]` where `Foo` can be matched to type
* parameters `C[T]`, `toF` will infer a `NonEmptyF[C, T]` for whatever
* values of C and T scalac chooses. For example, if `bar: NonEmpty[Map[K,
* V]]`, then `bar.toF: NonEmptyF[Map[K, *], V]`, because that's how scalac
* destructures that type.
*/
type NonEmptyF[F[_], A] = NonEmpty.NonEmptyF[F, A]
// aliases for Samir
val ±: : +-:.type = +-:
val : : :-+.type = :-+
}

View File

@ -0,0 +1,124 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.scalautil.nonempty
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.matchers.should.Matchers
import shapeless.test.illTyped
class NonEmptySpec extends AnyWordSpec with Matchers {
import scala.{collection => col}, col.{mutable => mut}, col.{immutable => imm}
"unapply" should {
"compile on immutable maps" in {
val NonEmpty(m) = imm.Map(1 -> 2)
(m: NonEmpty[imm.Map[Int, Int]]) should ===(imm.Map(1 -> 2))
}
"compile on immutable seqs and maps" in {
val NonEmpty(s) = imm.Seq(3)
(s: NonEmpty[imm.Seq[Int]]) should ===(imm.Seq(3))
}
"reject empty maps" in {
imm.Map.empty[Int, Int] match {
case NonEmpty(_) => fail("empty")
case _ => succeed
}
}
"reject empty seqs" in {
imm.Seq.empty[Int] match {
case NonEmpty(_) => fail("empty")
case _ => succeed
}
}
}
"groupBy1" should {
import NonEmptyReturningOps._
// wrapping with Set in a variable is a nice trick to disable subtyping and
// implicit conversion (strong and weak conformance, SLS §3.5.2-3), so you
// only see what an expression really infers as exactly
"produce Lists for Lists" in {
val g = Set(List(1) groupBy1 identity)
g: Set[imm.Map[Int, NonEmpty[List[Int]]]]
}
"produce Vectors for Vectors" in {
val g = Set(Vector(1) groupBy1 identity)
g: Set[imm.Map[Int, NonEmpty[Vector[Int]]]]
}
"produce Sets for Sets" in {
val g = Set(imm.Set(1) groupBy1 identity)
g: Set[imm.Map[Int, NonEmpty[imm.Set[Int]]]]
}
"produce Seqs for Seqs" in {
val g = Set(imm.Seq(1) groupBy1 identity)
g: Set[imm.Map[Int, NonEmpty[imm.Seq[Int]]]]
}
"reject maybe-mutable structures" in {
illTyped(
"col.Seq(1) groupBy1 identity",
"(?s).*?groupBy1 is not a member of (scala.collection.)?Seq.*",
)
}
}
"toF" should {
"destructure the Set type" in {
val NonEmpty(s) = imm.Set(1)
val g = Set(s.toF)
g: Set[NonEmptyF[imm.Set, Int]]
}
"destructure the Map type" in {
val NonEmpty(m) = Map(1 -> 2)
val g = Set(m.toF)
g: Set[NonEmptyF[Map[Int, *], Int]]
}
"allow underlying NonEmpty operations" in {
val NonEmpty(s) = imm.Set(1)
((s.toF incl 2): NonEmpty[imm.Set[Int]]) should ===(imm.Set(1, 2))
}
"allow access to Scalaz methods" in {
import scalaz.syntax.functor._, scalaz.std.map._
val NonEmpty(m) = imm.Map(1 -> 2)
(m.toF.map((3, _)): NonEmptyF[imm.Map[Int, *], (Int, Int)]) should ===(imm.Map(1 -> ((3, 2))))
}
}
"+-:" should {
val NonEmpty(s) = Vector(1, 2)
"preserve its tail type" in {
val h +-: t = s
((h, t): (Int, Vector[Int])) should ===((1, Vector(2)))
}
"have ±: alias" in {
val h ±: t = s
((h, t): (Int, Vector[Int])) should ===((1, Vector(2)))
}
}
// why we don't allow `scala.collection` types
"scala.collection.Seq" must {
"accept that its non-emptiness is ephemeral" in {
val ms = mut.Buffer(1)
val cs: col.Seq[Int] = ms
val csIsNonEmpty = cs.nonEmpty
ms.clear()
(csIsNonEmpty, cs) should ===((true, col.Seq.empty))
}
}
}