1 Generalized, named, and exportable default
declarations¶
The default
declaration as specified in Haskell 2010 report is very limited. This proposal aims to make it useful
while keeping backward compatibility. Another goal of the proposal is to leave the default
declaration in the deep
language background, in a manner of speaking, so that expert users can apply it to help beginners who can remain
blissfully unaware of it.
1.1 Motivation¶
Section 4.3.4 of the Haskell 2010
language report specifies the behaviour of the default
declaration. The specification sharply limits the
declaration, as it applies
only within the current module,
only to the class
Num
.
The first limitation means that every user is forced to repeat the same declaration in every module if they should try to use a non-standard numeric data type in an ambiguous way.
The reason for the latter limitation is that the numeric literals are the only literals with ambiguous types in the
language report. Since then, however, the OverloadedStrings
and OverloadedLists
language extensions have made
more syntactic constructs ambiguous. The former in particular is commonly used for more convenient coding of Text
literals. Another potential source of ambiguity are the Prelude and the core libraries which are slowly evolving to be
more generalized.
1.2 Proposed Change Specification¶
The present proposal is basically an expanded version of the earlier name the class Haskell Prime proposal. It is also a slightly amended version of a proposal for the ill-fated Haskell 2020 language stadardization attempt.
The Haskell 2010 language report specifies the following syntax for the default
declaration:
default
(qtycon1 , … , qtyconn) (n ≥ 0)where each type qtyconi must be an instance of class Num
.
1.2.1 Naming the class¶
In the current language standard, the default
declaration implicitly applies to class Num
only. The proposal is
to make this class explicit, so the syntax becomes
default
qtycls? (qtycon1 , … , qtyconn) (n ≥ 0)where each type qtyconi must be an instance of the specified class qtycls. The types may belong to any kind, but the class must have a single parameter.
If no class is specified, the earlier default of Num
is assumed. In other words, the Haskell ‘98 syntax of
default (Int, Float)
would mean exactly the same as
default Num (Int, Float)
This syntactic extension would be enabled by a new {-# LANGUAGE NamedDefaults #-}
pragma.
1.2.2 Exporting the defaults¶
Another thing the current report specifies is that the declaration applies only within the current module. This
proposal does not modify that behaviour: a default
declaration by itself does not apply outside its module. That
is the purpose of another extension to the module export list. To the existing syntax
module
modiddefault
qtyclsThe effect of the new alternative would be to export the default declaration that is in effect in the module for the named class qtycls. This can mean either that it’s declared in the same module or that it’s imported from another module.
When exporting a default Num
declaration, the class Num
has to be explicitly named like any other class.
An import
of a module always imports all the default
declarations listed in the module’s export list. There is
no way to exclude any of them. This is the default option for this proposal, but there are alternatives.
A module can export its default
declarations either by having no explicit export list (as in module M where
{...}
) or by specifying them explicitly in its export list using the above syntax extension. In particular,
re-export of a whole imported module (as in module M (module N) where{...}
does not export any default
declarations.
The syntactic extension to exports would be enabled by the same {-# LANGUAGE NamedDefaults #-}
pragma. The new
semantics of imports would be enabled by default with no LANGUAGE
extension required.
1.2.3 Subsumption¶
Definition: given two default
declarations for the same class
default
C (Type1a , … , Typema)default
C (Type1b , … , Typenb)
if m ≤ n and the first type sequence Type1a , … , Typema is a sub-sequence of the second sequence Type1b , … , Typenb (i.e., the former can be obtained by removing a number of Typeib items from the latter), we say that the second declaration subsumes the first one.
1.2.4 Rules for disambiguation of multiple declarations¶
Only a single default
declaration can be in effect in any single module for any particular class. If there is more
than one default
declaration in scope, the conflict is resolved using the following rules:
Two declarations for two different classes are not considered to be in conflict; they can, however, clash at a particular use site as we’ll see in the following section.
Two declarations for the same class explicitly declared in the same module are considered a static error.
A
default
declaration in a module takes precedence over any importeddefault
declarations for the same class. However the compiler may warn the user if an imported declaration is not subsumed by the local declaration.For any two imported
default
declarations for the same class where one subsumes the other, we ignore the subsumed declaration.If a class has neither a local
default
declaration nor an importeddefault
declaration that subsumes all other importeddefault
declarations for the class, the conflict between the imports is unresolvable. The effect is to ignore alldefault
declarations for the class, so that no declaration is in effect in the module. The compiler may choose to emit a warning in this case, but no error would be triggered about the imports. Of course an error may be triggered in the body of the module if it contains an actual ambiguous type for the class with the conflicting imported defaults, as per the following subsection.
As a result, in any module each class has either one default declaration in scope (a locally-declared one, or an imported one that subsumes all other imported ones), or none. This single default is used to resolve ambiguity, as described in the next subsection.
Note that a default
declaration that repeats a type name more than once is perfectly valid, and sometimes may
be necessary to resolve coflicts. For example, a module that imports two conflicting defaults
default C (Int, Bool)
and
default C (Bool, Int)
may use a local declaration
default C (Int, Bool, Int)
to override the imports. Because this declaration subsumes both imported defaults it will not trigger any compiler
warning. When used to resolve ambiguity (next section) it behaves exactly like default C( Int, Bool)
; that is, the
repeats can be discarded.
1.2.5 Rules for disambiguation at the use site¶
The disambiguation rules are a conservative extension of the existing rules in Haskell 2010, which state that ambiguous type variable v is defaultable if:
v appears only in constraints of the form C v, where C is a class, and
at least one of these classes is a numeric class, (that is,
Num
or a subclass ofNum
), andall of these classes are defined in the Prelude or a standard library.
Each defaultable variable is replaced by the first type in the default list that is an instance of all the ambiguous variable’s classes. It is a static error if no such type is found.
The new rules instead require only that
v appears in at least one constraint of the form C v, where C is a single-parameter class.
Informally speaking, the type selected for defaulting is the first type from the default
list for class C that
satisfies all constraints on type variable v. If there are multiple Ci v constraints with
competing default
declarations, they have to resolve to the same type.
To make the design more explicit, the following algorithm can be used for default resolution, but any other method that achieves the same effect can be substitued:
Let S be the complete set of unsolved constraints, and initialize Sx to an empty set of constraints. For every v that is free in S:
Define Cv = { Ci v | Ci v ∈ S }, the subset of S consisting of all constraints in S of form (Ci v), where Ci is a single-parameter type class.
Define Dv, by extending Cv with the superclasses of every Ci in Cv
Define Ev, by filtering Dv to contain only classes with a default declaration.
For each Ci in Ev, find the first type T in the default list for Ci for which, for every (Ci v) in Cv, the constraint (Ci T) is soluble.
If there is precisely one type T in the resulting type set, resolve the ambiguity by adding a
v ~ T
i constraint to a set Sx; otherwise report a static error.
1.3 Examples¶
The main motivation for expanding the default
rules is the widespread use of the OverloadedStrings
language
extension, usually for the purpose of using the Text
data type instead of String
.
With this proposal in effect, and some form of FlexibleInstances
, the Haskell Prelude could export the declarations
default IsString (String)
default IsList ([])
Then a user module could activate the OverloadedStrings
or OverloadedLists
extension without triggering any
ambiguous type errors, still using the String
and list type from the Prelude.
The authors of the alternative string implementations like Text
would export the following declaration instead:
default IsString (Text, String)
Any user module that activates the OverloadedStrings
extension and imports Data.Text
would thus obtain the
default declaration suitable for working with Text
without any extra effort. Since the Prelude declaration’s list
of types is a sub-sequence of the latter declarations, it would be subsumed by it.
A user module could, by chance or by design, import two independently-developed modules that export competing defaults
for the same class, for example the previous Text
module and the Foundation.String
module with its own
exported declaration
default IsString (Foundation.String, String)
In this case the importing module would discard both contradictory declarations. If the developers desire a particular
default, they just have to declare it in the importing module. Furthermore, if they export this default
declaration, every importer of the module will have the conflicts resolved for them:
module ProjectImports (Text.Text, Foundation.String,
default IsString)
import qualified Data.Text as Text
import qualified Foundation.String as Foundation
default IsString (Text.Text, Foundation.String, String)
An equivalent story can be told for the OverloadedLists
, by replacing Text
and Foundation.String
by
Vector
and Foundation.String
by Foundation.Array
.
1.4 Effect and Interactions¶
GHC already supports two extensions that modify the defaulting mechanism: ExtendedDefaultRules and OverloadedStrings.
1.4.1 ExtendedDefaultRules¶
The former is fully devoted to defaulting. Its effect is to extend the defaulting rules so that they apply not only to
the class Num
as specified by the language standard, but also to any class in the following list: Show
,
Eq
, Ord
, Foldable
, Traversable
, or any numeric class. This list is hard-coded and not
user-extensible. Furthermore, the extension adds ()
and []
to the list of default types to try. If the present
proposal is accepted, ExtendedDefaultRules
could be reformulated as a set of actual default
declarations
brought into the scope:
default Show ((), Integer, Double)
default Eq ((), Integer, Double)
default Ord ((), Integer, Double)
default Foldable ([])
default Traversable ([])
default Num ((), Integer, Double)
1.4.2 OverloadedStrings¶
The OverloadedStrings
extension by itself causes many new ambiguities, much like the ambiguites caused by the
overloaded numeric literals which were the original reason for default
declarations in the first place. To rectify
this problem, the extension tweaks the defaulting mechanism. To quote from the GHC manual:
Each type in a
default
declaration must be an instance ofNum
or ofIsString
.If no
default
declaration is given, then it is just as if the module contained the declarationdefault (Integer, Double, String)
.The standard defaulting rule is extended thus: defaulting applies when all the unresolved constraints involve standard classes or
IsString
; and at least one is a numeric class orIsString
.
Once again, if the present proposal were adopted, the above rules could be expressed as an actual default
declaration:
default IsString (Integer, Double, String)
1.4.3 OverloadedLists¶
The OverloadedLists
extension does not currently bring any defaulting rules into scope. There is no need to change
that. Once this proposal is adopted, a library like Vector
could export a rule:
default IsList ([], Vector)
1.4.4 ParallelListComp, TransformListComp, and MonadComprehensions¶
The same consideration could be extended to the ParallelListComp
, TransformListComp
, and
MonadComprehensions
extensions. None of them bring any special defaulting rules. The desugaring of the first two
extensions on their own seems to be hard-wired to list-specific functions like zip
. This means that their use
effectively neutralizes OverloadedLists
. When combined with the MonadComprehensions
extension, the
ParallelListComp
extension is generalized to target any MonadZip
instance, but TransformListComp
is
not. To target a type other then []
, GHC Users Guide instead suggests the combination of three extensions:
{-# LANGUAGE TransformListComp, MonadComprehensions, RebindableSyntax #-}
There is some opportunity here for the expanded use of the present proposal, but the backward compatibility is sufficiently messy for me to refrain from making any suggestions. The extensions are also fairly old and not particularly popular, so they may be best left alone.
1.5 Costs and Drawbacks¶
1.5.1 Use-site conflicts¶
The earlier Haskell Prime proposal notes several ways in which defaults for different classes can contradict each other:
default A (Int,String,())
default B (String,(),Int)
(A t, B t) => t
default C (Int,Double,String,())
default D (Double,String,Int,())
(C t, D t) => t
The solution to this problem depends on where the conflicting defaults come from.
If they are declared in the same module: just don’t do that; or
if the defaults are imported, declare one or more overriding defaults to resolve the conflict.
1.6 Alternatives¶
1.6.1 Explicit ImportedDefaults
¶
Originally this proposal came with a separate ImportedDefaults
extension to enable the imports of default
declarations.
The proposal in its present form does not preserve full backward compatibility at the module level: it may change the
semantics of a previously valid module that was relying on the implicit default (Integer, Double)
rule. It is much
more likely, however, for this extension to resolve a type ambiguity that was preventing the module to compile, so the
committee decided to just enable it by default.
1.6.2 Declaration imports¶
Most features of the present proposal are completely determined by the constraints of backward compatibility and ease of use, but in case of declaration imports the choice was more arbitrary.
As stated above, the default option is to automatically import all default
declarations the module exports, with
no choice offered to the importer. If a default is unwanted, it can easily be modified or turned off by another
default
declaration.
This choice has been made because it seems to be easiest on the beginners: they don’t need to know anything about
defaults, especially if they work with a prepared set of imports that take care to resolve the potential default
conflicts for them.
An alternative approach would be to treat default exports the same way normal named exports are treated: if an
import
declaration explicitly lists the names it wants to import, it has to also explicitly list default
and
the class name for each desired default declaration. While this solution would probably leave the language more
consistent, it would also make its infamous learning curve even steeper for beginners.
An optional extension compatible with either of these alternatives would be to allow the hiding
clause to list the
default
declarations that should not be brought into the scope. This is not a part of the present proposal simply
because it’s unnecessary.
1.6.3 Module re-exports¶
As proposed in the Exporting the defaults section, a re-export of a whole module would not export the default
declarations imported from that module. The reasoning behind this constraint was to prevent a module from exporting a
conflicting set of declarations without also exporting a local subsuming declaration, as in this example:
module M( f, g, module A, module B ) where
import A -- Say A exports default X( P, R )
import B -- Say B exports default X( Q, R )
default X( P, Q, R )
The alternative would be to simplify the semantics and have module A, module B
re-export export everything
including the conflicting default
declarations. The compiler could warn the author that the lack of an export of a
subsuming declaration makes life harder for the module’s importers.
1.6.4 Global coherence¶
A proposal was put forward to treat default
declarations the same way as instance
declarations, i.e., to
always export and import them and to insist on their global coherence. In some ways this is easier in case of
default
declarations, because coherence can always be recovered by adding a new default
declaration that
subsumes all conflicting declarations for the same class. For example if any two modules contain two conflicting
declarations from above:
default C (Int,Double,String,())
default D (Double,String,Int,())
any third (presumably higher-level) module can recover the coherence and resolve the conflict in favour of the first module by declaring:
default C (Int,Double,String,(),Int,())
Both old declarations are subsumed by the new one. However there would be no way to simply turn off a default
declaration within a module. Besides, default
coherence wouldn’t bring any benefits it does to instance
declarations.
1.6.5 Multi-parameter type classes and other constraints¶
This proposal does not cover MPTCs nor type equality constraints, but this section will speculate how it could be extended to cover them in future.
First, let us generalize the single-parameter type class defaults by expanding the class name and each type name to full constraints. The above example
default IsString (Text, String)
would then be written as
default IsString t => (t ~ Text, t ~ String)
The former notation would be syntactic sugar for the latter. Since comma is already used as a constraint combinator, we’d actually prefer to replace it by something else. The logical choice would be semicolon, which always appears inside braces in the rest of the language:
default IsString t => {t ~ Text; t ~ String}
So now we have a general enough notation to accommodate MPTCs. We could, for example, say
default HasKey m k => {m ~ IntMap v, k ~ Int;
m ~ Map k v;
m ~ [(k, v)];
m ~ Map k v, k ~ String}
The defaulting algorithm would replace the constraint on the left hand side consecutively by each semicolon-separated constraint group on the right-hand side until it finds one that completely resolves the ambiguity.
Again, this extension is not a part of the proposal because it would depend on type equality at least, and because its utility is unproven. Still, it’s good to know that the proposal does not close off this potentially important development direction.