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.