Re: Proposal: Add IsString instance for (Maybe a) to base

To play devil's advocate, why?
What does limiting IsString in this fashion gain anyone? It doesn't complicate type inference any more than it already is. For any applicative, there's the trivial instance
instance (IsString a, Applicative t) => IsString (t a) where fromString = pure . fromString
which is conceptually a very simple step and is always total.
Of course some functors admit other instances. I think some parser libraries already provide IsString instances as a nice syntax on matching string literals. A rule like this would invalidate those instances for no particularly good reason I can see. There is a tension between the "Do what I say" approach to library design and the "Do what I mean" approach where the library guesses at the programmer's intention and tries to succeed at all cost, sometimes failing silently when it guesses wrong. The Haskell development philosophy seems to err on the side of avoiding mistakes, which leads me to favor asking the programmer to be explicit in this particular case and not auto-inserting `Just`s for them.
Also, scope creep for type classes like `IsString` makes it more difficult to reason about what their methods mean. These more exotic `IsString` instances dilute the meaning and precision of the original type class. When I wrote a string in a program with `OverloadedStrings` enabled, the meaning used to be: "This is text". Then the meaning became: "This uses text internally, somewhere." As time goes on, I expect the meaning will eventually become: "This is convenient", whatever that means.

On Fri, Jul 12, 2013 at 12:57 PM, Gabriel Gonzalez
To play devil's advocate, why?
What does limiting IsString in this fashion gain anyone? It doesn't complicate type inference any more than it already is. For any applicative, there's the trivial instance
instance (IsString a, Applicative t) => IsString (t a) where fromString = pure . fromString
which is conceptually a very simple step and is always total.
Of course some functors admit other instances. I think some parser libraries already provide IsString instances as a nice syntax on matching string literals. A rule like this would invalidate those instances for no particularly good reason I can see.
There is a tension between the "Do what I say" approach to library design and the "Do what I mean" approach where the library guesses at the programmer's intention and tries to succeed at all cost, sometimes failing silently when it guesses wrong. The Haskell development philosophy seems to err on the side of avoiding mistakes, which leads me to favor asking the programmer to be explicit in this particular case and not auto-inserting `Just`s for them.
The programmer is being explicit; the type specifies the value is a Maybe. If it weren't, the Just wouldn't be inserted. Can you provide an example of code that compiles with this instance and does the wrong thing? I'm afraid I don't see a conflict between DWIS/DWIM here, as I cannot envision any situation where this would "guess wrong". I also cannot comprehend code where this instance would ever do anything other than "what I say". I can see a case where a user may be requested to supply an additional type annotation, however that's already true. Also, scope creep for type classes like `IsString` makes it more difficult
to reason about what their methods mean. These more exotic `IsString` instances dilute the meaning and precision of the original type class. When I wrote a string in a program with `OverloadedStrings` enabled, the meaning used to be: "This is text". Then the meaning became: "This uses text internally, somewhere." As time goes on, I expect the meaning will eventually become: "This is convenient", whatever that means.
I prefer to think of the meaning as "a mapping from string literals into the instance type." This quite closely with both the use of OverloadedStrings and the type of fromString. It's also perfectly compatible with a more general notion of IsString as proposed. What you consider scope creep is simply an artificial restriction of a more general instance.

* John Lato
The programmer is being explicit; the type specifies the value is a Maybe. If it weren't, the Just wouldn't be inserted.
Not necessarily — take Simon's original example: (shell "ls -l") { cwd = "/home/me" } If I didn't know the type of 'cwd', it would never occur to me while reading this code that you can supply Nothing there. Also, the fact that you can omit Just with string literals but not with string values or values of other types creates an unnecessary irregularity in the language. Roman

On Fri, Jul 12, 2013 at 2:13 PM, Roman Cheplyaka
* John Lato
[2013-07-12 13:56:31+0800] The programmer is being explicit; the type specifies the value is a Maybe. If it weren't, the Just wouldn't be inserted.
Not necessarily — take Simon's original example:
(shell "ls -l") { cwd = "/home/me" }
If I didn't know the type of 'cwd', it would never occur to me while reading this code that you can supply Nothing there.
So? If you're just reading the code, it doesn't matter. If you want to modify the code, presumably you would want to examine CreateProcess to at least get a glimpse of the types of fields. And if you wanted to modify the code to use the default value, I think a natural instinct would be to just do (shell "ls -l") which again would do the right thing. Also, the fact that you can omit Just with string literals but not with
string values or values of other types creates an unnecessary irregularity in the language.
OverloadedStrings (and number literals) are already a mess for this. Currently, we have (shell "ls -l") { cwd = Just "/home/me" } If I want to replace that literal with a value and I don't know the type of 'cwd', I already need to look it up. Otherwise I might try to do something like this: dirFromUser <- Text.getLine (shell "ls -l") { cwd = Just dirFromUser } and get a type error because it's expecting a Maybe String, not a Maybe Text. This happens to me very frequently, unless I remember to look up the record/function/whatever first to see what type is expected. I don't think adding a Just is any different from this situation. In either case, if you know the type of the thing you'll get the changes correct. If you don't know the type you're likely to either get it wrong or have to look it up. But as usual, the type makes it completely clear. A decent IDE helps a lot if you don't know the types of things in your code. Everyone should use one. Also, one nice thing about OverloadedStrings is that this doesn't need to be baked in. A library with a few orphan instances along these lines could easily be provided separately.

On Fri, Jul 12, 2013 at 2:45 AM, John Lato
On Fri, Jul 12, 2013 at 2:13 PM, Roman Cheplyaka
wrote: * John Lato
[2013-07-12 13:56:31+0800] The programmer is being explicit; the type specifies the value is a Maybe. If it weren't, the Just wouldn't be inserted.
Not necessarily — take Simon's original example:
(shell "ls -l") { cwd = "/home/me" }
If I didn't know the type of 'cwd', it would never occur to me while reading this code that you can supply Nothing there.
So? If you're just reading the code, it doesn't matter. If you want to
It does to me! Concrete types help me read. I have a lot of trouble understanding libraries that make extensive use of typeclasses, both reading their documentation and reading code that uses them.
OverloadedStrings (and number literals) are already a mess for this. Currently, we have
(shell "ls -l") { cwd = Just "/home/me" }
If I want to replace that literal with a value and I don't know the type of 'cwd', I already need to look it up. Otherwise I might try to do something like this:
dirFromUser <- Text.getLine (shell "ls -l") { cwd = Just dirFromUser }
I probably wouldn't, because I know the process package uses Strings. In general though I agree, with overloading there's a bit of a mess, but that's not license to expand the mess.
A decent IDE helps a lot if you don't know the types of things in your code. Everyone should use one.
It sounds reasonable in theory, but I've never seen an IDE I would call "decent".

On Fri, Jul 12, 2013 at 3:57 PM, Evan Laforge
On Fri, Jul 12, 2013 at 2:45 AM, John Lato
wrote: On Fri, Jul 12, 2013 at 2:13 PM, Roman Cheplyaka
wrote: * John Lato
[2013-07-12 13:56:31+0800] The programmer is being explicit; the type specifies the value is a Maybe. If it weren't, the Just wouldn't be inserted.
Not necessarily — take Simon's original example:
(shell "ls -l") { cwd = "/home/me" }
If I didn't know the type of 'cwd', it would never occur to me while reading this code that you can supply Nothing there.
So? If you're just reading the code, it doesn't matter. If you want to
It does to me! Concrete types help me read. I have a lot of trouble understanding libraries that make extensive use of typeclasses, both reading their documentation and reading code that uses them.
I agree completely, but it's already typeclass-based as soon as you enable OverloadedStrings. If you want concrete types, just don't use OverloadedStrings and you'll be good.
OverloadedStrings (and number literals) are already a mess for this. Currently, we have
(shell "ls -l") { cwd = Just "/home/me" }
If I want to replace that literal with a value and I don't know the type of 'cwd', I already need to look it up. Otherwise I might try to do something like this:
dirFromUser <- Text.getLine (shell "ls -l") { cwd = Just dirFromUser }
I probably wouldn't, because I know the process package uses Strings.
In all seriousness, I'm not that fond of the proposal myself. I just wanted to argue the other side because nobody else was. But so far, I've seen two main arguments against the proposal: 1. IsString is for things that are notionally some sort of string. 2. It makes the types more confusing. The second seems like a weak argument to me. First off, the proposal seems very similar to the recent generalization of Prelude.map, which had a great deal of support. Secondly, this objection appears to posit a programmer who knows the types well enough to differentiate between String/Text/ByteString, but not well enough to differentiate String/Maybe String. Which I suppose may be the case. It's easier to remember "package X uses Strings" than having to remember "X.foo :: String, X.bar :: Maybe String" etc. But I end up looking up the types of things quite frequently already, and I don't believe that this addition would have a very large impact on the number of type lookups I perform. I find the first argument more convincing, but I'm starting to think it's nothing more than a self-imposed limitation. Right now, most people seem to think IsString should mean "this data is notionally a string". I think that even Maybe String/Text fits that definition. But it seems even better to give IsString the meaning "string literals represent values of this type". It's both closer to how OverloadedStrings interprets IsString instances and more general. With this meaning, the slippery slope to other applicatives isn't a slope at all.

* John Lato
In all seriousness, I'm not that fond of the proposal myself. I just wanted to argue the other side because nobody else was. But so far, I've seen two main arguments against the proposal:
1. IsString is for things that are notionally some sort of string. 2. It makes the types more confusing.
The second seems like a weak argument to me. First off, the proposal seems very similar to the recent generalization of Prelude.map, which had a great deal of support.
The difference is that fromString is called implicitly. A similar precedent is numeric literals. When I type 1, a method is called, but most of the time I don't need to think about it because those methods agree with my intuitive semantics of numbers. It's easy to define a more or less proper instance instance (Applicative t, Num n) => Num (t n) which would allow to overload numbers even harder, but then I'd need to stop and think every time I see a number. Roman

On Fri, Jul 12, 2013 at 01:29:43PM +0300, Roman Cheplyaka wrote:
* John Lato
[2013-07-12 17:03:03+0800] In all seriousness, I'm not that fond of the proposal myself. I just wanted to argue the other side because nobody else was. But so far, I've seen two main arguments against the proposal:
1. IsString is for things that are notionally some sort of string. 2. It makes the types more confusing.
The second seems like a weak argument to me. First off, the proposal seems very similar to the recent generalization of Prelude.map, which had a great deal of support.
The difference is that fromString is called implicitly.
A similar precedent is numeric literals. When I type 1, a method is called, but most of the time I don't need to think about it because those methods agree with my intuitive semantics of numbers.
It's easy to define a more or less proper instance
instance (Applicative t, Num n) => Num (t n)
which would allow to overload numbers even harder, but then I'd need to stop and think every time I see a number.
Roman
I agree. When I see a string literal in the source code of a program, I want to be able to think of it as, well, a string. Not necessarily a String with a capital S, but a string of some sort nonetheless. A Maybe String is no such thing! The idea is only slightly less ridiculous than having a literal denote its singleton list, or singleton set, or tree with one leaf. (so -1 from me)

On 07/12/2013 02:03 AM, John Lato wrote:
In all seriousness, I'm not that fond of the proposal myself. I just wanted to argue the other side because nobody else was.
I appreciate that! :) I, too, enjoy playing the devil's advocate.
But so far, I've seen two main arguments against the proposal:
1. IsString is for things that are notionally some sort of string. 2. It makes the types more confusing.
The second seems like a weak argument to me. First off, the proposal seems very similar to the recent generalization of Prelude.map, which had a great deal of support.
There is a difference between type-classing `fmap` and type-classing string literals. `fmap` has laws governing its behavior that preserve a certain intuition and keep the instance writer on the straight and narrow path. Speaking of which, it would be an improvement if `fromString` instances some sort of functor laws, even if they were not totally rigorous and the destination category is changing on an instance-by-instance basis. For example, the `String`/`Text`/`ByteString` instances for `fromString` should obey (or *mostly* obey) these laws: fromString (str1 <> str2) = fromString str1 <> fromString str2 :: Vector Char/Text/ByteString fromString mempty = mempty Whereas the parser instances you mentioned would probably satisfy the following laws, at least for the `attoparsec` library, if the `string` parser returned `()` instead of the parsed string: fromString (str1 <> str2) = fromString str1 >> fromString str2 :: Parser () fromString mempty = return () I think the treatment of `fromString` as a functor from the String monoid to some other category is the closest theoretical statement of your intuition that an `IsString` instance should "represent" a `String`. Notice that this treatment actually rejects the proposed `Maybe` instance, since: fromString mempty /= (mempty :: (IsString a) => Maybe a) `mempty` should evaluate to `Nothing` in that context, but the proposal suggests it should be `Just ""`. You could argue that the current `Monoid` instance for `Maybe` is the wrong one and that `Just ""` is the correct identity, but then that would just confirm that the correct behavior is underspecified in this case and it is better to not let the programmer guess which behavior we meant.

On Fri, Jul 12, 2013 at 7:25 PM, Gabriel Gonzalez
On 07/12/2013 02:03 AM, John Lato wrote:
In all seriousness, I'm not that fond of the proposal myself. I just wanted to argue the other side because nobody else was.
I appreciate that! :) I, too, enjoy playing the devil's advocate.
But so far, I've seen two main arguments against the proposal:
1. IsString is for things that are notionally some sort of string. 2. It makes the types more confusing.
The second seems like a weak argument to me. First off, the proposal seems very similar to the recent generalization of Prelude.map, which had a great deal of support.
There is a difference between type-classing `fmap` and type-classing string literals. `fmap` has laws governing its behavior that preserve a certain intuition and keep the instance writer on the straight and narrow path.
Speaking of which, it would be an improvement if `fromString` instances some sort of functor laws, even if they were not totally rigorous and the destination category is changing on an instance-by-instance basis. For example, the `String`/`Text`/`ByteString` instances for `fromString` should obey (or *mostly* obey) these laws:
fromString (str1 <> str2) = fromString str1 <> fromString str2 :: Vector Char/Text/ByteString fromString mempty = mempty
None of this addresses my argument. That is, you (and most others) want to use IsString to denote things that are in some sense strings. This proposed distribution of fromstring over mappend is a reasonable attempt to formalize what it means to be string-like. But my point is that this may be the wrong intuition. I don't see any reason why IsString should be restricted to just notional strings.
participants (5)
-
Ben Millwood
-
Evan Laforge
-
Gabriel Gonzalez
-
John Lato
-
Roman Cheplyaka