Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -907,7 +907,10 @@ lazy val cats = projectMatrix
.settings(
isMimaEnabled := true,
libraryDependencies ++= Seq(
Dependencies.Cats.core.value
Dependencies.Cats.core.value,
Dependencies.Cats.laws.value % Test,
Dependencies.Cats.weaverDiscipline.value % Test,
Dependencies.Cats.alleycatsLaws.value % Test,
) ++ weaverDeps.value
)
.jvmPlatform(allJvmScalaVersions, jvmDimSettings)
Expand Down
32 changes: 32 additions & 0 deletions modules/cats/src-2/smithy4s/interopcats/internal/NotGiven.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2021-2025 Disney Streaming
*
* Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://disneystreaming.github.io/TOST-1.0.txt
*
* 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 smithy4s.interopcats.internal

import scala.annotation.{implicitAmbiguous, unused}

sealed trait NotGiven[A]

object NotGiven {
implicit def notGiven[A]: NotGiven[A] = new NotGiven[A] {}

@implicitAmbiguous(
"Found an implicit value of type ${A}, so NotGiven[${A}] cannot be satisfied."
)
implicit def amb1[A](implicit @unused ev: A): NotGiven[A] = null

implicit def amb2[A](implicit @unused ev: A): NotGiven[A] = null
}
22 changes: 22 additions & 0 deletions modules/cats/src-3/smithy4s/interopcats/internal/NotGiven.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2021-2025 Disney Streaming
*
* Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://disneystreaming.github.io/TOST-1.0.txt
*
* 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 smithy4s.interopcats.internal

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I put this in .internal so that it wouldn't be imported if someone did import smithy4s.interopcats.*. I'm happy to put it somewhere else if desired.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If it's in .internal, it means that users should really not use it (yourself included), put it in smithy.interopcats instead.


type NotGiven[+A] = scala.util.NotGiven[A]

object NotGiven:
export scala.util.NotGiven.given
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright 2021-2025 Disney Streaming
*
* Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://disneystreaming.github.io/TOST-1.0.txt
*
* 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 smithy4s.interopcats.instances

import cats._
import cats.data._
import cats.syntax.all._
import cats.kernel.instances.StaticMethods.wrapMutableIndexedSeq
import smithy4s.schema.CollectionTag

private[interopcats] class CatsInstancesForCollectionTag[
C[_]
] private[interopcats] (implicit CT: CollectionTag[C])
extends Traverse[C]
with Alternative[C]
with Monad[C]
with CoflatMap[C]
with Align[C] {

def pure[A](a: A): C[A] = CT.build(_(a))

override def flatMap[A, B](fa: C[A])(f: A => C[B]): C[B] =
CT.fromIterator {
CT.iterator(fa).flatMap { a =>
CT.iterator(f(a))
}
}

override def tailRecM[A, B](a: A)(f: A => C[Either[A, B]]): C[B] =
CT.build { (put: B => Unit) =>
@annotation.tailrec
def go(lists: List[Iterator[Either[A, B]]]): Unit =
lists match {
case hd :: tail if hd.isEmpty =>
go(tail)

case hd :: tail =>
hd.next() match {
case Right(b) =>
put(b)
go(hd :: tail)
case Left(a) =>
go(CT.iterator(f(a)) :: hd :: tail)
}

case Nil => ()
}

go(CT.iterator(f(a)) :: Nil)
}

override def traverse[G[_]: Applicative, A, B](
fa: C[A]
)(f: A => G[B]): G[C[B]] =
if (CT.isEmpty(fa)) CT.empty.pure[G]
else
Chain
.traverseViaChain {
val as = collection.mutable.ArrayBuffer[A]()
as ++= CT.iterator(fa)
wrapMutableIndexedSeq(as)
}(f)
.map(c => CT.fromIterator(c.iterator))

override def foldLeft[A, B](fa: C[A], b: B)(f: (B, A) => B): B =
CT.iterator(fa).foldLeft(b)(f)

override def foldRight[A, B](fa: C[A], lb: Eval[B])(
f: (A, Eval[B]) => Eval[B]
): Eval[B] = {
def loop(as: Iterator[A]): Eval[B] =
if (as.hasNext) f(as.next(), Eval.defer(loop(as)))
else lb

Eval.defer(loop(CT.iterator(fa)))
}

override def empty[A]: C[A] = CT.empty

override def combineK[A](x: C[A], y: C[A]): C[A] =
CT.fromIterator(CT.iterator(x) ++ CT.iterator(y))

override def coflatMap[A, B](fa: C[A])(f: C[A] => B): C[B] = CT.build {
(put: B => Unit) =>
CT.iterator(fa)
.foreach(a => put(f(CT.build(_(a)))))
}

override def functor: Functor[C] = this

override def align[A, B](fa: C[A], fb: C[B]): C[Ior[A, B]] = CT.build {
(put: Ior[A, B] => Unit) =>
@annotation.tailrec
def loop(as: Iterator[A], bs: Iterator[B]): Unit =
if (as.hasNext && bs.hasNext) {
put(Ior.Both(as.next(), bs.next()))
loop(as, bs)
} else if (as.hasNext) as.foreach(a => put(Ior.left(a)))
else if (bs.hasNext) bs.foreach(b => put(Ior.right(b)))
else ()

loop(CT.iterator(fa), CT.iterator(fb))
}
}
19 changes: 18 additions & 1 deletion modules/cats/src/smithy4s/interopcats/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ package smithy4s

import scala.util.hashing.MurmurHash3
import smithy4s.capability.MonadThrowLike
import cats.MonadThrow
import cats.{instances => _, _}
import cats.syntax.all._
import cats.kernel.Monoid
import smithy4s.interopcats.internal._
import smithy4s.schema.CollectionTag

package object interopcats {

Expand Down Expand Up @@ -60,4 +62,19 @@ package object interopcats {
MurmurHash3.finalizeHash(hashResult, hashes.length)
}

implicit def catsInstancesGivenCollectionTagForSet[C[_]](implicit
CT: CollectionTag[C],
@annotation.unused IsSet: C[Any] =:= Set[Any]
): Traverse[C] with Monad[C] =
new instances.CatsInstancesForCollectionTag[C]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

question: can you elaborate on why we have a separate instance for sets?

Cats normally doesn't even provide a functor for Set because of ordering problems and so on.

In addition, in a polymorphic context you won't see a value of C[Any] =:= Set[Any] (because the compiler can't know C is Set at that point), and it'd only be useful after you've matched specifically to case SetTag, at which point we might as well just provide a Traverse[Set].


implicit def catsInstancesGivenCollectionTag[C[_]](implicit

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

thought: wonder if we can avoid the NotGiven check by putting this in a separate trait (also inherited by this object), playing with implicit priority like Cats does with its own instances. That'd be a nice way to get rid of the scala2 NotGiven.

CT: CollectionTag[C],
@annotation.unused NotSet: NotGiven[C[Any] =:= Set[Any]]
): Traverse[C]
with Alternative[C]
with Monad[C]
with CoflatMap[C]
with Align[C] =
new instances.CatsInstancesForCollectionTag[C]
}
138 changes: 138 additions & 0 deletions modules/cats/test/src/CollectionTagLawTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright 2021-2025 Disney Streaming
*
* Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://disneystreaming.github.io/TOST-1.0.txt
*
* 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 smithy4s.interopcats

import alleycats.laws.discipline._
import cats._
import cats.kernel.CommutativeGroup
import cats.laws.discipline.arbitrary._
import cats.laws.discipline._
import cats.syntax.all._
import weaver._
import weaver.discipline._
import smithy4s.schema.CollectionTag

object CollectionTagLawTests extends FunSuite with Discipline {
private implicit def eqIndexedSeq[A: Eq]: Eq[IndexedSeq[A]] =
Eq.instance(_.zip(_).forall { case (a, b) => a === b })

private implicit def eqSet[A: Eq]: Eq[Set[A]] = Eq.instance(_.zip(_).forall {
case (a, b) => a === b
})

private implicit val miniIntAddition: CommutativeGroup[MiniInt] =
MiniInt.miniIntAddition

private implicit val list: CollectionTag[List] = CollectionTag.ListTag
private implicit val vector: CollectionTag[Vector] = CollectionTag.VectorTag
private implicit val indexedSeq: CollectionTag[IndexedSeq] =
CollectionTag.IndexedSeqTag
private implicit val set: CollectionTag[Set] = CollectionTag.SetTag

// List and Vector are fully lawful
checkAll(
"Monad[List] via CollectionTag.ListTag",
MonadTests(catsInstancesGivenCollectionTag[List])
.monad[MiniInt, MiniInt, MiniInt]
)
checkAll(
"Traverse[List] via CollectionTag.ListTag",
TraverseTests(catsInstancesGivenCollectionTag[List])
.traverse[MiniInt, MiniInt, MiniInt, MiniInt, Option, Option]
)
checkAll(
"Alternative[List] via CollectionTag.ListTag",
AlternativeTests(catsInstancesGivenCollectionTag[List])
.alternative[MiniInt, MiniInt, MiniInt]
)
checkAll(
"CoflatMap[List] via CollectionTag.ListTag",
CoflatMapTests(catsInstancesGivenCollectionTag[List])
.coflatMap[MiniInt, MiniInt, MiniInt]
)
checkAll(
"Align[List] via CollectionTag.ListTag",
AlignTests(catsInstancesGivenCollectionTag[List])
.align[MiniInt, MiniInt, MiniInt, MiniInt]
)

checkAll(
"Monad[Vector] via CollectionTag.VectorTag",
MonadTests(catsInstancesGivenCollectionTag[Vector])
.monad[MiniInt, MiniInt, MiniInt]
)
checkAll(
"Traverse[Vector] via CollectionTag.VectorTag",
TraverseTests(catsInstancesGivenCollectionTag[Vector])
.traverse[MiniInt, MiniInt, MiniInt, MiniInt, Option, Option]
)
checkAll(
"Alternative[Vector] via CollectionTag.VectorTag",
AlternativeTests(catsInstancesGivenCollectionTag[Vector])
.alternative[MiniInt, MiniInt, MiniInt]
)
checkAll(
"CoflatMap[Vector] via CollectionTag.VectorTag",
CoflatMapTests(catsInstancesGivenCollectionTag[Vector])
.coflatMap[MiniInt, MiniInt, MiniInt]
)
checkAll(
"Align[Vector] via CollectionTag.VectorTag",
AlignTests(catsInstancesGivenCollectionTag[Vector])
.align[MiniInt, MiniInt, MiniInt, MiniInt]
)

checkAll(
"Monad[IndexedSeq] via CollectionTag.IndexedSeqTag",
MonadTests(catsInstancesGivenCollectionTag[IndexedSeq])
.monad[MiniInt, MiniInt, MiniInt]
)
checkAll(
"Traverse[IndexedSeq] via CollectionTag.IndexedSeqTag",
TraverseTests(catsInstancesGivenCollectionTag[IndexedSeq])
.unorderedTraverse[MiniInt, MiniInt, MiniInt, Option, Option]
)
checkAll(
"Alternative[IndexedSeq] via CollectionTag.IndexedSeqTag",
AlternativeTests(catsInstancesGivenCollectionTag[IndexedSeq])
.alternative[MiniInt, MiniInt, MiniInt]
)
checkAll(
"CoflatMap[IndexedSeq] via CollectionTag.IndexedSeqTag",
CoflatMapTests(catsInstancesGivenCollectionTag[IndexedSeq])
.coflatMap[MiniInt, MiniInt, MiniInt]
)
checkAll(
"Align[IndexedSeq] via CollectionTag.IndexedSeqTag",
AlignTests(catsInstancesGivenCollectionTag[IndexedSeq])
.align[MiniInt, MiniInt, MiniInt, MiniInt]
)

// Monad[Set] is not fully lawful (see comments in alleycats) but
// it's mostly lawful and definitely useful
checkAll(
"FlatMapRecTests[Set] via CollectionTag.SetTag",
FlatMapRecTests(catsInstancesGivenCollectionTagForSet[Set])
.tailRecM[MiniInt]
)
checkAll(
"Traverse[Set] via CollectionTag.SetTag",
TraverseTests(catsInstancesGivenCollectionTagForSet[Set])
.unorderedTraverse[MiniInt, MiniInt, MiniInt, Option, Option]
)

}
6 changes: 6 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ object Dependencies {
val Cats = new {
val core: Def.Initialize[ModuleID] =
Def.setting("org.typelevel" %%% "cats-core" % "2.11.0")
val laws: Def.Initialize[ModuleID] =
Def.setting("org.typelevel" %%% "cats-laws" % "2.11.0")
val alleycatsLaws: Def.Initialize[ModuleID] =
Def.setting("org.typelevel" %%% "alleycats-laws" % "2.11.0")
val weaverDiscipline: Def.Initialize[ModuleID] =
Def.setting("org.typelevel" %%% "weaver-discipline" % "0.10.1")
}

val Monocle = new {
Expand Down