0

I'm trying to represent terms like the following:

a0 <> a1 <> ... <> an-1

Where ai must be element of a commutative Semigroup. For this one can choose a data representation like the following:

newtype SemigroupPolynomial a = SP Map a Integer

where the map contains the different terms of the polynomial and its count.

In this way, we can represent the sum

3 + 3 + 6

as (assuming OverloadedLists):

SP [(3, 2), (6, 1)]

But also we can represent terms like:

3 * 3 * 6

The SemigroupPolynomial could be an instance of Semigroup:

instance ??? a => Semigroup (SemigroupPolynomial a) where
    (MP p0) <> (MP p1) = 
        MP $ Map.filter (0/=) $ Map.unionWith (+) p0 p1

No the question is which constraints do I have to put in ??? so that:

  1. The <> operation is commutative and associative.
  2. It can be used to represent sums and products, as exemplified above.

A similar question on how to represent commutative Monoids was already asked here. However it seems that the constraint (Abelian m, Monoidal m) might be too strong (I don't require a zero element), and it will prevent me to use this to represent products.

Damian Nadales
  • 4,907
  • 1
  • 21
  • 34
  • You don't really need the instance to know anything about commutativity, do you? Sounds like a law-thing that would normally just be expressed in documentation. (I know, this isn't elegant, but...) What you do need is the much uglier `Ord` constraint in oder to use `Map`. – leftaroundabout Sep 21 '17 at 11:08
  • By representing a sum like `2 + 6 + 2` as a map I'm loosing information about the order of the terms, so I do need the commutativity constraint I think. Maybe not in the `Semigroup` instance, but somewhere I would think... – Damian Nadales Sep 21 '17 at 11:10
  • 2
    Well, you basically just act _as if_ it's commutative. Whether the key type really _is_ commutative is another question. I agree that in principle this _should_ be expressed by a dedicated class (basically an empty subclass of `Semigroup` that doesn't add any methods, only a law), but I'm not sure if that's practical. – leftaroundabout Sep 21 '17 at 11:16

2 Answers2

1

As @leftroundabout has commented, you don't need a constraint here. Don't be fooled by the word "constraint". In Haskell, the primary purpose of constraints isn't to constrain the behavior of a particular type or operation in some manner. Rather, it's to constrain the set of types that a function will accept to those types that support a set of operations.

When I write:

fmapTwice :: (Functor f) => (a -> a) -> f a -> f a
fmapTwice f = fmap (f . f)

I'm not really constraining the type f to act like a functor and obey the rules required of functors. Rather, I'm constraining the fmapTwice function to only apply to types f that support the fmap operation.

Nothing stops some jerk from writing:

data Foo a = Foo a | NoFoo deriving (Show)
instance Functor Foo where
    fmap _ _ = NoFoo   -- invalid functor violates:  fmap id = id

and applying my function to this invalid functor:

> fmapTwice (*2) (Foo 10)
NoFoo
>

Haskell relies on programmer discipline to ensure that something declared as having a Functor instance is a well behaved functor.

In your example, the instance:

import Data.Semigroup
import qualified Data.Map as Map
import Data.Map.Strict (Map)

data SemigroupPolynomial a = SP (Map a Integer) deriving (Show)
instance (Ord a) => Semigroup (SemigroupPolynomial a) where
    (SP p0) <> (SP p1) = 
        SP $ Map.filter (0/=) $ Map.unionWith (+) p0 p1

doesn't require any constraints other than Ord a, to ensure that a can be used as a Map key.

Now, it's up to you to make sure you only use your SemigroupPolynomial to represent commutative operations:

foldSP :: (a -> a -> a) -> SemigroupPolynomial a -> a
foldSP f (SP m) = foldr1 f $ concatMap (\(a, n) -> replicate (fromIntegral n) a)
                                       (Map.assocs m)

main = do let sp = singleton 3 <> singleton 3 <> singleton 6
          print sp
          print $ foldSP (*) sp
          print $ foldSP (+) sp
          print $ foldSP (-) sp   -- wrong, but it's your own damn fault

If you want to somehow introduce a requirement of commutativity into your data type, one way of doing it (that doesn't involve Haskell "constraints" at all) is to write something like:

data CommutativeOp a = CO (a -> a -> a)
foldSP :: CommutativeOp a -> SemigroupPolynomial a -> a
foldSP (CO f) (SP m) = <same as above>

Now, as long as you realize that when you write:

plusOp = CO (+)
timesOp = CO (*)

you are making a declaration that (+) and (*) are commutative operations, this will ensure that foldSP is only applied to such operations:

main = do let sp = singleton 3 <> singleton 3 <> singleton 6
          print $ foldSP plusOp sp
          print $ foldSP timesOp sp

If you want to somehow introduce a commutativity constraint on the type a to ensure that SemigroupPolynomial a is a valid representation, then you can't do this for a equal to Int, obviously, since it depends on which binary operation Int -> Int -> Int is used for the fold.

Instead, you need to embed the operation into the type, perhaps using newtypes that represent the operation, like Sum or Product in Data.Semigroup. Then, you can introduce a type class (with no operations) to represent the commutativity constraint:

class Commutative a
instance Commutative (Sum a)
instance Commutative (Product a)
instance (Ord a, Commutative b) => SemigroupPolynomial b where
    ...definition on (<>) as above...

and now the fold operation would use the operation implicit in the newtype (here, just using the monoid instance):

foldSP' :: (Monoid a) => SemigroupPolynomial a -> a
foldSP' (SP m) = mconcat $ concatMap (\(a, n) -> replicate (fromIntegral n) a)
                                     (Map.assocs m)

Maybe this is what you wanted. If so, the full example looks like this:

import Data.Semigroup
import qualified Data.Map as Map
import Data.Map.Strict (Map)

newtype SemigroupPolynomial a = SP (Map a Integer) deriving (Show)

class Commutative a
instance Commutative (Sum a)
instance Commutative (Product a)
instance (Ord a, Commutative a) => Semigroup (SemigroupPolynomial a) where
    (SP p0) <> (SP p1) = 
        SP $ Map.filter (0/=) $ Map.unionWith (+) p0 p1

singleton :: a -> SemigroupPolynomial a
singleton x = SP $ Map.singleton x 1

foldSP' :: (Monoid a) => SemigroupPolynomial a -> a
foldSP' (SP m) = mconcat $ concatMap (\(a, n) -> replicate (fromIntegral n) a)
                                     (Map.assocs m)

main = do let sp1 = singleton (Sum 3) <> singleton (Sum 3) <> singleton (Sum 6)
          print sp1
          print (foldSP' sp1)
          let sp2 = singleton (Product 3) <> singleton (Product 3) 
                          <> singleton (Product 6)
          print sp2
          print (foldSP' sp2)
K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
1

What you've constructed there is a free monoid, which is in turn just a list. You could probably use [(a, Integer)] rather than a map and it would be fine for some use cases. Since Haskell doesn't have dependent types, I doubt there will be anything nicer in terms of types.

No the question is which constraints do I have to put in ??? so that:

The <> operation is commutative and associative.

It can be used to represent sums and products, as exemplified above.

You should probably create a Ring typeclass, or use that of numeric-prelude. That will let users know your data type is associative, and has addition and multiplication. There's not any great type-level way to indicate these to my knowledge. You could use a property test to make sure that everything worked via hspec or a similar library.

However it seems that the constraint (Abelian m, Monoidal m) might be too strong (I don't require a zero element), and it will prevent me to use this to represent products.

There's no algebraic structure associated with polynomials "over" a semigroup, so you may have to implement that yourself. Since you say that <> is commutative, your elements will satisfy Abelian m. It's just a question of adding a unit.

Community
  • 1
  • 1