
From: "Iavor Diatchki"
I know this is not exactly accurate, but for beginners' purposes one could explain 'Functor' as a container type such as a list or a tree.
Only to students who already know what a (parameterised) list and tree ARE. Introducing both those concepts, and generalising them first over arbitrary element types, and then over the kind of container, is something that will take some time. It's on the way to that point that one needs to be able to talk about and use lists, without encountering Functor even in error messages.
In any case, if one wanted to avoid the issue, it seems entirely reasonable to define a function 'mapList' that has the list specific type. Most classes probably do this anyway.
That would be the alternative, but I doubt whether any classes do that right now. What's the point, when map already does the job?
From: Jon Fairbairn
A reasonable choice (forestalling the objection that using List.map, listMap or mapList would be too distracting for students) would be
lmap:: (t -> t') -> [t] -> [t'] map:: Functor f => (t -> t') -> f t -> f t'
I'd prefer mapList to lmap, to be honest--at least the connection between the name and the type is clear. But remember, this would at a stroke make Haskell' incompatible with all existing Haskell textbooks. Even if publishers brought out new editions, even if we told students to buy them, there are many, many second hand books in circulation, and it would be years before one could rely on students having Haskell' books. All that time, students would write map instead of mapList because that's what the book says, and get stuck with incomprehensible error messages. Is it really worth an incompatible change in the library functions used by all beginners, just to rename fmap to map? It seems to me that the gain from a change is very small, and the cost considerable. John

On 2006-08-18, John Hughes
Jon Fairbairn
A reasonable choice (forestalling the objection that using List.map, listMap or mapList would be too distracting for students) would be
lmap:: (t -> t') -> [t] -> [t'] map:: Functor f => (t -> t') -> f t -> f t'
I'd prefer mapList to lmap, to be honest--at least the connection between the name and the type is clear. But remember, this would at a stroke make Haskell' incompatible with all existing Haskell textbooks. Even if publishers brought out new editions, even if we told students to buy them, there are many, many second hand books in circulation, and it would be years before one could rely on students having Haskell' books. All that time, students would write map instead of mapList because that's what the book says, and get stuck with incomprehensible error messages. Is it really worth an incompatible change in the library functions used by all beginners, just to rename fmap to map? It seems to me that the gain from a change is very small, and the cost considerable.
But list is a functor, so it should work. They just get harder error messages, when their programs are wrong. Let's work on the error messages, not keeping the language harder to understand for more general programs. -- Aaron Denney -><-

On 2006-08-18 at 17:30+0200 "John Hughes" wrote:
From: Jon Fairbairn
A reasonable choice (forestalling the objection that using List.map, listMap or mapList would be too distracting for students) would be
lmap:: (t -> t') -> [t] -> [t'] map:: Functor f => (t -> t') -> f t -> f t'
I'd prefer mapList to lmap, to be honest--at least the connection between the name and the type is clear. But remember, this would at a stroke make Haskell' incompatible with all existing Haskell textbooks.
so publishers should love it! ;-)
Even if publishers brought out new editions, even if we told students to buy them, there are many, many second hand books in circulation, and it would be years before one could rely on students having Haskell' books. All that time, students would write map instead of mapList because that's what the book says, and get stuck with incomprehensible error messages. Is it really worth an incompatible change in the library functions used by all beginners, just to rename fmap to map? It seems to me that the gain from a change is very small, and the cost considerable.
It depends over how long a time you amortise the cost. I've heard that when people complained about the problems of having tab significant in makefiles, no correction was made because there were about a dozen users. We're farther on than that, but still at a stage where the population of users is not that large, and the users are flexible. So I think we should be reasonably bold about these things. To reinforce what Aaron said, if a programme works now, it'll still work if map suddenly means fmap. Jón -- Jón Fairbairn Jon.Fairbairn at cl.cam.ac.uk

From: "Jon Fairbairn"
To reinforce what Aaron said, if a programme works now, it'll still work if map suddenly means fmap.
Well, this isn't quite true, is it? Here's an example: class Foldable f where fold :: (a -> a -> a) -> a -> f a -> a instance Foldable [] where fold = foldr example = fold (+) 0 (map (+1) (return 2)) example has the value 3 (of course), but if you replace map by fmap then the code no longer compiles. In any case, I'm dubious about this as a criterion. I would guess that the majority if compiler runs for beginners (and perhaps for the rest of us too!) end in a type error, not a successful compilation, so arguably the quality of error messages when a type-check fails is more important than which programs compile. John

On 8/20/06, John Hughes
From: "Jon Fairbairn"
To reinforce what Aaron said, if a programme works now, it'll still work if map suddenly means fmap.
Well, this isn't quite true, is it? Here's an example:
class Foldable f where fold :: (a -> a -> a) -> a -> f a -> a
instance Foldable [] where fold = foldr
example = fold (+) 0 (map (+1) (return 2))
example has the value 3 (of course), but if you replace map by fmap then the code no longer compiles.
There's a proposal http://hackage.haskell.org/trac/haskell-prime/wiki/Defaulting that mentions extending defaulting to other typeclasses. That seems to fix this particular problem, but above you mentioned that this was "a whole new can of worms." Could you elaborate or point me to a discussion of the worms? Thanks, Jeffrey

From: "Jeffrey Yasskin"
On 8/20/06, John Hughes
wrote: From: "Jon Fairbairn"
To reinforce what Aaron said, if a programme works now, it'll still work if map suddenly means fmap.
Well, this isn't quite true, is it? Here's an example:
class Foldable f where fold :: (a -> a -> a) -> a -> f a -> a
instance Foldable [] where fold = foldr
example = fold (+) 0 (map (+1) (return 2))
example has the value 3 (of course), but if you replace map by fmap then the code no longer compiles.
There's a proposal http://hackage.haskell.org/trac/haskell-prime/wiki/Defaulting that mentions extending defaulting to other typeclasses. That seems to fix this particular problem, but above you mentioned that this was "a whole new can of worms." Could you elaborate or point me to a discussion of the worms?
Well, that proposal doesn't cover exporting defaults from modules. If beginners are not to need to write a default declaration in their own module, then we would need either to import the right default from the Prelude, or to have a default default for Monad and Functor, just as we have for numeric classes today. Importing defaults raises questions over how and when one can override an imported default with a different default within one's own module. The big problem with extended defaulting, though, is ambiguity. The proposal above includes an example: default A (Int, String, ()) default B (String, Int, ()) suggesting that a type in both classes should be defaulted to (), because this is the "first unambiguous type". That isn't a formal definition, and I actually have difficulty conceiving of a formal definition that would lead to this result. Even given such a thing, it would have very odd consequences: presumably if the first line read default A (Int,()), then Int would now be the first unambiguous type, and thus the chosen default. That means that removing a default *that was not chosen* would change the behaviour of the program! I wouldn't like to have to explain THAT to anybody! But this simple example of ambiguity is really the least worm in the can. What about subclassing? Presumably a class default should also apply to its subclasses... yet at times, we certainly want to override a previous default in a subclass (e.g. Num and Floating). How should defaulting interact with context reduction? Imagine a default C (String), and an instance definition instance C a => C [a]. If we see a C [b], should we apply the detault at once, instantiating b to Char, or after context reduction, instantiating b to String? How should defaulting interact with functional dependencies? Given a bidirectional fundep class C a b | a->b, b->a, then defaults on a and b separately can conflict. Worms! Paul Hudak and I thought about this when Haskell was first designed. We did come up with a proposal, but the rest of the committee thought it was too complex for what it bought us, hence it was omitted. Nowadays the class system is a lot more complicated than it was then--constructor classes, multi-parameter classes, fundeps--and I'm not terribly hopeful about the prospects for coming up with a simple and powerful design for defaulting. John

From: "Jon Fairbairn"
To reinforce what Aaron said, if a programme works now, it'll still work if map suddenly means fmap.
Well, this isn't quite true, is it? Here's an example: class Foldable f where fold :: (a -> a -> a) -> a -> f a -> a instance Foldable [] where fold = foldr example = fold (+) 0 (map (+1) (return 2)) example has the value 3 (of course), but if you replace map by fmap then the code no longer compiles. In any case, I'm dubious about this as a criterion. I would guess that the majority if compiler runs for beginners (and perhaps for the rest of us too!) end in a type error, not a successful compilation, so arguably the quality of error messages when a type-check fails is more important than which programs compile. John

From: "Jon Fairbairn"
To reinforce what Aaron said, if a programme works now, it'll still work if map suddenly means fmap.
Well, this isn't quite true, is it? Here's an example: class Foldable f where fold :: (a -> a -> a) -> a -> f a -> a instance Foldable [] where fold = foldr example = fold (+) 0 (map (+1) (return 2)) example has the value 3 (of course), but if you replace map by fmap then the code no longer compiles. In any case, I'm dubious about this as a criterion. I would guess that the majority if compiler runs for beginners (and perhaps for the rest of us too!) end in a type error, not a successful compilation, so arguably the quality of error messages when a type-check fails is more important than which programs compile. John

On 2006-08-20, John Hughes
From: "Jon Fairbairn"
To reinforce what Aaron said, if a programme works now, it'll still work if map suddenly means fmap.
Well, this isn't quite true, is it? Here's an example:
class Foldable f where fold :: (a -> a -> a) -> a -> f a -> a
instance Foldable [] where fold = foldr
example = fold (+) 0 (map (+1) (return 2))
example has the value 3 (of course), but if you replace map by fmap then the code no longer compiles.
Solely due to the compiler no longer seeing that list is the only intermediate type allowed. But you have to admit this code is a bit forced. People won't be combining things quite this way, and will be passing in values rather than bare returns.
In any case, I'm dubious about this as a criterion. I would guess that the majority if compiler runs for beginners (and perhaps for the rest of us too!) end in a type error, not a successful compilation, so arguably the quality of error messages when a type-check fails is more important than which programs compile.
Right, like I said, we need to work on better error messages. -- Aaron Denney -><-

On 2006-08-20 at 15:52+0200 "John Hughes" wrote:
From: "Jon Fairbairn"
To reinforce what Aaron said, if a programme works now, it'll still work if map suddenly means fmap.
Well, this isn't quite true, is it? Here's an example:
class Foldable f where fold :: (a -> a -> a) -> a -> f a -> a
instance Foldable [] where fold = foldr
example = fold (+) 0 (map (+1) (return 2))
example has the value 3 (of course), but if you replace map by fmap then the code no longer compiles.
The horror! Yet the code would still "work" in a sense, because map is effectively fmap with a type signature '(fmap::(a->b)->[a]->[b])'. Clearly the programmer meant to write either
example = fold (+) 0 (fmap (+1) [2])
or something with a type signature to disambiguate the "internal" overloading -- so I think it's probably a good thing that this ends up producing an error message, since it should have been given one in the first place! ;-)
In any case, I'm dubious about this as a criterion. I would guess that the majority if compiler runs for beginners (and perhaps for the rest of us too!) end in a type error, not a successful compilation, so arguably the quality of error messages when a type-check fails is more important than which programs compile.
Certainly (we all want the best error messages possible), except that we only need to worry about backwards compatibility for programmes that used to compile. Are there examples where replacing map with fmap changes the meaning of the programme? Jón -- Jón Fairbairn Jon.Fairbairn at cl.cam.ac.uk

On Fri, Aug 18, 2006 at 05:30:53PM +0200, John Hughes wrote:
on students having Haskell' books. All that time, students would write map instead of mapList because that's what the book says, and get stuck with incomprehensible error messages. Is it really worth an incompatible change in the library functions used by all beginners, just to rename fmap to map? It seems to me that the gain from a change is very small, and the cost considerable.
Yeah, the change doesn't seem worth it to me. And I still have concerns about ambiguity errors, if a beginner ever has to use an explicit type signature it sort of ruins the whole "type inference" benefit. I think everyone has tried to write class Cast a b where cast :: a -> b at some point but found it not very useful as whenever it was fed or used as an argument to another overloaded function, you ended up with ambiguity errors. with all the added generality being added all over the place, you need collections of functions that work on concrete data types to 'fix' things every now and again. lists are common enough that I think they deserve such 'fixing' functions. And it has nothing to do with newbies. having to write type annotations when not even doing anything tricky is not an acceptable solution for a type infered language. John -- John Meacham - ⑆repetae.net⑆john⑈

Hello,
I agree that this is a small change, and I don't expect that it will happen.
On 8/21/06, John Meacham
Yeah, the change doesn't seem worth it to me. And I still have concerns about ambiguity errors, if a beginner ever has to use an explicit type signature it sort of ruins the whole "type inference" benefit.
There is a big difference between having to declare all types vs. writing type signatures only in some places. In any case, it seems to me that it is good to encourage beginners to write type signatures, because (i) it clears their thinking about what they are trying to do, (ii) it leads to more accurate type errors, because the system can detect if it is the definition of a function that is wrong or its use. In fact, I write type signatures for the same reasons.
I think everyone has tried to write
class Cast a b where cast :: a -> b
at some point but found it not very useful as whenever it was fed or used as an argument to another overloaded function, you ended up with ambiguity errors.
with all the added generality being added all over the place, you need collections of functions that work on concrete data types to 'fix' things every now and again. lists are common enough that I think they deserve such 'fixing' functions. And it has nothing to do with newbies.
having to write type annotations when not even doing anything tricky is not an acceptable solution for a type infered language.
The problem you are describing above is entirely different... I agree that we should not overload everything, after all there must be some concrete types in the program. However, having a 'map' function that is specialized to lists in the standard library seems quite ad-hoc to me, in a way it is comparable to saying that 'return' should be specialized to IO, and we should have 'mreturn' in the Monad class (I am not suggesting this! :-) -Iavor

On Tue, Aug 22, 2006 at 01:52:45PM -0700, Iavor Diatchki wrote:
Hello, I agree that this is a small change, and I don't expect that it will happen.
On 8/21/06, John Meacham
wrote: Yeah, the change doesn't seem worth it to me. And I still have concerns about ambiguity errors, if a beginner ever has to use an explicit type signature it sort of ruins the whole "type inference" benefit.
There is a big difference between having to declare all types vs. writing type signatures only in some places.
I am not talking about type signatures, I am talking about having to annotate in the middle of a term. f x y | x `member` map g freeVars y = .... having to become f x y | x `member` map g (freeVars y :: [Id]) = .... types in the middle of terms are badbad. (and having to break up your logical structure is worse)
The problem you are describing above is entirely different... I agree that we should not overload everything, after all there must be some concrete types in the program.
However, having a 'map' function that is specialized to lists in the standard library seems quite ad-hoc to me, in a way it is comparable to saying that 'return' should be specialized to IO, and we should have 'mreturn' in the Monad class (I am not suggesting this! :-)
Well there will always be a tension between generality and ambiguity, finding the nice optimal spot is something we are going to need to do. I am just saying that talking about beginners is something of a red herring, and the real issue we should be concerned about is this tension. Concessions are made in both directions all the time, we don't have a returnM, returnP like you say (but there used to be, before type classes and such) but at the same time, fundeps were added to the mtl to avoid having to annotate everything with type signatures. So, I am not saying renaming fmap to map is bad outright, I am just saying that the question is trickier than just the error message problem it was previously stated in terms of. John -- John Meacham - ⑆repetae.net⑆john⑈

Hello,
On 8/22/06, John Meacham
I am not talking about type signatures, I am talking about having to annotate in the middle of a term.
f x y | x `member` map g freeVars y = ....
having to become
f x y | x `member` map g (freeVars y :: [Id]) = ....
There is no need to write such types... In this particular case the type of 'elem' indicates that the argument is a list. I don't think that a polymorphic 'map' function requires any more signatures than, say, '>>='. This certainly is not my experience when I use 'fmap'...
So, I am not saying renaming fmap to map is bad outright, I am just saying that the question is trickier than just the error message problem it was previously stated in terms of.
Do you have an example illustrating what is tricky about 'fmap'? As far as I understand 'map' used to be polymorphic, and later the distinction between 'map' and 'fmap' was specifically introduced to avoid the error messages that may confuse beginners. -Iavor
participants (6)
-
Aaron Denney
-
Iavor Diatchki
-
Jeffrey Yasskin
-
John Hughes
-
John Meacham
-
Jon Fairbairn