Skip to content

Generalise EqM/OrdM to decidable monoids - #1258

Open
alexfmpe wants to merge 2 commits into
haskell:masterfrom
alexfmpe:short-circuit
Open

alexfmpe wants to merge 2 commits into
haskell:masterfrom
alexfmpe:short-circuit

Conversation

@alexfmpe

Copy link
Copy Markdown
Contributor

Name is tentative

Set 
  eq:                           OK
    32.6 μs ± 2.1 μs, 128 KB allocated,  29 B  copied,  44 MB peak memory,       same as baseline
  compare:                      OK
    32.3 μs ± 1.9 μs, 128 KB allocated,  28 B  copied,  44 MB peak memory,       same as baseline


All
  eq:      OK
    167  μs ±  16 μs, 640 KB allocated,  60 B  copied,  21 MB peak memory,       same as baseline
  compare: OK
    167  μs ±  13 μs, 640 KB allocated,  58 B  copied,  21 MB peak memory,       same as baseline

r@(e :*: x') -> if e then runEqM g x' else r
instance (Eq m, Monoid m) => Semigroup (ShortCircuit m a) where
f <> g = ShortCircuit $ \x -> case runShortCircuit f x of
r@(e :*: x') -> if e == mempty then runShortCircuit g x' else r

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 assume e == All True will always optimize to if e

@meooow25

meooow25 commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

I'm not sure about this. EqM and OrdM are easy to understand on their own, and I find that this change makes it harder to understand. The relationship with monoids is not reflected in the code at all: it doesn't use <> and there is no explanation for why it should test against mempty.
It would be nice if it were generalizing to some well-known structure, but that's not the case either. I can't find anything on "decidable monoids" with a Google search.


If I think of generalizing this, I would proceed a bit differently.

First, we need the property that <> returns one of its arguments (I don't know what this is called either).

data Which = L | R
instance Semigroup a => ChoosesOne a where
  -- Law: (x <> y) == case sappendWhich x y of { L -> x; R -> y }
  sappendWhich :: a -> a -> Which

Then we can have

choosePair :: ChoosesOne a => StrictPair s a -> StrictPair s a -> StrictPair s a
choosePair p1@(_ :*: x1) p2(_ :*: x2) = case sappendWhich x1 x2 of { L -> p1; R -> p2 }

newtype ChooseWithState s a = CWS (s -> StrictPair s a)

instance ChoosesOne a => Semigroup (ChooseWithState s a) where
  CWS f1 <> CWS f2 = CWS $ \s0 ->
    let p1@(_ :*: s1) = f1 s0
    in choosePair p1 (f2 s1)

With another property that the semigroup has left-zeros, we can optimize it to the current implementation.

instance Semigroup a => HasLeftZeros a where
  -- Law: isLeftZero x <=> x <> y == x
  isLeftZero :: a -> Bool

newtype ShortCircuitChooseWithState s a = SCCWS (s -> StrictPair s a)

instance (ChoosesOne a, HasLeftZeros a) => Semigroup (ShortCircuitChooseWithState s a) where
  SCCWS f1 <> SCCWS f2 = SCCWS $ \s0 ->
    let p1@(x1 :*: s1) = f1 s0
    in if isLeftZero x1
       then p1
       else f2 s1

All this is to say that I find it convoluted and don't see the benefit in trying to generalize it.

@alexfmpe

alexfmpe commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor Author

The relationship with monoids is not reflected in the code at all: it doesn't use <> and there is no explanation for why it should test against mempty.

First, we need the property that <> returns one of its arguments (I don't know what this is called either).

With another property that the semigroup has left-zeros

All good points, this was half-baked since I mostly mechanically de-duplicated. Let me try to make this more precise.

The whole point is to be able to do short-circuit, which means we can't always apply <>, but for All/Ordering it turns out that we always short-circuit, either via identity or left-absorbing element, so <> ends up never being called and is merely needed for the left-zero law. ChoosesOne is stronger than we need. In particular it also means that forall x. x <> x = x, whereas short-circuiting could happen also on a product as soon as a 0 was seen. Num Integer seems to do this for (*)

That is, for All/Ordering we have isLeftZero = (/= mempty) but any isLeftZero would do:

first f (a :*: b) = f a :*: b

instance HasLeftZeros a => Monoid (ShortCircuit k v) where
  ShortCircuit f <> ShortCircuit g = ShortCircuit $ \x -> case f x of
    r@(m :*: x') -> if isLeftZero m then r else first (m <>) (g x') 

All this is to say that I find it convoluted and don't see the benefit in trying to generalize it.

I wouldn't say convoluted, but agree with not trying to generalize at this point since we have no instances of partial shortcircuit that actually uses <>.

It would be nice if it were generalizing to some well-known structure, but that's not the case either. I can't find anything on "decidable monoids" with a Google search.

I don't think it's what we want here after all, but FWIW there's https://hackage.haskell.org/package/patch-0.0.8.4/docs/Data-Monoid-DecidablyEmpty.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants