RFC: Can DefaultSignature compile-time conditional APIs be regarded "benign"?

Dear List, After recent discussions[1][2], from which I gathered that (compile-time) conditional APIs should be avoided, I'm left wondering if a certain kind of compile-time conditional API falls into that category as well, or whether it can be classified as "benign" and should therefore be tolerated. So I'm posting this for open discussion in the hope to reach a community consensus on whether the conditional use of the "DefaultSignature" language extension in APIs should be tolerated or not (e.g. for haskell-platform packages). To this end, in the rest of this post I try to make a case for why this class of conditional APIs is harmless. ----- So what's bad about "conditional" APIs? The way I understand the issue with respect to compile-time conditional APIs which either a) omit exposed modules or exported symbols altogether based on compile-time configuration, or even worse b) export different type-signatures for the very same symbol based on compile-time configuration are "bad", as they cause problems in client code using those APIs, such as a) not being able to properly declare the dependency on specific API features being enabled in the library API in CABAL files (since CABAL only allows to depend package version numbers), b) and from the other direction, there's no simple way for the library package to advertise the conditional features currently present in the registered package. c) Moreover, if the client code wants to use symbols which depend on compile-time configuration, it either becomes non-portable and/or becomes compile-time conditional itself. Note: It should be stressed that in the latter case the client code needs to take care to *exactly* replicate the semantics of the conditional switching between the API variants as employed by the library code (which can be CPP-based and/or CABAL-based), as any mismatch may result in compile time failures. This also poses the issue on how to properly document this "meta-property" in the library API documentation. (there may be more reasons, I just listed the ones off the top of my head) On the other hand, I think that there are cases, when a compile-time conditional API is "benign", for instance when the following condition holds: a) the library package exposes a /non-essential/ API feature based only on availability of a given compiler-feature/extension, b) the conditional feature is only ever visible to the client code if the client makes use of this compiler-feature itself, and last but not least c) the semantics of the client code must not change if the client code has been made portable (with respect to the availability of the conditional feature) E.g. consider the following hypothetical class definition: class Size a where size :: a -> Int #ifdef HAS_DEFAULT_SIGNATURE default size :: Generic a => a -> Int size x = ...Generics based implementation... #endif Now, if the client code doesn't exploit the default-signature implementation, then the client code is perfectly portable, and even if the conditional API features are available they have no visible effect whatsoever. OTOH, if the client code wants to make use of the default implementation, it needs to be built with a compiler supporting the Generics language feature, and due to a) the library is guaranteed to provide the default-signature implementation as well as soon as the client is able to use it. The client-code is now deliberately non-portable (but on the other hand, the operational semantics are guaranteed to be consistent, as there's only one case to consider) Consequently, as both scenarios don't lead to problematic situations, I'd argue that there is no harm in allowing this kind of compile-time conditional APIs using the DefaultSignature language extension. ---- cheers, hvr [1]: "Proposal: Adding generics-based rnf-helper to `deepseq`" on libraries list http://comments.gmane.org/gmane.comp.lang.haskell.libraries/17940 [2]: "[ANNOUNCE] hashable-generics" on haskell-cafe list http://comments.gmane.org/gmane.comp.lang.haskell.cafe/101323

On 11 November 2012 15:34, Herbert Valerio Riedel
I'd argue that there is no harm in allowing this kind of compile-time conditional APIs using the DefaultSignature language extension.
I agree. There's one minor issue: Say some client does not intend to rely on DefaultSignatures but forgets to write a definition for the method. He will then not get a "No explicit method or default declaration" warning. However, in this case I think the benefits outweigh this disadvantage. FWIW I prefer that deepseq and hashable both use the DefaultSignatures extension to provide generic default definitions. Bas

On Sun, Nov 11, 2012 at 11:22 AM, Bas van Dijk
On 11 November 2012 15:34, Herbert Valerio Riedel
wrote: I'd argue that there is no harm in allowing this kind of compile-time conditional APIs using the DefaultSignature language extension.
I agree.
There's one minor issue: Say some client does not intend to rely on DefaultSignatures but forgets to write a definition for the method. He will then not get a "No explicit method or default declaration" warning. However, in this case I think the benefits outweigh this disadvantage.
FWIW I prefer that deepseq and hashable both use the DefaultSignatures extension to provide generic default definitions.
hashable won't be using DefaultSignatures. See the discussion here: https://github.com/tibbe/hashable/pull/33

Johan Tibell
On Sun, Nov 11, 2012 at 11:22 AM, Bas van Dijk
wrote: On 11 November 2012 15:34, Herbert Valerio Riedel
wrote: I'd argue that there is no harm in allowing this kind of compile-time conditional APIs using the DefaultSignature language extension.
I agree.
There's one minor issue: Say some client does not intend to rely on DefaultSignatures but forgets to write a definition for the method. He will then not get a "No explicit method or default declaration" warning. However, in this case I think the benefits outweigh this disadvantage.
FWIW I prefer that deepseq and hashable both use the DefaultSignatures extension to provide generic default definitions.
hashable won't be using DefaultSignatures. See the discussion here:
Update: However, starting with hashable-1.2.0.0 DefaultSignatures are being used notwithstanding :-) cheers, hvr

On Sun, Nov 11, 2012 at 08:22:08PM +0100, Bas van Dijk wrote:
On 11 November 2012 15:34, Herbert Valerio Riedel
wrote: I'd argue that there is no harm in allowing this kind of compile-time conditional APIs using the DefaultSignature language extension.
I agree.
There's one minor issue: Say some client does not intend to rely on DefaultSignatures but forgets to write a definition for the method. He will then not get a "No explicit method or default declaration" warning. However, in this case I think the benefits outweigh this disadvantage.
If deepseq does not use DefaultSignatures, then clients must write instance NFData MyType where rnf = ... e ... If deepseq conditionally uses DefaultSignatures, and a client wishes to make use of that but still remain portable, then it must write instance NFData MyType where #if COMPILER_SUPPORTS_DEFAULT_SIGNATURES rnf = ... e ... #endif which, even ignoring the fact that we don't have an easy way to get a correct COMPILER_SUPPORTS_DEFAULT_SIGNATURES symbol at the moment, is more typing, and would not result in better/faster code. And worse still, if someone with a featureful compiler forgets to write the instance body, they will get no warning that it will not work for other users. It's true that if the client's portability is <= that of DefaultSignatures for other reasons then it could omit the instance body, but that feels like a dangerous direction to head in to me. And it would not be much more effort to write instance NFData MyType where rnf = genericRnf (where genericRnf is exported by the deepseq package). I think deepseq should either unconditionally use DefaultSignatures, or not use them at all. At this point, my preference would be the latter. Thanks Ian

Bas van Dijk
There's one minor issue: Say some client does not intend to rely on DefaultSignatures but forgets to write a definition for the method. He will then not get a "No explicit method or default declaration" warning. However, in this case I think the benefits outweigh this disadvantage.
...maybe the following shameless plug offers a different perspective in the design-space of DefaultSignatures: http://hackage.haskell.org/trac/ghc/ticket/7395#comment:7 But I'm not sure yet, whether handling DefaultSignatures implementations more like builtin classes and keeping vanilla H98 typeclass default-implementations in separate "namespace" would actually help here...

On Sun, 11 Nov 2012, Herbert Valerio Riedel wrote:
E.g. consider the following hypothetical class definition:
class Size a where size :: a -> Int #ifdef HAS_DEFAULT_SIGNATURE default size :: Generic a => a -> Int size x = ...Generics based implementation... #endif
Now, if the client code doesn't exploit the default-signature implementation, then the client code is perfectly portable, and even if the conditional API features are available they have no visible effect whatsoever.
OTOH, if the client code wants to make use of the default implementation, it needs to be built with a compiler supporting the Generics language feature, and due to a) the library is guaranteed to provide the default-signature implementation as well as soon as the client is able to use it. The client-code is now deliberately non-portable (but on the other hand, the operational semantics are guaranteed to be consistent, as there's only one case to consider)
That is, if I want to write a package that is portable I have to implement 'size' myself. But if I forget to do so then GHC will not warn me. That is, I have to know and remember that 'size' is somehow special. APIs that change according to available compiler features are more problematic than they look first. E.g. QuickCheck depends on TemplateHaskell if you run on GHC. That is, if you run on GHC then a module is included that provides TemplateHaskell functions. If you do not run on GHC this module is not included because you cannot use it anyway. However, this way QuickCheck becomes restricted to certain _versions_ of GHC, because TemplateHaskell changes from version to version of GHC. Thus when speaking about portability, we should always think about portability between GHC versions. (My proposal for a quote of the week is: GHC's strongest competitor is the next version of GHC.) Will the Generics based default implementation of 'size' be portable to future version of Generics? For a central package like deepseq I would prefer to provide functions like genericSize that other libraries can use if they want to rely on Generics. They would then write instance Size Foo where size = genericSize instead of instance Size Foo where This would be ok, wouldn't it?

Henning Thielemann
On Sun, 11 Nov 2012, Herbert Valerio Riedel wrote:
E.g. consider the following hypothetical class definition:
class Size a where size :: a -> Int #ifdef HAS_DEFAULT_SIGNATURE default size :: Generic a => a -> Int size x = ...Generics based implementation... #endif
Now, if the client code doesn't exploit the default-signature implementation, then the client code is perfectly portable, and even if the conditional API features are available they have no visible effect whatsoever.
OTOH, if the client code wants to make use of the default implementation, it needs to be built with a compiler supporting the Generics language feature, and due to a) the library is guaranteed to provide the default-signature implementation as well as soon as the client is able to use it. The client-code is now deliberately non-portable (but on the other hand, the operational semantics are guaranteed to be consistent, as there's only one case to consider)
That is, if I want to write a package that is portable I have to implement 'size' myself. But if I forget to do so then GHC will not warn me. That is, I have to know and remember that 'size' is somehow special.
a) If you are allowed to forget to implement 'size', then your type was able to provide a 'Generic' instance, how was that instance possible to come into existence in the first place? b) what if GHC provided something akin to a -fwarn-instantiate-default-signatures GHC option, which might even be turned on by default? c) what if GHC behaved like outlined in http://hackage.haskell.org/trac/ghc/ticket/7395#comment:7 instead?
APIs that change according to available compiler features are more problematic than they look first. E.g. QuickCheck depends on TemplateHaskell if you run on GHC. That is, if you run on GHC then a module is included that provides TemplateHaskell functions. If you do not run on GHC this module is not included because you cannot use it anyway. However, this way QuickCheck becomes restricted to certain _versions_ of GHC, because TemplateHaskell changes from version to version of GHC. Thus when speaking about portability, we should always think about portability between GHC versions. (My proposal for a quote of the week is: GHC's strongest competitor is the next version of GHC.) Will the Generics based default implementation of 'size' be portable to future version of Generics?
Fair enough, but breakage with newer GHCs can also happen when staying in the pure Haskell98/2010 domain (just think of the Num/Show/Eq superclass decoupling that occured in recent GHC versions). That is, for Haskell libraries there's always the risk needing maintainance when a new GHC version comes out. (But I'm not arguing this to be a good thing either...)
For a central package like deepseq I would prefer to provide functions like genericSize that other libraries can use if they want to rely on Generics. They would then write
instance Size Foo where size = genericSize
instead of
instance Size Foo where
This would be ok, wouldn't it?
Are you preferring 'genericSize' to avoid the implicity that comes with DefaultSignatures, and the resulting risk of inadvertently writing non-portable code because GHC currently doesn't warn you?

Hi,
I would prefer it if packages would use DefaultSignatures conditionally,
instead of creating new packages just to define a generic variant of a
function. In general, though, I think it's good practice to define the
defaults as separate functions (like `genericSize`) and export them, so
that those not using DefaultSignatures can still write an instance easily.
As for the concerns about people using DefaultSignatures and not realizing
their code is not portable, I guess we could reject programs that use a
default signature (when writing an empty instance) unless they enable
-XDefaultSignatures. Then people would know they are relying on an
extension.
Cheers,
Pedro
On Sun, Nov 11, 2012 at 9:06 PM, Herbert Valerio Riedel
Henning Thielemann
writes: On Sun, 11 Nov 2012, Herbert Valerio Riedel wrote:
E.g. consider the following hypothetical class definition:
class Size a where size :: a -> Int #ifdef HAS_DEFAULT_SIGNATURE default size :: Generic a => a -> Int size x = ...Generics based implementation... #endif
Now, if the client code doesn't exploit the default-signature implementation, then the client code is perfectly portable, and even if the conditional API features are available they have no visible effect whatsoever.
OTOH, if the client code wants to make use of the default implementation, it needs to be built with a compiler supporting the Generics language feature, and due to a) the library is guaranteed to provide the default-signature implementation as well as soon as the client is able to use it. The client-code is now deliberately non-portable (but on the other hand, the operational semantics are guaranteed to be consistent, as there's only one case to consider)
That is, if I want to write a package that is portable I have to implement 'size' myself. But if I forget to do so then GHC will not warn me. That is, I have to know and remember that 'size' is somehow special.
a) If you are allowed to forget to implement 'size', then your type was able to provide a 'Generic' instance, how was that instance possible to come into existence in the first place?
b) what if GHC provided something akin to a -fwarn-instantiate-default-signatures GHC option, which might even be turned on by default?
c) what if GHC behaved like outlined in http://hackage.haskell.org/trac/ghc/ticket/7395#comment:7 instead?
APIs that change according to available compiler features are more problematic than they look first. E.g. QuickCheck depends on TemplateHaskell if you run on GHC. That is, if you run on GHC then a module is included that provides TemplateHaskell functions. If you do not run on GHC this module is not included because you cannot use it anyway. However, this way QuickCheck becomes restricted to certain _versions_ of GHC, because TemplateHaskell changes from version to version of GHC. Thus when speaking about portability, we should always think about portability between GHC versions. (My proposal for a quote of the week is: GHC's strongest competitor is the next version of GHC.) Will the Generics based default implementation of 'size' be portable to future version of Generics?
Fair enough, but breakage with newer GHCs can also happen when staying in the pure Haskell98/2010 domain (just think of the Num/Show/Eq superclass decoupling that occured in recent GHC versions). That is, for Haskell libraries there's always the risk needing maintainance when a new GHC version comes out. (But I'm not arguing this to be a good thing either...)
For a central package like deepseq I would prefer to provide functions like genericSize that other libraries can use if they want to rely on Generics. They would then write
instance Size Foo where size = genericSize
instead of
instance Size Foo where
This would be ok, wouldn't it?
Are you preferring 'genericSize' to avoid the implicity that comes with DefaultSignatures, and the resulting risk of inadvertently writing non-portable code because GHC currently doesn't warn you?
_______________________________________________ Libraries mailing list Libraries@haskell.org http://www.haskell.org/mailman/listinfo/libraries

* José Pedro Magalhães
Hi,
I would prefer it if packages would use DefaultSignatures conditionally, instead of creating new packages just to define a generic variant of a function. In general, though, I think it's good practice to define the defaults as separate functions (like `genericSize`) and export them, so that those not using DefaultSignatures can still write an instance easily.
Do you propose exposing genericSize conditionally? I'm strongly against this. I think that packages' interfaces may not depend on the compiler or compilation options. Only the implementation of the intreface may.
As for the concerns about people using DefaultSignatures and not realizing their code is not portable, I guess we could reject programs that use a default signature (when writing an empty instance) unless they enable -XDefaultSignatures. Then people would know they are relying on an extension.
Can't we simply ignore default signatures (and corresponding default definitions) when DefaultSignatures is off? E.g. instance Show Foo is accepted in any case, but has different semantics depending on whether DefaultSignatures is on. This way it's more Haskell2010 compatible. I realise that changing semantics is not very good, but people usually test this kind of code anyway, so I don't think it will be a big problem in practice. In that case it'd make sense to add a separate flag (-XUseGenericDefinition?) whose impact on the program semantics is more obvious from its name. Roman

Hi Roman,
On Mon, Nov 12, 2012 at 9:44 AM, Roman Cheplyaka
* José Pedro Magalhães
[2012-11-12 09:27:17+0000] Hi,
I would prefer it if packages would use DefaultSignatures conditionally, instead of creating new packages just to define a generic variant of a function. In general, though, I think it's good practice to define the defaults as separate functions (like `genericSize`) and export them, so that those not using DefaultSignatures can still write an instance easily.
Do you propose exposing genericSize conditionally?
No; it should be exported in both cases (it doesn't harm exporting it even when DefaultSignatures is supported). Cheers, Pedro

I like those ideas.
However this still leaves #7395 unresolved.
I do like hvr's idea of using the deriving mechanism to derive classes
with DefaultSignatures.
Bas
#7395 http://hackage.haskell.org/trac/ghc/ticket/7395
On 12 November 2012 10:27, José Pedro Magalhães
Hi,
I would prefer it if packages would use DefaultSignatures conditionally, instead of creating new packages just to define a generic variant of a function. In general, though, I think it's good practice to define the defaults as separate functions (like `genericSize`) and export them, so that those not using DefaultSignatures can still write an instance easily.
As for the concerns about people using DefaultSignatures and not realizing their code is not portable, I guess we could reject programs that use a default signature (when writing an empty instance) unless they enable -XDefaultSignatures. Then people would know they are relying on an extension.
Cheers, Pedro
On Sun, Nov 11, 2012 at 9:06 PM, Herbert Valerio Riedel
wrote: Henning Thielemann
writes: On Sun, 11 Nov 2012, Herbert Valerio Riedel wrote:
E.g. consider the following hypothetical class definition:
class Size a where size :: a -> Int #ifdef HAS_DEFAULT_SIGNATURE default size :: Generic a => a -> Int size x = ...Generics based implementation... #endif
Now, if the client code doesn't exploit the default-signature implementation, then the client code is perfectly portable, and even if the conditional API features are available they have no visible effect whatsoever.
OTOH, if the client code wants to make use of the default implementation, it needs to be built with a compiler supporting the Generics language feature, and due to a) the library is guaranteed to provide the default-signature implementation as well as soon as the client is able to use it. The client-code is now deliberately non-portable (but on the other hand, the operational semantics are guaranteed to be consistent, as there's only one case to consider)
That is, if I want to write a package that is portable I have to implement 'size' myself. But if I forget to do so then GHC will not warn me. That is, I have to know and remember that 'size' is somehow special.
a) If you are allowed to forget to implement 'size', then your type was able to provide a 'Generic' instance, how was that instance possible to come into existence in the first place?
b) what if GHC provided something akin to a -fwarn-instantiate-default-signatures GHC option, which might even be turned on by default?
c) what if GHC behaved like outlined in http://hackage.haskell.org/trac/ghc/ticket/7395#comment:7 instead?
APIs that change according to available compiler features are more problematic than they look first. E.g. QuickCheck depends on TemplateHaskell if you run on GHC. That is, if you run on GHC then a module is included that provides TemplateHaskell functions. If you do not run on GHC this module is not included because you cannot use it anyway. However, this way QuickCheck becomes restricted to certain _versions_ of GHC, because TemplateHaskell changes from version to version of GHC. Thus when speaking about portability, we should always think about portability between GHC versions. (My proposal for a quote of the week is: GHC's strongest competitor is the next version of GHC.) Will the Generics based default implementation of 'size' be portable to future version of Generics?
Fair enough, but breakage with newer GHCs can also happen when staying in the pure Haskell98/2010 domain (just think of the Num/Show/Eq superclass decoupling that occured in recent GHC versions). That is, for Haskell libraries there's always the risk needing maintainance when a new GHC version comes out. (But I'm not arguing this to be a good thing either...)
For a central package like deepseq I would prefer to provide functions like genericSize that other libraries can use if they want to rely on Generics. They would then write
instance Size Foo where size = genericSize
instead of
instance Size Foo where
This would be ok, wouldn't it?
Are you preferring 'genericSize' to avoid the implicity that comes with DefaultSignatures, and the resulting risk of inadvertently writing non-portable code because GHC currently doesn't warn you?
_______________________________________________ Libraries mailing list Libraries@haskell.org http://www.haskell.org/mailman/listinfo/libraries
_______________________________________________ Libraries mailing list Libraries@haskell.org http://www.haskell.org/mailman/listinfo/libraries

On Mon, 12 Nov 2012, Bas van Dijk wrote:
I like those ideas.
However this still leaves #7395 unresolved.
I do like hvr's idea of using the deriving mechanism to derive classes with DefaultSignatures.
The more I think about it the more my impression becomes that we are trying to solve a problem the complicated way where a simple solution already exists. I see the motivation behind DefaultSignatures and encountered the situation often enough that lead to the introduction of DefaultSignatures. However I always solved the problem by providing functions like genericSize or genericRnf (though I have not used Generics for default implementations). Its simple and its portable. Now with DefaultSignatures we can simplify some bits, but in order to stay portable with DefaultSignatures things get more complicated than staying portable without DefaultSignatures at all.
participants (7)
-
Bas van Dijk
-
Henning Thielemann
-
Herbert Valerio Riedel
-
Ian Lynagh
-
Johan Tibell
-
José Pedro Magalhães
-
Roman Cheplyaka