Repository navigation
Description
Continuing Taneb/groups#7 (comment) in a more appropriate place:
Context: patch currently provides (but does not directly use) a Group class with lawless instance (Ord k, Group g) => Group (MonoidalMap k g). (let x = fromList [(1, y)] in x ~~ x evaluates to fromList [(1, mempty)] instead of mempty.) I'm sure that something downstream is using this class to provide efficient Patch instances or something.
Context: patch, groups, group-theory (via reexport from groups), and monoid-subclasses all provide a class that requires (<>) to be commutative. Some as a subclass of Semigroup, some as a subclass of their Group.
There are two options:
Option 1 - Create (here or elsewhere) and use an InverseSemigroup class
Since it can have lawful instance (Ord k, InverseSemigroup g) => InverseSemigroup (MonoidalMap k g):
class Semigroup g => InverseSemigroup g where
-- Laws:
-- x <> inv x <> x = x
-- inv x <> x <> inv x = x
-- inverses are unique
-- All idempotents commute
-- All idempotents have the from y = x <> inv x for some x
inv :: g -> g
(~~) :: g -> g -> g
pow :: Integral n => g -> n -> g
-- For -XDerivingVia
newtype ViaGroup g = ViaGroup g
instance Group g => InverseSemigroup (ViaGroup g)Option 2 - Write Patch instances using monoid-subclasses instead
monoid-subclasses has class (Commutative m, LeftReductive m, RightReductive m) => Reductive m (and similar for Cancellative), and they may get you what you want. Some thoughts:
Reductiveprovides an operator(</>) :: Reductive m => m -> m -> Maybe m;Cancellativeadds two additional laws to(</>):(a <> b) </> a == Just b(a <> b) </> b == Just a
- You can't recover an inversion operation from
Cancellativealone, as you can't be certain ofisJust (mempty </> x). (Considerinstance Cancellative Natural.)- Every finite cancellative monoid is a group, but this might not be useful.
- Instance
Cancellative m => Cancellative (MonoidalMap k m)smells like it would be lawful:instance (Ord k, Commutative m) => Commutative (MonoidalMap k m) instance (Ord k, LeftReductive m) => LeftReductive (MonoidalMap k m) where stripPrefix (MonoidalMap prefix) (MonoidalMap m) = MonoidalMap <$> mergeA (traverseMissing $ \_ _ -> Nothing) (traverseMissing $ const pure) (zipWithAMatched $ \_ pf v -> stripPrefix pf v) prefix m instance (Ord k, RightReductive m) => RightReductive (MonoidalMap k m) where stripSuffix (MonoidalMap suffix) (MonoidalMap m) = MonoidalMap <$> mergeA (traverseMissing $ \_ _ -> Nothing) (traverseMissing $ const pure) (zipWithAMatched $ \_ sf v -> stripSuffix sf v) suffix m instance (Ord k, Reductive m) => Reductive (MonoidalMap k m) where MonoidalMap x </> MonoidalMap y = MonoidalMap <$> mergeA (traverseMissing $ \_ _ -> Nothing) (traverseMissing $ \_ _ -> Nothing) (zipWithAMatched $ const (</>)) x y instance (Ord k, LeftCancellative m) => LeftCancellative (MonoidalMap k m) instance (Ord k, RightCancellative m) => RightCancellative (MonoidalMap k m) instance (Ord k, CancellativeMonoid m) => Cancellative (MonoidalMap k m)
- This may be enough for your uses of
patch- instead of computing the inverse of a patch, instead attempt to unapply it directly? - If you need to send data structures across a network boundary, you could do this using
[Either m m], like the free group infree-algebras. - If that's not enough, then I think you probably need to build your
patch-using stuff atop a newInverseSemigroupclass. - I'm very interested to hear what you end up doing here, and if you do make a minimal package providing
class Semigroup m => Commutative m, let me know so I can help PRmonoid-subclasses,monoidal-containers, etc.
Activity
endgame commentedon Jan 6, 2022
CC @cgibbard @Ericson2314
Ericson2314 commentedon Jan 6, 2022
A bit off-topic, but @endgame you might be interested in https://github.com/obsidiansystems/finite-support-normal-form/blob/main/src/Data/Map/Total/FSNF.hs . It's a bit heavyweight --- using a type family like that is definitely a smell --- but by making it impossible to have a
memptyin the values it avoids a lot of trouble.Note that it requires https://github.com/mstksg/nonempty-containers/pull/13/files to build.
endgame commentedon Jan 6, 2022
That is indeed heavyweight, but I think that if you want to ban
memptys from your map, you have to use a newtype of some sort - normalising after applying(<>)is not enough. I suspectnewtype Normalized c = Normalized cwith the following instances would probably be lawful, and may be a better option than adding another niche, law-only algebra class:Here, "normalisation" means "there are no
memptyvalues" in the map. Though such a structure (even if you usenewtype NormalizedMap k v = ...) cannot be aFunctor, asmap g . map f /= map (g . f)iffreturnsmempty.endgame commentedon Jul 11, 2022
Thinking about this again: would putting
Groupinstances onDefaultable.Map.Generalized.Defaultable Mapgive us what we want? Holding that default value ofmemptymeans it's no longer possible to distinguishk |-> memptyfrom "kis not present", which was the edge case that broke theGroupinstance.Though you lose
MonoidNull/DecidablyEmpty, and risk carrying around a bunch of junkk |-> memptyelements; there should probably be some way to aggressively prune "present-but-default" values from a defaultable map when you know thatEqis cheap.Ericson2314 commentedon Jul 11, 2022
Since nothing in
patchitself actually uses this, it is very hard to make a decision. I think we should just delete the instance, see what breaks, and deal with it downstream.endgame commentedon Jul 11, 2022
The main downstream thing I'm aware of is
QueryTinreflex: https://hackage.haskell.org/package/reflex-0.8.2.1/docs/Reflex-Query-Base.html#t:QueryTjonathanknowles commentedon Mar 9, 2023
@endgame Not sure if you're still interested in this topic (as the last comment was from some time ago), but I did recently create a similar type to the one you describe:
Data.Total.MonoidMapData/Total/MonoidMap/Internal.hsFeatures:
memptyvalues (i.e., there should be nomemptyvalues in the internal data structure).monoid-subclasseslibrary.Groupinstance.MonoidNullfor internal comparisons withmempty, to avoid requiring anEqinstance.I haven't released it to hackage yet, as I'm still reviewing the API.
Not sure if you're interested in taking a look, but I'd be very interested to hear any feedback that you might have!
All the best
Jonathan
endgame commentedon Mar 9, 2023
Very cool, thanks for pinging me. I think that in many cases I want a non-total map with a valid grouplike structure, so I think eventually I might write a package that provides
class Monoid g => InverseSemigroup g, along with aderiving viahelpernewtype ViaGroup g;instance Group g => InverseSemigroup (ViaGroup g). But for total monoidal maps, your code looks pretty nice.I probably won't have time to go over it thoroughly, but it's good that you're exploring this design space. You might also want to look at packages
defaultable-mapandtotal-map— since your map is total, you can probably do what they do to get at least anApplicativeinstance. If you want to ask me specific questions, file an issue against your package and @-me there so we don't derail this thread?jonathanknowles commentedon Mar 9, 2023
No worries. Thanks for looking!
Definitely -- these are two packages whose design I'm studying very carefully.
I'm not sure it's possible to build a lawful
Applicativeinstance, at least with the current design, asmemptyvalues are never included in the internal data structure, and AFAIK this restriction prevents a lawfulFunctorinstance. But I'm open to suggestions! (I believe the solution used bytotal-mapis to never cullmemptyvalues unless thetrimoperation is called, which allows it to satisfy the composition law.)Sure, thanks for looking! I'll try to avoid creating further noise in this thread. 😄