Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exportable named defaults #409

Merged
merged 13 commits into from Oct 1, 2021

Conversation

blamario
Copy link
Contributor

@blamario blamario commented Mar 8, 2021

This proposal has been accepted; the following discussion is mostly of historic interest.

@blamario
Copy link
Contributor Author

blamario commented Mar 8, 2021

@nomeata
Copy link
Contributor

nomeata commented Mar 8, 2021

Thanks! The rendering is a bit off, maybe double-check the rst syntax (in particular for code blocks)

@blamario
Copy link
Contributor Author

blamario commented Mar 8, 2021

Which code block do you have in mind? Or maybe you're thinking of a block quote instead?

@nomeata
Copy link
Contributor

nomeata commented Mar 8, 2021

My bad, I only gave it a corsary look, and what looked like “bad code block” is actually “carefully constructed math” (the grammar rules). Ignore me :-)

@blamario
Copy link
Contributor Author

blamario commented Apr 2, 2021

At @rae's suggestion, I'm bringing the proposal before the committee.
@nomeata

@nomeata nomeata requested a review from gridaphobe April 4, 2021 10:34
@nomeata nomeata added the Pending shepherd recommendation The shepherd needs to evaluate the proposal and make a recommendataion label Apr 4, 2021
@nomeata nomeata changed the title Exportable named defaults Exportable named defaults (under review) Apr 4, 2021
@nomeata nomeata removed the request for review from gridaphobe April 4, 2021 10:35
@i-am-tom
Copy link

i-am-tom commented Apr 9, 2021

To a degree, this kind of "fallback" is currently achievable with INCOHERENT:

instance {-# INCOHERENT #-} s ~ String => IsString s where ...

This isn't to say I'm pro- or anti- the proposal, but I'm interested in how the two behaviours would interact: if I have such an instance, and also a default, which one "wins" and when? I presume that, unlike the incoherence trick, providing a default instance wouldn't interfere with, say, :t fromString in ghci?

@blamario
Copy link
Contributor Author

I admit I've never seen this INCOHERENT trick before. What a disturbing sight it is.

if I have such an instance, and also a default, which one "wins" and when?

The default declarations only play a role for ambiguous types. The incoherent instance rule would resolve the type, so it wouldn't be ambiguous any more. In other words, default is the last line of defense before the compiler surrenders and reports the error.

You can actually verify this today using the existing default declaration for Num. The output of the test below is 4.0, rather than Down 4:

import Data.Ord (Down)
instance {-# INCOHERENT #-} n ~ Float => Num n where
    ...
default (Down Int)
main = print 4

I presume that, unlike the incoherence trick, providing a default instance wouldn't interfere with, say, :t fromString in ghci?

Exactly, no declared or inferred type would change. Also unlike the incoherence (or any other instance-based) trick, a default doesn't need to be globally, well, coherent: different and contradictory default declarations are allowed in different modules.

Copy link
Contributor

@gridaphobe gridaphobe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on the fence about this proposal.

On the one hand, I'm sympathetic to the motivation. It would be nice to just import Data.Text and have string literals default to Text.

On the other hand, defaulting has always felt like a bit of a misfeature to me, and I'm not sure this proposal moves the needle. In particular, the design around explicit exports and module-local overrides feels in conflict with the global coherence of typeclass instances.

What if we instead required defaulting rules to be globally coherent? Your rule (4) for disambiguating multiple rules assumes that rules should be coherent. I think the main change to the proposal would be that default rules would always be imported and exported, just like typeclass instances. I'm not sure this is the right design, maybe the way people think about defaulting rules isn't globally coherent, but it would be a conservative first step. We could always add the ability to selectively export and override defaulting rules later.

proposals/0000-exportable-named-default.rst Outdated Show resolved Hide resolved
proposals/0000-exportable-named-default.rst Outdated Show resolved Hide resolved
proposals/0000-exportable-named-default.rst Outdated Show resolved Hide resolved
proposals/0000-exportable-named-default.rst Outdated Show resolved Hide resolved
proposals/0000-exportable-named-default.rst Outdated Show resolved Hide resolved
proposals/0000-exportable-named-default.rst Outdated Show resolved Hide resolved
@gridaphobe
Copy link
Contributor

@blamario this proposal hasn't received much attention from the community. Have you advertised it? If not, I'd be happy to do so for you.

I think it would be good to get more community feedback on this proposal. It address a real problem, but it's not clear to me how severe the problem is, and I expect that the break from the typeclass model of global coherence would be somewhat controversial.

@blamario
Copy link
Contributor Author

blamario commented Apr 12, 2021

On the other hand, defaulting has always felt like a bit of a misfeature to me, and I'm not sure this proposal moves the needle. In particular, the design around explicit exports and module-local overrides feels in conflict with the global coherence of typeclass instances.

For better or worse, default declarations are a part of the language. One could argue (and formally propose) that they should be removed and the language simplified. If we're not doing that, I think we should embrace and use them to improve the language.

What if we instead required defaulting rules to be globally coherent? Your rule (4) for disambiguating multiple rules assumes that rules should be coherent. I think the main change to the proposal would be that default rules would always be imported and exported, just like typeclass instances. I'm not sure this is the right design, maybe the way people think about defaulting rules isn't globally coherent, but it would be a conservative first step. We could always add the ability to selectively export and override defaulting rules later.

That is a viable alternative to consider. I don't see it as a "conservative first step" though. It would not be backward compatible, if I understand what you're proposing correctly. The existing default rules are neither exported not imported.

I'm skeptical of the ImportedDefaults extension. We usually try to design extensions such that the authoring module (i.e. the one providing the named default) must enable the extension, but client modules do not.

I was of two minds about this extension. In the end I decided to add the extension in order to preserve full backward compatibility at the module level. I'm quite willing to make it the default behaviour, however, if others agree. It would certainly be a preferred candidate to add to the Haskell2022 extension set, perhaps alongside OverloadedStrings.

It's not totally clear here if by "sub-sequence" you mean ...

I believe this is explained by the parenthetical remark in the same sentence. Your conclusion is correct.

At first I was a bit concerned by this clause, it seems like it might be better to make incompatible default declarations a compile-time error.

Most Haskell code is free of ambiguous types, in my experience, so I wouldn't want to stop compiling if two imported defaults conflict but never need to be applied.

But I think the actual effect of this clause is to turn any ambiguous type that would have been defaulted by one of the defaults into an error. Maybe add one more sentence to make that obvious.

Correct. Okay, I will add a clarification. I'll also mention that the compiler should report a warning on conflicting imported defaults.

FYI, this link seems to be dead, but archive.org saved a copy: https://web.archive.org/web/20200107071106/https://prime.haskell.org/wiki/Defaulting.

That's disturbing. Is prime.haskell.org gone for good?

I assume this would be transient and that if these extensions someday made it into the standard we would always require the class to be specified?

That entirely depends on how much we value backward compatibility. If that's where we want to end up, we should start by warning if Num is not explicitly specified. That is not a part of my proposal, but I'm open to it.

this proposal hasn't received much attention from the community. Have you advertised it? If not, I'd be happy to do so for you.

Thank you, feel free to do it. I'm not good at advertising.

@simonpj
Copy link
Contributor

simonpj commented Apr 12, 2021

It's a well-written proposal, and I can understand the motivation -- but it is (necessarily I think) somewhat complicated.

It would take Real Work to implement, and maintain. For example, an exported default declaration default C(T) is a bit like an instance declaration... but if it's in a module that defines neither C nor T, it's like an orphan instance, which cause a good deal of trouble in the implementation. I'm reluctant to add a new sort of orphan declaration.

I could be persuaded if there were many users for whom the existing ExtendedDefaultRules extension (which should surely be discussed in the proposal!) is not enough. User manual entry

Overall, like Eric, the proposal seems has clear merits, but I'm not sure it has enough power-to-weight ratio to make me want to implement it.

@blamario
Copy link
Contributor Author

I can add a discussion of the ExtendedDefaultRules extension. I ignored it because it's well hidden in the GHCi part of documentation so I assumed it was an embarrassing hack that shouldn't be brought to light, but I'll cover it.

Can somebody with the knowledge clarify an undocumented point? The manual doesn't say whether the extension actually creates any new implicit default list and what it is. Is there any place where this is specified?

While I'm at it, I'll also discuss the default extension implied by OverloadedStrings in section 6.9.6.

The reason both existing extensions (I hope there aren't more?) fall short is exactly because they are module-local, being easier to implement. They can't make Text the default for the IsString constraint, because it's not part of base. I expect GHC will just follow the path of least resistance and expand base to include Data.Text. Then it can add another implicit default when Text is imported.

@treeowl
Copy link
Contributor

treeowl commented Apr 12, 2021

I haven't read the proposal yet, but the other day I was thinking it might be nice to switch from talking about default instances to talking about default type arguments for functions. That way, for example, (^) could default to Integer -> Word -> Integer. Or even more generally, associating functions with "fallback constraints" on their type arguments (i.e., defaulting to having some type arguments relate in a certain fashion). Either way, the defaults/fall-backs would be calculated when the function is compiled, and would be invoked (when needed) where the function is used. For compatibility with the current defaulting system, one defaulting constraint available would be the locally-modifiable "default default".

@gridaphobe
Copy link
Contributor

What if we instead required defaulting rules to be globally coherent? Your rule (4) for disambiguating multiple rules assumes that rules should be coherent. I think the main change to the proposal would be that default rules would always be imported and exported, just like typeclass instances. I'm not sure this is the right design, maybe the way people think about defaulting rules isn't globally coherent, but it would be a conservative first step. We could always add the ability to selectively export and override defaulting rules later.

That is a viable alternative to consider. I don't see it as a "conservative first step" though. It would not be backward compatible, if I understand what you're proposing correctly. The existing default rules are neither exported not imported.

Why wouldn't they be exported from the Prelude and thus implicitly imported by all modules? (I'm assuming here that we always import defaulting rules like instances.) Modules using NoImplicitPrelude wouldn't get the rules, true, but that's an advanced enough use case that I think we can bend the definition of backwards-compatible. Similarly, I would imagine that the various prelude-replacements would gladly take on the responsibility of providing sensible defaulting rules.

I'm skeptical of the ImportedDefaults extension. We usually try to design extensions such that the authoring module (i.e. the one providing the named default) must enable the extension, but client modules do not.

I was of two minds about this extension. In the end I decided to add the extension in order to preserve full backward compatibility at the module level. I'm quite willing to make it the default behaviour, however, if others agree. It would certainly be a preferred candidate to add to the Haskell2022 extension set, perhaps alongside OverloadedStrings.

If you agree that it would be better to have ImportedDefaults on by default, go ahead and put that in the actual proposal. We often have an "Alternatives" section where we collect options that were considered and dispreferred for various reasons, you could discuss the tradeoffs of full compatibility vs sensible defaults there. Then if the committee or community disagree, they can just say "I really think we should do Alternative X instead".

It's not totally clear here if by "sub-sequence" you mean ...

I believe this is explained by the parenthetical remark in the same sentence. Your conclusion is correct.

Ah right.

At first I was a bit concerned by this clause, it seems like it might be better to make incompatible default declarations a compile-time error.

Most Haskell code is free of ambiguous types, in my experience, so I wouldn't want to stop compiling if two imported defaults conflict but never need to be applied.

But I think the actual effect of this clause is to turn any ambiguous type that would have been defaulted by one of the defaults into an error. Maybe add one more sentence to make that obvious.

Correct. Okay, I will add a clarification. I'll also mention that the compiler should report a warning on conflicting imported defaults.

I'd probably model this after how we deal with conflicting class instances, which IIRC would mean no warning on import, but an error on use. The principle being that we shouldn't yell at a client module for something the library author did (break coherence).

I assume this would be transient and that if these extensions someday made it into the standard we would always require the class to be specified?

That entirely depends on how much we value backward compatibility. If that's where we want to end up, we should start by warning if Num is not explicitly specified. That is not a part of my proposal, but I'm open to it.

I'd prefer the end state to be one with fewer special cases, even if it takes a while to get there :)

this proposal hasn't received much attention from the community. Have you advertised it? If not, I'd be happy to do so for you.

Thank you, feel free to do it. I'm not good at advertising.

Done! I've posted to r/haskell and the Haskell Café.

@augustss
Copy link

I have wished for this kind of extension many times. Data.Text is just one example, I have several other uses cases.

I wouldn't worry about global coherence; it's no worse than what we have today. In fact, I'd argue that we don't want coherence.

@gridaphobe
Copy link
Contributor

In fact, I'd argue that we don't want coherence.

I think that's entirely possible, but I don't have a concrete usecase, just a hunch that defaulting rules are a bit different from instances. Do you have a specific example in mind?

@blamario
Copy link
Contributor Author

I have wished for this kind of extension many times. Data.Text is just one example, I have several other uses cases.

I would really appreciate if you could list a few of them. I'm beginning to worry that if people concentrate only on the issue of Text, they'll just reach for the simplest solution which is to add Text to base and another special rule to GHC.

…adedStrings, implicit ImportedDefaults alternative
@konn
Copy link

konn commented Apr 13, 2021

+1 for NamedDefaults, but I'm a bit sceptical about ExportedDefaults and ImportedDefaults, as it brings new kind of a "pollution" into scope and makes debugging a bit cumbersome.

@konn
Copy link

konn commented Apr 13, 2021

Personally, I've been implementing a computational algebra system with much finer algebraic structures than the standard Num-class. It uses RebindableSyntax extension which doesn't work properly with the current defaulting declaration. I think NamedDefaults fills this gaps.

@amigalemming
Copy link

I am compiling all my packages with -Wall and also use GHCi with -Wall. This includes a warning whenever I rely on default types. I think this is a good one, because it means every type choice is under my control and the compiler does not need to guess anything. Am I right, that this proposal only affects people who are not using -Wall?

@jyp
Copy link

jyp commented Apr 13, 2021

I'd love to be able to declare defaults for arbitrary classes, say example default Ring(Integer). I don't care to export orphan defaults. If it helps the implementation then they should be forbidden.

@gridaphobe
Copy link
Contributor

Thanks @blamario! I left one small question about the change in the 2nd commit. Otherwise, I agree this looks complete.

@simonpj since you had raised some concerns about the resolution algorithm, could you take another look as well?

@gridaphobe
Copy link
Contributor

@simonpj ping, I'd still like you to take another look at the updated algorithm before accepting this proposal.

@simonpj
Copy link
Contributor

simonpj commented Sep 10, 2021

@simonpj ping, I'd still like you to take another look at the updated algorithm before accepting this proposal.

I apologise. I have been 100.0% stalled because of my departure from MSR, so I have a massive backlog. And I'm giving to talks today. Next week, I promise -- yell at me if I don't deliver!

@gridaphobe
Copy link
Contributor

/remind me to yell at @simonpj on 9/17

@reminders-prs
Copy link

reminders-prs bot commented Sep 10, 2021

@gridaphobe set a reminder for Sep 17th 2021

@reminders-prs reminders-prs bot removed the reminder label Sep 17, 2021
@reminders-prs
Copy link

reminders-prs bot commented Sep 17, 2021

👋 @gridaphobe, yell at @simonpj

@gridaphobe
Copy link
Contributor

@simonpj ping!

@simonpj
Copy link
Contributor

simonpj commented Sep 21, 2021

@simonpj ping!

OK i have looked and made some suggestions.

@blamario
Copy link
Contributor Author

OK i have looked and made some suggestions.

Where?

proposals/0000-exportable-named-default.rst Outdated Show resolved Hide resolved
proposals/0000-exportable-named-default.rst Outdated Show resolved Hide resolved
proposals/0000-exportable-named-default.rst Outdated Show resolved Hide resolved
proposals/0000-exportable-named-default.rst Outdated Show resolved Hide resolved
proposals/0000-exportable-named-default.rst Outdated Show resolved Hide resolved
@simonpj
Copy link
Contributor

simonpj commented Sep 21, 2021

Sorry I failed to press submit

@simonpj
Copy link
Contributor

simonpj commented Sep 22, 2021

The virtue of my formulation was that the algorithm could be invoked on a unambiguous type as above and it wouldn't make any modifications.

I'm sorry, I don't understand this comment.

A type signature never invokes the defaulting process described in Section 2.5.

  • The process described there is invoked once, at the end of type inference, as a last-ditch attempt to resolve any still-un-solved constraints.
  • The defaulting process is applied only to "wanted" constraints.
  • Wanted constraints arise from calls of type-class-overloaded functions.

For example, when I call sort :: forall a. Ord a => [a] -> [a], the type inference engine instantiates sort's type with a=alpha, where alpha is a unification variable standing for an as-yet-unknown monotype, and emits a "wanted" constraint [W] Ord alpha. Later, we may figure out that alpha = Int; so now we have a wanted constraint [W] Ord Int. We can solve that by using the Ord Int instance.

@blamario
Copy link
Contributor Author

As I said: I guess I'm missing the picture of the overall type resolutioninference process. I was worried that type defaulting could interfere with the rest of the process if it introduced a wrong constraint. If it's only ever invoked as a last-ditch attempt that's not an issue.

@gridaphobe
Copy link
Contributor

@simonpj are you satisfied with @blamario's changes?

Let *S* be the complete set of unsolved constraints, and initialize *S*\ `x`:subscript: to an empty set of constraints.
For every *v* that is free in *S*:

1. Define *C*\ `v`:subscript: = { *C*\ `i`:subscript: v | *C*\ `i`:subscript: v ∈ *B*\ `v`:subscript: }, the subset of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nearly. You want S rather than Bv here. (Bv isn't even defined.)

*C*\ `v`:subscript:
3. Define *E*\ `v`:subscript:, by filtering *D*\ `v`:subscript: to contain only classes with a default declaration.
4. For each *C*\ `i`:subscript: in *E*\ `v`:subscript:, find the first type *T*\ `i`:subscript: in the default list
for *C*\ `i`:subscript: that satisfies all required constraints from the set *C*\ `v`:subscript:.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be more precise to say "...for which, for every (Ci v) in Cv, the constraint (Ci T) is soluble".

(To avoid suffix confusion, I'd remove the "i" subscript from T.

@simonpj
Copy link
Contributor

simonpj commented Sep 23, 2021

@simonpj are you satisfied with @blamario's changes?

Two small suggestions, then yes. Pull the trigger.

@gridaphobe gridaphobe changed the title Exportable named defaults (under review) Exportable named defaults Oct 1, 2021
@gridaphobe gridaphobe merged commit 664f880 into ghc-proposals:master Oct 1, 2021
@gridaphobe gridaphobe added Accepted The committee has decided to accept the proposal and removed Pending committee review The committee needs to evaluate the proposal and make a decision labels Oct 1, 2021
@gridaphobe
Copy link
Contributor

@blamario thank you for your contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Accepted The committee has decided to accept the proposal
Development

Successfully merging this pull request may close these issues.

None yet