Imagine that you are a new user who has just started to learn Haskell. You’ve written a few simple functions that work with numbers, and now you’re about to start working with lists. Do you think you’d find Foldable
to be inherently more confusing than Eq
, Ord
, Num
, Fractional
, Integral
and RealFrac
, which you’ve already encountered (and had hand-waved away). Introductory Haskell, as it is commonly taught tends to be a confusing mess of relying heavily on type classes, yet not initially teaching them, and “one more” doesn’t noticeably increase that burden.
I have also taught Haskell to beginners several times, and I have to agree with the other experience reports that typeclass-polymorphic functions are usually not a big problem. The approach I took when teaching was to introduce type applications early on. We would then use ghci to first type in :t fmap
which yields Functor f => (a -> b) -> f a -> f b
. We would then look at :t fmap @Maybe
and :t fmap @[]
to inspect the type signatures (a -> b) -> Maybe a -> Maybe b
and (a -> b) -> [a] -> [b]
. In my experience this works pretty well.
One of the big problems I repeatedly saw with beginners is punning: The fact that we use both [5]
and [Int]
and [a]
and [x]
, and similarly for tuples, is a major stumbling block. In order to properly tell them apart you have to be quite fluent in Haskell so that you can always distinguish contexts where a type and contexts where a term is expected.
One of the big problems I repeatedly saw with beginners is punning: The fact that we use both
[5]
and[Int]
and[a]
and[x]
, and similarly for tuples, is a major stumbling block.
-
For lists you could try using the constructors directly:
5:[]
,x:[]
, introducing the more convenient syntax when students are writing out lists of length 5…or'H':'e':'l':'l':'o':',':' ':'R':'e':'a':'l':'W':'o':'r':'l':'d':'!':' ':' ':':':'-':'D':[]
. -
For tuples…perhaps an “it could be worse” approach is the least worst option: start off with the formal product-style notation and wait for the problems with that mathematical punning to appear, then switch to Haskell’s syntax.
I’m sure you’re pumped to hear that the proposal to introduce -XNoListTuplePuns
has been accepted.
Of course because the industry users don’t want “all of their programs to stop compiling” these pragmas won’t be on by default, but who knows, they might get included in a future
-XGHC####
pragma .
Hmm. Strange. I had no problem whatsoever. Perhaps my advantage was I was self-taught, didn’t have a lecturer trying to confuse me.
Indeed what annoys me about Lists is that as soon as I want to iterate over them, I can’t use [ , ]
but have to switch to (x: xs)
.
If I’d encountered :: List a
back then, I’d want to know what this List
thing is, whereas :: [Int]
is plain on its face – as is :: (Int, String)
. I’m not convinced there’s anything much wrong with punning-in-general data T a = T a
– as compared with huge amounts of Dependent Haskell that is entirely baffling and not at all like Math :: forall a. forall b -> ...
– where’d the .
go?; that’s entirely too subtle a syntactic way to signal a type not a term goes here.
An approach that can combine some of the advantages of both is to make it so that your datatypes all use the same names for equivalent functions, and then import them qualified; now your refactoring doesn’t have to change all the individual function names, it just needs to change the qualified import. E.g., suppose you have a module that uses Data.HashMap
as its dictionary-shaped data type; so you would go import qualified Data.HashMap as HashMap
, and possibly import Data.HashMap (HashMap)
; and you would then use functions like HashMap.lookup
, HashMap.insert
, etc. But now you realize that you’re putting untrusted inputs into your hashmap, making you susceptible to HashDOS attacks, so you decide to switch to Map
. Well, now all you need to do is change your import, and go s/HashMap/Map/g
on your source file, and you’re basically done, because HashMap
and Map
use (roughly) the same APIs.
I’ve never done this, but you could take it a step further and name your qualified imports by the role your data type plays in the module, and even import the same module multiple times to serve different roles. E.g., you could do this at the top of your source file:
import qualified Data.HashMap as Dict
type Dict = Data.HashMap.HashMap
-- and then, for example:
let myDict :: Dict String Int = Dict.singleton "hello" 1
myDict' = Dict.insert "world" 23 myDict
print $ Dict.lookup "world" myDict'
And now if you want to switch to Map
, you only have to change the import and the type alias.
In any case, I don’t think the refactoring argument is very strong; a much stronger argument is that when you write functions in terms of fmap
, they can accept a much wider range of data types than if you had written them in terms of map
. E.g., let’s say you want a function that takes a list of Maybe
s and replaces Nothing
s with a default value, like so:
setDefaults :: a -> [Maybe a] -> [a]
setDefaults def = map setDefault
where
setDefault Nothing = def
setDefault (Just x) = x
Now that function will only work on lists. But if we change map
to fmap
, we can make it work on any functor, and it costs us literally no extra effort (except for a slightly longer type signature, and one extra character in the implementation):
setDefaults :: Functor f => a -> f (Maybe a) -> f a
setDefaults def = fmap setDefault
where
setDefault Nothing = def
setDefault (Just x) = x
In other words; when the type is known anyway, using a more general function doesn’t buy you an awful lot, but if you write code that gets reused in such a way that your choice for a more general function propagates into a more general type, then that means your code becomes more useful, because it can cater for a wider range of data types, without sacrificing soundness.
You can take it even further by just using Backpack. I think it’s a bit unfortunate that Backpack works on the package level (I have some ideas about changing that), but otherwise it seems like a much more principled approach.
This is a No true Scotsman fallacy. We have evidence from three different people above, yet you dismiss it because their students are not True™ Beginners. Of course they are not, every True™ Beginner has to experience difficulties with Foldable
in Prelude
.
> all (error "have difficulty with Foldable in Prelude") []
True
The FTP was not about introducing the Foldable and Traversable classes or about adding some controversial instances to them. FTP was about exporting those classes by default from Prelude. For some versions of GHC and base this even meant, that Data.List exported the Foldable methods instead of functions restricted to the List type.
The other controversial addition was defining instances for Foldable on tuples. This together with FTP became really bad, because since then every Haskell programmer, both beginner and experts, don’t get a type error anymore, if they accidentally write length ('a','b')
. Without FTP you had to write Fold.length ('a',b')
to get hit by this problem.
Before FTP it was seen as a no-go to touch Prelude. Starting with FTP it became quite usual, which is bad. I always try to write my libraries to be compatible with a wide range of GHC and base versions. One way to achieve this is to minimize the use of GHC extensions. However, this does not save me from changes to base
and even more Prelude. I appreciate some changes like making Semigroup a superclass of Monoid and Applicative a superclass of Monad, and I am happy to find ways to program in a way that works before and after those changes. But I am unhappy about having to invest precious time into changes that only assist programmers that are too lazy to write import statements (or let the IDE do this) and who do not care about former and future versions of GHC and base.
As a consequence I have given up on writing warning-free code across GHC versions. I define one main GHC version for me and try to make my code warning free in this version. This is a real drawback since warnings can spot programming errors early, but not if they are lost in a series of import warnings caused by FTP and similar proposals.
If you are happy with <$>
then you can also define your own map
as alias to fmap
. You could use this at any call-site, but not in an instance definition.
I was wondering what people said about the Foldable instance for Tuple at the time so I tracked down the commit. The discussion links are dead.
Looking at the libraries mailing list archive from January 2011 it seems the discussion is here: Proposal: Add missing Foldable/Traversable instances for Prelude types
It’s not very interesting. The main point of discussion was if it should be lazy or not.