Hi people,
in the last couple days this list has once again seen examples of
how our class system is not perfect yet. Here are some of the
problems we face:
- Useful, but confusing instances like Foldable ((,) a)
- Alternative possible instances like Alternative []
- Orphan instances
- Reliance on the order of arguments makes some instances
impossible, for example Traversable (,a)
How we usually resolve some of such issues is with newtype.
Among the drawbacks are
- This clutters code with artificial wrappers and unwrappers
that have nothing to do with the task at hand
- It implies two levels of hierarchy by marking the one instance
without a newtype as special
- Every type needs its own wrapper. E.g. a Foldable (,a)
(if possible) would need a different wrapper from a Foldable
(a,,c)
- Definitions are scattered at unexpected places, like All
and Any, partly to avoid orphan instances.
After some thought I therefore propose a language extension I
call "aspects". Keep in mind that this is a very rough draft just
to gauge your reaction.
The core change would be the introduction of a keyword "aspect"
that would work in a comparable way to the keyword "module".
In other words you could say
aspect Data.Aspect.ChooseNonEmpty where
import qualified Data.Set as Set
instance Alternative [] where
empty = []
a <|> b = if null a then b else a
instance Alternative Set.Set where …
empty = Set.empty
a <|> b = if null a then b else a
Changes compared to a normal module would be:
- An aspect can only contain instances
- An aspect can import anything, but exports only instances
- An aspect will never produce orphan instance warnings (duh.)
- An aspect can be a file level definition, but it can also be
contained in a module (we'll see why that is useful)
You also wouldn't import an aspect like a normal module, but with
a second syntax extension:
import Data.List under (Data.Aspect.ChooseNonEmpty)
import qualified Data.Set as Set hiding (Set)
import qualified Data.Set under (Default, Data.Aspect.ChooseNonEmpty) as CNE (Set)
So you could also import the same structure twice with different
aspects under different names:
import Data.Bool
import qualified Data.Bool under (Data.Aspect.All) as All (Bool)
import qualified Data.Bool under (Data.Aspect.Any) as Any (Bool)
Now, because of the first import, you could use the boolean
functions normally, and even use the normal Bool type in
signatures. And because of the qualified imports if you want to
use one of the Monoid instances, all you would have to
do is change Bool in the type signature to Any.Bool
or All.Bool respectively, like so:
allEven :: (Integral a) => [a] -> Bool
allEven = foldMap even -- error: Could not deduce (Monoid Bool)…
-- old way
allEven :: (Integral a) => [a] -> Bool
allEven = getAll . foldMap (All . even)
-- new way
allEven :: (Integral a) => [a] -> All.Bool -- qualified name adds the monoidal aspect (and possibly others)
allEven = foldMap even -- works
In other words, aspects would only be used for instance lookups.
That is also why you could state several aspects at once when
importing. Conflicts would be solved as usual: All is well until
you try to use class functions that create an ambiguity.
I imagine a few special rules to make backwards compatibility
easier. Most importantly, you could define default aspects in
several ways:
- aspect Default where … --
reserved aspect name
- default aspect ChooseNonEmpty
where … -- re-used reserved keyword,
but also gives a name to the aspect
- default aspect where … -- short form
for default aspect Default where …
- An instance defined in a module outside of an aspect would
automatically be in the Default aspect. In other words
the definition can be seen as a short form of a local extension
to the aspect. That's also why aspects would be allowed to be
part of a module.
If you don't specify an aspect while importing, it would be
imported under the Default aspect. To hide the Default
aspect, just don't add it to the aspect list when importing.
Other random thoughts about this:
- An aspect doesn't need to be complete. E.g. I imagine an
aspect that only defines Alternative.empty, with
several other aspects relying on that by importing this
incomplete aspect. OO programmers might call them abstract
aspects. This might possibly help resolve some disputes about
the "perfect" hierarchy.
- If aspects can be part of a module, can aspects also be part
of an aspect? Maybe, but I haven't made a cost-benefit analysis
yet.
- Aspects seem to form a level of container between definitions
and modules. Maybe there should also be a new container type (or
several) for the other parts of code? Say, a container that can
contain everything but instances.
- There could be an extension to the export syntax to choose
which aspects to export. I just don't see the usefulness right
now.
- There could also be special syntax like import * under
(Default,SpecialAspect) as a short form to add some
aspects to every imported module.
- The Default aspect is obviously extensible. I
consider that a universally useful, if not essential feature. On
the other hand in this proposal aspects use the module name
space – which means such extensions would only be possible on a
package level or by using several code folder roots. I'm not
sure I'm happy with that.
- The proposal doesn't solve the issue that instances rely the
order of arguments. But an introduction of such new syntax might
be a good time to introduce two extensions at once. I imagine
something like instance Foldable (,) _ a where…
The biggest drawbacks from this idea that I can see right now
are:
- The language extension might be infectious: once one library
in a project uses it, many parts of the project might need it.
This is different from most extensions that stay rather local.
- Such a change might necessitate huge amounts of cpp.
- Because aspects would be extensible and would have a global
name space, care would have to be taken to not create a mess.
So… feel free to bikeshed and more importantly criticize! What am
I overlooking? Would you consider such an idea worthwhile? Happy
to hear your thoughts.
Cheers,
MarLinn