-
Notifications
You must be signed in to change notification settings - Fork 97
add Cats instances for C[_] given CollectionTag[C] #1834
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: series/0.18
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| } |
| 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 | ||
|
|
||
| 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)) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
||
|
|
@@ -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] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| implicit def catsInstancesGivenCollectionTag[C[_]](implicit | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thought: wonder if we can avoid the |
||
| 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] | ||
| } | ||
| 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] | ||
| ) | ||
|
|
||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I put this in
.internalso that it wouldn't be imported if someone didimport smithy4s.interopcats.*. I'm happy to put it somewhere else if desired.There was a problem hiding this comment.
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 insmithy.interopcatsinstead.