9

I've seen a data type defined like the following with a corresponding Monoid instance:

data Foo where
  FooEmpty :: String -> Foo
  FooAppend :: Foo -> Foo -> Foo

-- | Create a 'Foo' with a specific 'String'.
foo :: String -> Foo
foo = FooEmpty

instance Monoid Foo where
  mempty :: Foo
  mempty = FooEmpty ""

  mappend :: Foo -> Foo -> Foo
  mappend = FooAppend

You can find the full code in a gist on Github.

This is how Foo can be used:

exampleFoo :: Foo
exampleFoo =
  (foo "hello" <> foo " reallylongstringthatislong") <>
  (foo " world" <> mempty)

exampleFoo ends up as a tree that looks like this:

FooAppend
  (FooAppend
    (FooEmpty "hello")
    (FooEmpty " reallylongstringthatislong"))
  (FooAppend
    (FooEmpty " world")
    (FooEmpty ""))

Foo can be used to turn sequences of Monoid operations (mempty and mappend) into an AST. This AST can then be interpreted into some other Monoid.

For instance, here is a translation of Foo into a String that makes sure the string appends will happen optimally:

fooInterp :: Foo -> String
fooInterp = go ""
  where
    go :: String -> Foo -> String
    go accum (FooEmpty str) = str ++ accum
    go accum (FooAppend foo1 foo2) = go (go accum foo2) foo1

This is really nice. It is convenient that we can be sure String appends will happen in the right order. We don't have to worry about left-associated mappends.

However, the one thing that worries me is that the Monoid instance for Foo is not a legal Monoid instance.

For instance, take the first Monoid law:

mappend mempty x = x

If we let x be FooEmpty "hello", we get the following:

mappend mempty (FooEmpty "hello") = FooEmpty "hello"
mappend (FooEmpty "") (FooEmpty "hello") = FooEmpty "hello"  -- replace mempty with its def
FooAppend (FooEmpty "") (FooEmpty "hello") = FooEmpty "hello" -- replace mappend with its def

You can see that FooAppend (FooEmpty "") (FooEmpty "hello") does not equal FooEmpty "hello". The other Monoid laws also don't hold for similar reasons.

Haskellers are usually against non-lawful instances. But I feel like this is a special case. We are just trying to build up a structure that can be interpreted into another Monoid. In the case of Foo, we can make sure that the Monoid laws hold for String in the fooInterp function.

  1. Is it ever okay to use these types of non-lawful instances to build up an AST?
  2. Are there any specific problems that need to be watched for when using these types of non-lawful instances?
  3. Is there an alternative way to write code that uses something like Foo? Some way to enable interpretation of a monoidal structure instead of using mappend on a type directly?
illabout
  • 3,517
  • 1
  • 18
  • 39
  • 1
    As an aside, it is also possible to generalize the `Foo` type [for any](https://gist.github.com/cdepillabout/b49b640dcf8d2066260a4dbe42f56000#file-example-hs-L45) `Monoid`. It is also possible to write a type like `Foo` for *any* [typeclass](https://gist.github.com/cdepillabout/b49b640dcf8d2066260a4dbe42f56000#file-example-hs-L76). – illabout Aug 25 '17 at 15:31
  • 1
    Not really an answer to your question as a whole, but if you wanted `O(1)` list appends (which apply to strings) in a `Monoid`, check out [`DList`](https://hackage.haskell.org/package/dlist-0.8.0.3/docs/Data-DList.html#t:DList). – Alec Aug 25 '17 at 16:32
  • 1
    You *can* make `Foo` law abiding by adding another constructor: `data Foo = FooEmpty | FooString String | FooAppend Foo Foo`, then `foo :: String -> Foo; foo = FooString` and `instance Monoid Foo where { mempty = FooEmpty; mappend FooEmpty m = m ; mappend m FooEmpty = m ; mappend m m' = FooAppend m m' }`. If you didn't want to add another constructor, you could pull the same trick by comparing inputs to `mappend` to `FooEmpty ""`, but your semantics are a little different. – rampion Aug 25 '17 at 17:13
  • 4
    @rampion Doesn't that instance violate associativity, anyway? – chi Aug 25 '17 at 18:38
  • 1
    chi: You're right, it does. – rampion Aug 25 '17 at 19:03

3 Answers3

15

Quoting this answer on a similar question:

You can think of it from this alternative point of view: the law (a <> b) <> c = a <> (b <> c) doesn't specify which equality should be used, i.e. what specific relation the = denotes. It is natural to think of it in terms of structural equality, but note that very few typeclass laws actually hold up to structural equality (e.g. try proving fmap id = id for [] as opposed to forall x . fmap id x = id x).

For example, it's mostly fine if you do not export the constructors of Foo, and only export functions that, from the point of view of users, behave as if Foo were a monoid. But most of the time it is possible to come up with a representation that's structurally a monoid, good enough in practice, though maybe not as general (below, you cannot reassociate arbitrarily after the fact, because interpretation is mixed with construction).

type Foo = Endo String

foo :: String -> Foo
foo s = Endo (s <>)

unFoo :: Foo -> String
unFoo (Endo f) = f ""

(Data.Monoid.Endo)


Here is another SO question where a non-structural structure (Alternative) is considered at first.

Li-yao Xia
  • 31,896
  • 2
  • 33
  • 56
  • The comment you quoted says "Try proving fmap id = id for [] as opposed to forall x . fmap id x = id x". I don't quite understand what difference they are implying between `fmap id` and `forall x. fmap id x`. Aren't they the same thing? Are the proofs for these two equalities different? – illabout Aug 25 '17 at 17:41
  • Also, I'm not 100% sure what you're saying with the `Endo` example. Maybe you're saying that unlike my `Foo` type, `Endo String` **is** a `Monoid`. It will also give us optimal appends. However, reassociating after the fact will not work. So maybe `Endo String` should be used instead of my `Foo` type? – illabout Aug 25 '17 at 17:46
  • 2
    illabout: It comes down how you define equivalence between functions. `fmap id [1...n] == id [1..n]`, but it takes O(n) time to run the `fmap id` on the left hand side and O(1) time to run the `id` on the right hand side. The functions produce the same output, but with different complexity so they must be different functions. But we still say `fmap id == id` because we define equivalence of functions as equality of outputs for the law. – rampion Aug 25 '17 at 17:50
  • 1
    I think that the `fmap id` example is confusing. In the linked question, the issue was Double rounding errors, which break some laws if interpreted strictly. Here we do not have rounding errors to consider. I also see no difference between `fmap id=id` and its extensional variant `forall x. forall id x = id x`, so I'm unsure why this is mentioned. (Eta expansion holds on non-bottom function values, in Haskell.) – chi Aug 25 '17 at 18:46
  • 1
    @chi I think we can prove `forall Eq x . fmap id x == id x` where `x` is an instance of `Eq` and using its `==` equality, but we cannot prove `fmap id ≡ id` with `Eq` since there is no function instance. So we have to choose ourselves what `≡` means. We can choose our own equality for `Foo` as well - like ``instance Eq Foo where (==) = (==) `on` fooInterp`` – Bergi Aug 25 '17 at 22:40
  • 1
    @Bergi I usually take such equality to mean denotational equality, not Eq's one. Possibly ignoring some bottoms. But I agree that "equality" in these laws is quite unspecified. – chi Aug 25 '17 at 22:42
  • 1
    @chi Yes, exactly that's the point of this answer. What are the denotational semantics of `Foo`? Structural equality doesn't fit us so we don't use it here. – Bergi Aug 25 '17 at 22:44
9

This will come up for most non-trivial data structures. The only exceptions I can think of off the top of my head are (some) trie-like structures.

  1. Balanced tree data structures allow multiple balancings of most values. This is true of AVL trees, red-black trees, B-trees, 2-3 finger trees, etc.

  2. Data structures designed around "rebuilding", such as Hood-Melville queues, allow variable amounts of duplication within structures representing most values.

  3. Data structures implementing efficient priority queues allow multiple arrangements of elements.

  4. Hash tables will arrange elements differently depending on when collisions occur.

None of these structures can be asymptotically as efficient without this flexibility. The flexibility, however, always breaks laws under the strictest interpretation. In Haskell, the only good way to deal with this is by using the module system to make sure no one can detect the problem. In experimental dependently typed languages, researchers have been working on things like observational type theory and homotopy type theory to find better ways to talk about "equality", but that research is pretty far from becoming practical.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
  • 1
    Yeah, but none of this is relevant for a `Monoid` instance like the OP's. A tree structure that does balancing or something would most certainly _not_ just wrap `mappend` always in the same constructor, as that would easily lead to totally degenerate structure. The only reason to implement such a constructor is to make the exact associativity of expressions visible, but that's exactly what must not be possible according to the monoid rules. A structure that does _not_ care for associativity should just have a constructor like `MConcat :: Seq Foo -> Foo`. – leftaroundabout Aug 25 '17 at 18:41
  • @leftaroundabout, the `Monoid` instance will look different, but it also won't be (strictly) associative. Isn't that the real point? – dfeuer Aug 25 '17 at 18:43
  • @leftaroundabout, I don't understand what you're getting at. – dfeuer Aug 25 '17 at 18:58
  • 1
    In ["Free Monoids In Haskell"](http://comonad.com/reader/2015/free-monoids-in-haskell/) Dan Doel touches upon this when he talks about mimicking quotients in Haskell by hiding implementation details within a module. – rampion Aug 25 '17 at 19:14
  • 1
    @dfeuer I'm saying, these types you mention are not designed to in any way reflect the _way the values were built_ at all. On the contrary, they smartly restructure in such a way that _no matter_ how the value was defined, you'll get an efficient representation. This is in great contrast to the OP's example, which exactly replicates the `mappend` associativity in its ADT tree, even if this associativity is degenerate in list-form. – leftaroundabout Aug 26 '17 at 09:09
  • @leftaroundabout What do you mean by the associativity in my example being "degenerate in list-form"? What does "degenerate" mean in this context? (Also, what do you mean by "list-form"? I was under the impression that my example is creating an AST for `mappend`, which is different from Haskell's list type and the `Monoid` instance for list.) – illabout Aug 30 '17 at 17:26
  • 1
    @illabout by “degenerate in list-form” I mean expressions of the form `a <> (b <> (c <> ... (p <> (q <> r))...))`. If you construct that with `FooAppent`, the structure will be _O_ (_n_) deep, whereas the tree structures that David refers to always strive to keep a depth of only _O_ (log _n_). – leftaroundabout Aug 30 '17 at 18:39
8

Is it ever okay to use these types of non-lawful instances to build up an AST?

This is a matter of opinion. (I'm firmly in the 'never ok' camp.)

Are there any specific problems that need to be watched for when using these types of non-lawful instances?

  • cognitive burden placed on potential users and future maintainers
  • potential bugs because we use the type in a place that makes assumptions based on the broken law(s)

edit to answer questions in comments:

Would you be able to come up with specific examples of how it raises the cognitive burden on users?

Imagine how annoyed you would be if someone did this in C:

 // limit all while loops to 10 iterations
 #define while(exp) for(int i = 0; (exp) && i < 10; ++i)

Now we have to keep track of the scope of this pseudo-while definition and its implications. It's a non-Haskell example, but I think the principle is the same. We shouldn't expect the semantics of while to be different in a particular source file just like we shouldn't expect the semantics of Monoid to be different for a particular data type.

When we say something is an X, then it should be a X because people understand the semantics of X. The principle here is don't create exceptions to well understood concepts.

I think the point of using lawful abstractions (like monoid) in the first place is to alleviate the need for programmers to learn and remember a myriad of different semantics. Thus, every exception we create undermines this goal. In fact, it makes it worse; we have to remember the abstraction and on top of that remember all the exceptions. (As an aside, I admire but pity those who learned English as a second language.)

Or how it can lead to potential bugs?

some library:

-- instances of this class must have property P
class AbidesByP where
   ...

-- foo relies on the property P
foo :: AbidesByP a => a -> Result
foo a = ... 

my code:

data MyData = ...

-- note: AbidesByP's are suppose to have property P, but this one doesn't
instance AbidesByP MyData where
   ...

some other programmer (or me in a few months):

doSomethingWithMyData :: MyData -> SomeResult
doSomethingWithMyData x = let ...
                              ...
                              ...
                              r = foo x      -- potential bug
                              ...
                              ...
                           in ...

Is there an alternative way to write code that uses something like Foo?

I'd probably just use the constructor to contruct:

(foo "hello" `FooAppend` foo " reallylongstringthatislong") `FooAppend` (foo " world" `FooAppend` foo "")

or make an operator:

(<++>) = FooAppend

(foo "hello" <++> foo " reallylongstringthatislong") <++> (foo " world" <++> foo "")
user2297560
  • 2,953
  • 1
  • 14
  • 11
  • Thanks a lot for this answer. When writing the question, I really felt myself falling into the "eh, it's okay this one time" camp. So I really appreciate the opposite view. Would you be able to come up with specific examples of how it raises the cognitive burden on users? Or how it can lead to potential bugs? I know it is really tough to come up with specific examples off the top of your head to a somewhat abstract question, but I really want to make sure I have a good idea of the downsides I'd be dealing with. – illabout Aug 25 '17 at 17:34
  • Also, I just added a third question: "Is there an alternative way to write code that uses something like Foo? Some way to enable interpretation of a monoidal structure instead of using mappend on a type directly?" I'm interested in your answer to this question. – illabout Aug 25 '17 at 17:48
  • Thanks for expanding your answer. Unlike the other two answers, this answer really answered all of my questions. However, I'm not quite satisfied with the "How it can lead to potential bugs?" part. I agree with you that most of the time it is not good to write non-law-abiding instances for the reasons you give. And my `Foo` type is not law-abiding if you're using structural equality. However, I think @li-yao-xia brings up a good point that it is "good enough" as long as we use a module system to force the user to use `fooInterp`. – illabout Aug 26 '17 at 03:40
  • However, you would be able to win me over if you could come up with a way that `Foo` and `fooInterp` could lead to potential bugs or increased cognitive overhead. – illabout Aug 26 '17 at 03:41
  • 1
    @illabout Actually, I agree. Encapsulating the implementation details and providing only a law-abiding interface is completely fine. – user2297560 Aug 26 '17 at 11:51