haskell / text

Haskell library for space- and time-efficient operations over Unicode text.
http://hackage.haskell.org/package/text
BSD 2-Clause "Simplified" License
406 stars 158 forks source link

Add default IsString (Text, String) #610

Open Bodigrim opened 1 month ago

Bodigrim commented 1 month ago

Once https://gitlab.haskell.org/ghc/ghc/-/merge_requests/11853 is merged (which is presumably in time for GHC 9.12), we should add under appropriate CPP guard

{-# LANGUAGE NamedDefaults #-}
default IsString (Text, String)

CC @blamario

Bodigrim commented 1 month ago

Not sure how to fit lazy Text though. Should Data.Text export default IsString (StrictText, String) and Data.Text.Lazy export default IsString (LazyText, String)? But then if someone exports both, defaults will clash and annihilate.

We can put default IsString (StrictText, String) into Data.Text and then default IsString (LazyText, StrictText, String) into Data.Text.Lazy. If someone exports both, defaults will subsume one another resulting in the latter one. Yet in this case pretty much any string literal will be lazy Text, which is not terribly helpful.

What I don't know is whether defaults are imported through qualified imports.

There are also Builders. What should they declare as defaults?

@blamario any suggestions?

blamario commented 1 month ago

My understanding is that the use of strict lazy text is approximately evenly divided in practice, so there's no reason to prefer one over the other. My preferred choice would then be to

  1. put default IsString (StrictText, String) into Data.Text,default IsString (LazyText, String) into Data.Text.Lazy, etc for builders;
  2. if necessary, and only if necessary, also add modules Data.Text.Default and Data.Text.Lazy.Default which would re-export the parent module as well as default IsString (StrictText, LazyText, String) and default IsString (LazyText, StrictText, String) respectively, with the sole purpose of resolving the ambiguity.
Bodigrim commented 1 month ago

My understanding is that the use of strict lazy text is approximately evenly divided in practice, so there's no reason to prefer one over the other.

In general yes, but for literal strings I'd imagine strict Text to be much more relevant, because there is no point to chunk it.

What I don't know is whether defaults are imported through qualified imports.

Could you please clarify this?

blamario commented 1 month ago

Yes, any import of a module will import all the defaults it exports. Same behaviour as with class instances, except the latter are also implicitly exported.

Bodigrim commented 1 month ago

The thing is that people commonly import all three modules at once. This is especially true for Builder and lazy Text: I can barely imagine a module that imports Data.Text.Lazy.Builder but not Data.Text.Lazy. If would be unfortunate if such common situation annihilates conflicting defaults.

The suggestion with *.Default modules does not feel appealing to me.

blamario commented 1 month ago

If would be unfortunate if such common situation annihilates conflicting defaults.

If and when it does, the one-line fix would be to add a de-conflicting default declaration to the importing module. That's why I said I'd add the *.Default modules only if necessary, i.e. if the user base clamours for an even easier solution.

phadej commented 1 month ago

One can imagine a use case where having default IsString (String) but also using text is desirable. It feels that the exportable defaults are just lacking in that respect. (In particular, why wouldn't Prelude export default IsString (String)? There are Num defaults, and maybe I want Int instead of Integer 99% of the time)

I feel, that it's better for text to not do anything in source, but add a note in the documentation, so the users who need could add default IsString (Text) to their modules. Note that asking people to do default IsString (Text) themselves is virtually the same as providing *.Default module proposed above.

blamario commented 1 month ago

One can imagine a use case where having default IsString (String) but also using text is desirable.

A default declaration doesn't prevent you from using text, or anything else for that matter: if a module already compiles, it will continue compiling with additional defaults exactly the same.

why wouldn't Prelude export default IsString (String)?

The Prelude could export default IsString (String). As per above, this wouldn't break anything, and it would be a precondition for adding OverloadedStrings to the GHC2025 edition.

phadej commented 1 month ago

this wouldn't break anything

Yes, but it will break utility of default IsString (Text) as proposal says

If a class has neither a local default declaration nor an imported default declaration that subsumes all other imported default declarations for the class, the conflict between the imports is unresolvable.

So if there are exported default IsString (String) in Prelude, and default IsString (Text) in Data.Text, then as far as I understand for all users of Prelude and Data.Text the situation will be as without any defaults, i.e .as today.

OTOH, if default IsString (String, Text) (or (String, StrictText, LazyText, Builder) is added, then some other library with own type (e.g. os-string) will then conflict still. The default doesn't seem to behave well in diamond dependency graphs.

phadej commented 1 month ago

Also note, the extra default IsString (Text, String) may change the code which used to see default IsString (String) only. That would be spooky action at the distance: an added import (at least slightly) changing the result of elaboration!

GHC will pick the first type which satisfies all constraints, but if Text and String both satisfy everything, then it will flip.