-
Notifications
You must be signed in to change notification settings - Fork 112
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
[RFC] Lattice foundation #353
Conversation
# Conflicts: # core/shared/src/main/scala/zio/prelude/Absorption.scala # core/shared/src/main/scala/zio/prelude/BottomTopShape.scala # core/shared/src/main/scala/zio/prelude/Complement.scala # core/shared/src/main/scala/zio/prelude/ComplementShape.scala # core/shared/src/main/scala/zio/prelude/DistributiveJoinMeet.scala # core/shared/src/main/scala/zio/prelude/Involution.scala # core/shared/src/main/scala/zio/prelude/JoinMeetShape.scala # core/shared/src/test/scala/zio/prelude/AbsorptionSpec.scala # core/shared/src/test/scala/zio/prelude/ComplementSpec.scala # core/shared/src/test/scala/zio/prelude/DistributiveJoinMeetSpec.scala # core/shared/src/test/scala/zio/prelude/InvolutionSpec.scala # docs/overview/diagrams.md
# Conflicts: # core/shared/src/main/scala/zio/prelude/Associative.scala # core/shared/src/test/scala/zio/prelude/AssociativeSpec.scala # core/shared/src/test/scala/zio/prelude/CommutativeSpec.scala # core/shared/src/test/scala/zio/prelude/IdentitySpec.scala # core/shared/src/test/scala/zio/prelude/InverseSpec.scala
…ludedMiddle and Noncontradiction
# Conflicts: # core/shared/src/test/scala/zio/prelude/AssociativeSpec.scala # core/shared/src/test/scala/zio/prelude/CommutativeSpec.scala # core/shared/src/test/scala/zio/prelude/IdempotentSpec.scala # core/shared/src/test/scala/zio/prelude/IdentitySpec.scala # core/shared/src/test/scala/zio/prelude/InverseSpec.scala # docs/overview/diagrams.md
# Conflicts: # docs/overview/diagrams.md
# Conflicts: # core/shared/src/test/scala/zio/prelude/CommutativeSpec.scala # docs/overview/diagrams.md
# Conflicts: # docs/overview/diagrams.md
# Conflicts: # core/shared/src/main/scala/zio/prelude/Associative.scala # core/shared/src/main/scala/zio/prelude/newtypes/package.scala # docs/overview/diagrams.md
# Conflicts: # core-tests/shared/src/test/scala/zio/prelude/IdempotentSpec.scala # core/shared/src/main/scala-3/zio/prelude/newtypes/package.scala # core/shared/src/main/scala/zio/prelude/Associative.scala # core/shared/src/test/scala/zio/prelude/CommutativeSpec.scala # core/shared/src/test/scala/zio/prelude/InverseSpec.scala # core/shared/src/test/scala/zio/prelude/NewtypeSpec.scala
Hello @adamgfraser, @jdegoes, @lemastero and anybody else involved. The issues here is how we represent Latices in ZIO Prelude. In contrast to for example
After a whole year I wasn't able to come up with any breakthrough, the options are still:
So how do we get out of this pickle? I would appreciate any help or advice. Btw I think it would be better to come to a conclusion on this PR first and only then finish #351, because here the challenges are more general. |
@sideeffffect I would be inclined to go with your second option and use our functionality for coherent types to make the appropriate instances available when working with any of the types. We could then define type aliases for common intersections of these different types. |
I explored the option (2) and here are my findings
|
I think this is definitely on the right track. I would define something like: type Lattice[A] = Associative[Max[A]] with Associative[Min[A]] I realize you defined it in terms of |
How could this help us? 🤔 What would be the signature of
I'm not entirely sure what you mean. Are you suggesting to rename |
package zio.prelude
import zio.prelude.newtypes._
object Example {
implicit final class LatticeSyntax[A](private val self: A) extends AnyVal {
def join(that: A)(implicit ev1: Associative[Max[A]], ev2: Associative[Min[A]]): A =
ev1.combine(Max(self), Max(that))
def meet(that: A)(implicit ev1: Associative[Max[A]], ev2: Associative[Min[A]]): A =
ev2.combine(Min(self), Min(that))
}
} |
🎉 🎉 🎉 It's been in from of my eyes the whole time. To my defense, I'm not a mathematician... And not an attentive reader. As per https://en.wikipedia.org/wiki/Lattice_(order)#General_lattice
That means we can describe a lattice with our Please have a new look at the code now, Adam 🙏 |
Big improvement! The main source of complexity we should try to eliminate at this point is the use of path dependent types. Here is one potential approach: package zio.prelude.experimental
trait Absorption[A] {
def join(l: => A, r: => A): A
def meet(l: => A, r: => A): A
}
object Absorption {
/**
* Summons an implicit `Absorption[A]`.
*/
def apply[A](implicit absorption: Absorption[A]): Absorption[A] =
absorption
implicit lazy val BoolInstance: Absorption[Boolean] =
new Absorption[Boolean] {
override def join(l: => Boolean, r: => Boolean): Boolean = l || r
override def meet(l: => Boolean, r: => Boolean): Boolean = l && r
}
implicit def SetInstance[A]: Absorption[Set[A]] =
new Absorption[Set[A]] {
override def join(l: => Set[A], r: => Set[A]): Set[A] = l | r
override def meet(l: => Set[A], r: => Set[A]): Set[A] = l & r
}
}
trait AbsorptionSyntax {
/**
* Provides infix syntax for joining or meeting two values.
*/
implicit class AbsorptionOps[A](private val l: A) {
/**
* A symbolic alias for `join`.
*/
def vvv(r: => A)(implicit absorption: Absorption[A]): A =
absorption.join(l, r)
/**
* Join two values.
*/
def join(r: => A)(implicit absorption: Absorption[A]): A =
absorption.join(l, r)
/**
* A symbolic alias for `meet`.
*/
def ^^^(r: => A)(implicit absorption: Absorption[A]): A =
absorption.meet(l, r)
/**
* Meet two values.
*/
def meet(r: => A)(implicit absorption: Absorption[A]): A =
absorption.meet(l, r)
}
} |
I'm not convinced that we want to drop type members. I originally added them at the suggestion of @jdegoes, btw: They are a big part of the modularity and expressivity story of the current design. Let's take this example: type BoundedLattice[A] = Absorption.Aux[A, BoundedSemilattice, BoundedSemilattice]
type OrthoComplementedLattice[A] = ExcludedMiddle[A] with Involution[A] with Noncontradiction[A] {
type Join[x] = BoundedSemilattice[x]
type Meet[x] = BoundedSemilattice[x]
}
type Ring[A] = Semiring[A] with Subtract[A] {
type Addition[x] <: AbelianGroup[x]
type Multiplication[x] <: Monoid[x]
} Here we say that a BoundedLattice is an Absorption where both Join and Meet form a BoundedSemilattice. But there could be exemplars of BoundedLattice where they satisfy more strict laws (subtype of Absorption) or where their Join or Meet satisfy stricter laws (subtypes of BoundedSemilattice). And all of these exeplars would be valid BoundedLattice. Or OrthoComplementedLattice satisfies ExcludedMiddle, Involution and Noncontradiction (and thus Absorption), but that's not enough, its Join and Meet both have to form BoundedSemilattice. But there could be a useful exemplar of a structure that satisfies ExcludedMiddle, Involution and Noncontradiction, but either Join or Meet (or both) are mere Semilattice. And that would be a different thing from OrthoComplementedLattice, right? Or if we take an example from the Ring PR: Ring is a Semiring where its Addition forms AbelianGroup and its Multiplication forms Monoid. (Or anythign that is more strict/specific than that.) But we have the freedom to define a structure which is a Semiring with Multiplication that is PartialInverse. With type members, we have this LEGO where we can assemble the pieces as we want, they allow for a very modular design. Scala's type system here allows us to express these structures very closely how they are in mathematics, which is great IMHO. Type members are also nice that, in contrast to type parameters, don't get in the way and have to interact with them only if you want to. I hope that this modularity will allow us to avoid making special case for everything, like you and @jdegoes talked about here Wouldn't you be worried that without the design with type members, we would loose the benefits outlined above? |
Right. I think the question is whether you want to specify those properties as part of one type alias or not. You could think of these abstractions as really describing three separate things: (1) the properties of some "maximum" operation, (2) the properties of some "minimum" operation, (3) the laws that describe how those two operations should relate to each other. So then the question is should this abstraction describe (1) or (1), (2), and (3). You could imagine a method like: def foo[A](implicit max: Identity[Max[A]], min: Associative[Min[A]], excludedMiddle: ExcludedMiddle[A]) = ??? Here you have arguably quite a precise and modular way of specifying what properties you need for the "join" operator, what properties you need for the "meet" operator, and how those two should relate together. This has the benefit of being simpler in some ways since there are no path dependent types and you are just requiring one or more capabilities as we do elsewhere. The disadvantage is that there is no "one type" for some of these abstractions. Being one of them just is having these two operations and having them relate to each other in a particular way. In a way that is perhaps more fundamental (there is nothing "more" to these structures than having these two operations and having them relate to each other in a particular way) but it is also nice to have a reification of that. I do worry that this adds a level of complexity beyond what we have used elsewhere in this library and I would be concerned about introducing it to teams that I would use other features of ZIO Prelude for. At the same time, these abstractions are inherently more esoteric and this is the experimental package so perhaps that is appropriate. I'm good with whatever approach you want to take but would just suggest you consider the complexity costs here and consider if there are ways to express these ideas that would be more accessible while reflecting their underlying structure. |
Adam, please have another look. |
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.
Sounds good. Let's get this in and then I agree we will have to see how it works.
🎉 🎉 🎉 |
* Update sbt-ci-release to 1.5.9 (#747) Co-authored-by: Ondra Pelech <ondra.pelech@gmail.com> * Scala 3.1 (#763) * Scala 3.1 * Use standard directory names and include laws in root * Rename ScalaDotty to Scala3 * rename Refined -> Smart Newtype (#768) * implement zvalidation.noneorfail (#767) * Update mdoc, sbt-mdoc to 2.2.24 (#769) * Update scalafmt-core to 3.0.7 (#770) * Update sbt-bloop to 1.4.10 (#771) * Update auxlib, javalib, nativelib, nscplugin, ... to 0.4.1 (#772) * Update sbt-ci-release to 1.5.10 (#764) * Run tests on 8 as the main Java version (#773) * Update Node.js to v16 (#776) Co-authored-by: Renovate Bot <bot@renovateapp.com> * Update sbt-unidoc to 0.5.0 (#775) Co-authored-by: Ondra Pelech <ondra.pelech@gmail.com> * Update Node.js to v16.13.0 (#777) Co-authored-by: Renovate Bot <bot@renovateapp.com> * WIP: Begin Newtype fix (#778) * begin newtype fix * another attempt * fix for scala 3 * fix example * [RFC] Lattice foundation (#353) * Lattice foundation * Add Complement and Involution * fix diagram * JoinMeetShape * add classic * fix diagram * Move to experimental * Use type members * Use `AndF` and `OrF` * Add BooleanAlgebra * fix * Derive AndF/OrF from JoinMeetShape instances, split Complement to ExcludedMiddle and Noncontradiction * fix diagram * fix diagrams * Lazy laws * Move to experimental and split to laws and tests * lawsNative * Update diagram * Absorption at the top * Reuse laws * Complement * Lattice without the reification, i.e. abstract (#1) * Improve Scala Steward config (#782) * Update scalafmt-core to 3.0.8 (#781) * Update autoupdate.yaml (#784) * Update silencer-lib, silencer-lib_2.13.6, ... to 1.7.7 (#785) * Update actions/checkout action to v2.4.0 (#788) Co-authored-by: Renovate Bot <bot@renovateapp.com> * fix subtype creation issue (#787) * Update sbt-bloop to 1.4.11 (#789) * Better error message when forgetting "inline" in Scala 3 Newtype (#790) * better error message when forgetting "inline" in Scala 3 Newtype * add error message when unable to read assertion at compile-time due to abstraction * add better newtype errors for scala 2 (#792) * Update scalafmt-core to 3.1.0 (#797) * Update sbt-scalafix to 0.9.32 (#800) * Update scalafmt-core to 3.1.1 (#801) * ZValidation: add get, getOrElse, getOrElseWith (#802) * Implement ZValidation.partition (#799) * implement zvalidation.partition * format * implement partitionmapv * Adds Associative.intercalate and ForEach.intercalate (#798) * Update sbt-scalafmt to 2.4.4 (#805) * Revert "Make Unit An Identity For State (#608)" (#807) This reverts commit 85ae020. * Update scalafmt-core to 3.1.2 (#808) * merge fix Co-authored-by: Scala Steward <43047562+scala-steward@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: Adam Fraser <adam.fraser@gmail.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Renovate Bot <bot@renovateapp.com> Co-authored-by: Alex Simkin <alex.simkin@outlook.com>
This is the foundation to have Lattice (and the whole family, like Distributive lattice, ...) algebraic structures and instances for Scala library/ZIO types.
You can see the skeleton of the hierarchy here:
https://github.com/sideeffffect/zio-prelude/blob/lattice/docs/overview/diagrams.md (named JoinMeet)
More is still to come, other laws, comments/documentation and instances for more types, currently there is just
Set[A]
to try things out.I would be interested in your feedback 😃
/cc @adamgfraser @jdegoes
There are some similarities to Ring-like structures, which are discussed in this PR #351 so maybe we can gain something when we will be able how the design decisions play out in both Type class hierarchies.