mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-19 08:48:21 +03:00
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:
parent
d92f2c7003
commit
2e671e4e5d
@ -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",
|
||||
],
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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]
|
||||
}
|
@ -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]
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
@ -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 = :-+
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user