
How does compiler can infer a type for "allEven = foldMap even" ?
Something like
allEven :: (Monoid (aspect Bool), Integral a) => [a] -> Bool ?
Should all Bool's in function body have the same aspects?
2017-05-06 19:42 GMT+03:00 MarLinn
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
_______________________________________________ Haskell-Cafe mailing list To (un)subscribe, modify options or view archives go to: http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe Only members subscribed via the mailman list are allowed to post.