purescriptabstract-algebra

Why is the `Data.Int` type not a `Semigroup` in PureScript but `String` is?


> "lo" <> "fa"
"lofa"

> 1 <> 2
Error found:
in module $PSCI
at :1:1 - 1:7 (line 1, column 1 - line 1, column 7)

  No type class instance was found for
                                
    Data.Semigroup.Semigroup Int

Is this for purely technical reasons as there would have to be at least 2 Semigroup instances (for addition and multiplication), but type classes cannot be implemented multiple times for the same type?

Even if it could be done using instance chaining, one would have to create an instance of the entire "stack" of algebraic abstractions (Monoid, Group, Commutative) for each operation, right?

In contrast, Semiring is conveniently defined as a set with 2 binary operations1, which fits the bill perfectly:

A semiring (S, +, ) is a set S with two binary compositions such that (S, +) and (S, ) are semigroups and distributes over +.
source, pdf

So, technically, Int is a semigroup under addition (a commutative monoid even), but PureScript is not capable to "infer" that Semigroup's append (<>) and Semiring's add (+), in effect, do the same thing, but, for obvious reasons, cannot be used interchangeably.2

Then again, I'm quite new to pure functional programming, PureScript, and abstract algebra, so my statements above may not make much sense - and thanks in advance for dispelling my confusion!


[1]: Although, nLab states that "mathematicians disagree on the definition of a semiring"...

[2]: Are there programming languages where this level of granularity is achieved? (That is, provided if this is even a thing and if I didn't totally misinterpret these mathematical ideas.)


Solution

  • Would you expect 1 <> 2 to evaluate to 3 or to 2? Why?

    I must say, I'm not sure I understood your elaboration correctly: in one place you are assuming that the Semiring Int instance would be implemented in terms of addition, but in another place you admit that there are two possible ways to implement it - addition and multiplication.

    But it's not, as you say, "purely technical reasons". The ambiguity is fundamental. Assuming there were no technical reasons of any kind and you could implement whatever you wanted, what should 1 <> 2 evaluate to? Should it be 3 or 2?

    There is no right answer. There isn't even an answer that would be "reasonable most of the time", like with strings. Whether you want addition or multiplication depends on the context.

    And that is why there is no one standard implementation to rule them all. Sure, the standard library could just pick one, but then it would lead to subtle bugs for people who assumed the other way, or, at best, to angry questions and complaints.


    But there is a way to pick one or the other explicitly. Meet newtype Additive and its evil twin Multiplicative. They would wrap your Int (or any other Semiring actually) and provide a Semigroup instance in additive or multiplicative sense respectively:

    > import Data.Monoid.Additive
    > Additive 1 <> Additive 2
    (Additive 3)
    
    > import Data.Monoid.Multiplicative
    > Multiplicative 1 <> Multiplicative 2
    (Multiplicative 2)
    

    And you can, of course, use that in any context where Monoid-ish classes are expected:

    > fold (Additive <$> [1,2,3,4])
    (Additive 10)
    
    > fold (Multiplicative <$> [1,2,3,4])
    (Multiplicative 24)
    

    And while we're on the subject, you might be interested in some other similar newtypes from the Data.Monoid.* namespaces, such as Conj and Disj, solving a similar problem for booleans, Dual, which reverses direction, or Endo for composing functions.

    And a special shout out for Down, working tirelessly to enable reverse sorting.