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 mappend
s.
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.
- Is it ever okay to use these types of non-lawful instances to build up an AST?
- Are there any specific problems that need to be watched for when using these types of non-lawful instances?
- Is there an alternative way to write code that uses something like
Foo
? Some way to enable interpretation of a monoidal structure instead of usingmappend
on a type directly?