diff --git a/ledger-service/db-backend/BUILD.bazel b/ledger-service/db-backend/BUILD.bazel index 25262cb70a..175f1fb139 100644 --- a/ledger-service/db-backend/BUILD.bazel +++ b/ledger-service/db-backend/BUILD.bazel @@ -34,6 +34,9 @@ da_scala_library( runtime_deps = [ "@maven//:ch_qos_logback_logback_classic", ], + deps = [ + "//libs-scala/scala-utils", + ], ) da_scala_test( @@ -44,10 +47,12 @@ da_scala_test( "@maven//:org_scalacheck_scalacheck", "@maven//:org_scalatest_scalatest", "@maven//:org_scalatestplus_scalacheck_1_14", + "@maven//:org_scalaz_scalaz_core", ], scalacopts = lf_scalacopts, # data = ["//docs:quickstart-model.dar"], deps = [ ":db-backend", + "//libs-scala/scala-utils", ], ) diff --git a/ledger-service/db-backend/src/main/scala/com/digitalasset/http/dbbackend/Queries.scala b/ledger-service/db-backend/src/main/scala/com/digitalasset/http/dbbackend/Queries.scala index c755374a80..e4d40b63ac 100644 --- a/ledger-service/db-backend/src/main/scala/com/digitalasset/http/dbbackend/Queries.scala +++ b/ledger-service/db-backend/src/main/scala/com/digitalasset/http/dbbackend/Queries.scala @@ -3,13 +3,17 @@ package com.daml.http.dbbackend +import com.daml.scalautil.nonempty +import nonempty.{NonEmpty, +-:} +import nonempty.NonEmptyReturningOps._ + import doobie._ import doobie.implicits._ +import scala.collection.immutable.{Iterable, Seq => ISeq} import scalaz.{@@, Foldable, Functor, OneAnd, Tag} import scalaz.Id.Id import scalaz.syntax.foldable._ import scalaz.syntax.functor._ -import scalaz.syntax.std.boolean._ import scalaz.syntax.std.option._ import scalaz.std.stream.unfold import scalaz.std.AllInstances._ @@ -231,7 +235,7 @@ sealed abstract class Queries { gvs: Get[Vector[String]], pvs: Put[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 = ())) /** 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]( parties: OneAnd[Set, String], - queries: Seq[(SurrogateTpId, Fragment)], + queries: ISeq[(SurrogateTpId, Fragment)], trackMatchIndices: MatchedQueryMarker[T, Mark], )(implicit log: LogHandler, @@ -354,18 +358,22 @@ object Queries { oaa.copy(tail = oaa.tail.flatMap(Vector(a, _))) // 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[Map[A, B]] = - unfold(iter.groupBy(_._1).transform((_, i) => i.toList): Map[A, List[(_, B)]]) { m => - // invariant: every value of m is non-empty - m.nonEmpty option { - val hd = m transform { (_, abs) => - val (_, b) +: _ = abs - b + private[dbbackend] def uniqueSets[A, B](iter: Iterable[(A, B)]): Seq[NonEmpty[Map[A, B]]] = + unfold( + iter + .groupBy1(_._1) + .transform((_, i) => i.toList): Map[A, NonEmpty[List[(_, B)]]] + ) { + case NonEmpty(m) => + 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) } - (hd, tl) - } + case _ => None } private[http] val Postgres: Queries = PostgresQueries @@ -415,7 +423,7 @@ private object PostgresQueries extends Queries { private[http] override def selectContractsMultiTemplate[T[_], Mark]( parties: OneAnd[Set, String], - queries: Seq[(SurrogateTpId, Fragment)], + queries: ISeq[(SurrogateTpId, Fragment)], trackMatchIndices: MatchedQueryMarker[T, Mark], )(implicit log: LogHandler, @@ -450,8 +458,8 @@ private object PostgresQueries extends Queries { case MatchedQueryMarker.ByInt => type Ix = Int uniqueSets(queries.zipWithIndex map { case ((tpid, pred), ix) => (tpid, (pred, ix)) }).map { - preds: Map[SurrogateTpId, (Fragment, Ix)] => - val predHd +: predTl = preds.toVector + preds: NonEmpty[Map[SurrogateTpId, (Fragment, Ix)]] => + val predHd +-: predTl = preds.toVector val predsList = OneAnd(predHd, predTl).map { case (tpid, (predicate, _)) => (tpid, predicate) } @@ -561,7 +569,7 @@ private object OracleQueries extends Queries { private[http] override def selectContractsMultiTemplate[T[_], Mark]( parties: OneAnd[Set, String], - queries: Seq[(SurrogateTpId, Fragment)], + queries: ISeq[(SurrogateTpId, Fragment)], trackMatchIndices: MatchedQueryMarker[T, Mark], )(implicit log: LogHandler, diff --git a/ledger-service/db-backend/src/test/scala/com/digitalasset/http/dbbackend/QueriesSpec.scala b/ledger-service/db-backend/src/test/scala/com/digitalasset/http/dbbackend/QueriesSpec.scala index 6e48d27d62..f00e132242 100644 --- a/ledger-service/db-backend/src/test/scala/com/digitalasset/http/dbbackend/QueriesSpec.scala +++ b/ledger-service/db-backend/src/test/scala/com/digitalasset/http/dbbackend/QueriesSpec.scala @@ -6,6 +6,7 @@ package com.daml.http.dbbackend import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import scala.collection.immutable.Seq class QueriesSpec extends AnyWordSpec with Matchers with ScalaCheckDrivenPropertyChecks { "uniqueSets" should { diff --git a/libs-scala/scala-utils/BUILD.bazel b/libs-scala/scala-utils/BUILD.bazel index f48573d35a..16ad0321b7 100644 --- a/libs-scala/scala-utils/BUILD.bazel +++ b/libs-scala/scala-utils/BUILD.bazel @@ -7,6 +7,7 @@ load( "da_scala_test", "lf_scalacopts", ) +load("@scala_version//:index.bzl", "scala_major_version", "scala_version_suffix") scalacopts = lf_scalacopts + [ "-P:wartremover:traverser:org.wartremover.warts.NonUnitStatements", @@ -14,7 +15,15 @@ scalacopts = lf_scalacopts + [ da_scala_library( 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, tags = ["maven_coordinates=com.daml:scala-utils:__VERSION__"], visibility = [ @@ -27,7 +36,11 @@ da_scala_library( da_scala_test( name = "test", srcs = glob(["src/test/scala/**/*.scala"]), + plugins = [ + "@maven//:org_typelevel_kind_projector_{}".format(scala_version_suffix), + ], scala_deps = [ + "@maven//:com_chuusai_shapeless", "@maven//:org_scalaz_scalaz_core", ], scalacopts = scalacopts, diff --git a/libs-scala/scala-utils/src/main/2.12/scalautil/nonempty/NonEmptyCollCompat.scala b/libs-scala/scala-utils/src/main/2.12/scalautil/nonempty/NonEmptyCollCompat.scala new file mode 100644 index 0000000000..132536411c --- /dev/null +++ b/libs-scala/scala-utils/src/main/2.12/scalautil/nonempty/NonEmptyCollCompat.scala @@ -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] +} diff --git a/libs-scala/scala-utils/src/main/2.13/scalautil/nonempty/NonEmptyCollCompat.scala b/libs-scala/scala-utils/src/main/2.13/scalautil/nonempty/NonEmptyCollCompat.scala new file mode 100644 index 0000000000..52d0bbec13 --- /dev/null +++ b/libs-scala/scala-utils/src/main/2.13/scalautil/nonempty/NonEmptyCollCompat.scala @@ -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] +} diff --git a/libs-scala/scala-utils/src/main/scala/com/daml/scalautil/nonempty/NonEmpty.scala b/libs-scala/scala-utils/src/main/scala/com/daml/scalautil/nonempty/NonEmpty.scala new file mode 100644 index 0000000000..bb0210999c --- /dev/null +++ b/libs-scala/scala-utils/src/main/scala/com/daml/scalautil/nonempty/NonEmpty.scala @@ -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) +} diff --git a/libs-scala/scala-utils/src/main/scala/com/daml/scalautil/nonempty/NonEmptyReturningOps.scala b/libs-scala/scala-utils/src/main/scala/com/daml/scalautil/nonempty/NonEmptyReturningOps.scala new file mode 100644 index 0000000000..1cd4595fd0 --- /dev/null +++ b/libs-scala/scala-utils/src/main/scala/com/daml/scalautil/nonempty/NonEmptyReturningOps.scala @@ -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 + } +} diff --git a/libs-scala/scala-utils/src/main/scala/com/daml/scalautil/nonempty/Patterns.scala b/libs-scala/scala-utils/src/main/scala/com/daml/scalautil/nonempty/Patterns.scala new file mode 100644 index 0000000000..ce834a6c4c --- /dev/null +++ b/libs-scala/scala-utils/src/main/scala/com/daml/scalautil/nonempty/Patterns.scala @@ -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)) +} diff --git a/libs-scala/scala-utils/src/main/scala/com/daml/scalautil/nonempty/package.scala b/libs-scala/scala-utils/src/main/scala/com/daml/scalautil/nonempty/package.scala new file mode 100644 index 0000000000..87849158fb --- /dev/null +++ b/libs-scala/scala-utils/src/main/scala/com/daml/scalautil/nonempty/package.scala @@ -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 = :-+ +} diff --git a/libs-scala/scala-utils/src/test/scala/com/daml/scalautil/nonempty/NonEmptySpec.scala b/libs-scala/scala-utils/src/test/scala/com/daml/scalautil/nonempty/NonEmptySpec.scala new file mode 100644 index 0000000000..7619e8c9fd --- /dev/null +++ b/libs-scala/scala-utils/src/test/scala/com/daml/scalautil/nonempty/NonEmptySpec.scala @@ -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)) + } + } +}