Still - I don't know whether it's just due to unfamiliarity, but it seems to me that the simplicity and readability of the "OOP" equivalent (not requiring you to understand the semantics of "map" and "join" in the context of Reader), are a bit better. Also - OOP languages allow for dynamic dispatch and if I'm not totally mistaken (but I might), Haskell at least doesn't allow that unless you start adding language extensions, so you can't even make something like `Logger` a type class. I guess that would work in Scala, though, but then again, Scala also supports my original "OOP" approach anyway.
The ad-hoc polymorphism idea doesn't really solve the issue, I think, because even if you bundle up all the values into an "Environment" variable, you still have to pass it around from function to function.
Others have mentioned closures, too. I'm not sure if they mean something like:
data UserRepository = Repository { load :: UserID -> IO User, save :: User -> IO () }
mkUserRepository :: Directory -> Logger -> UserRepository
mkUserRepository baseDir logger = UserRepository { load = load, save = save }
where load = ...
save = ...
I mean, I guess that works somehow, but now we've made it possible to create user repositories with weird semantics (I know you hide the default data type constructor from other modules etc., but still). It also makes the implementation weirdly separated from the data type. And it makes me wonder... where would I even add the documentation for what the functions do. Having top-level functions certainly seems preferable to me.My point is, I think nothing would stop a PFP language from theoretically allowing to have some sort of "module scope" where we pass some dependencies to the module that can then be re-used internally. The module could even be represented internally as a data type + a constructor function, but it would allow you to use top-level functions that can use those arguments. It shouldn't break any of the correctness / purity guarantees.
E.g. (just a quick sketch):
-- UserRepository is simultaneously a module, a constructor function and a data type
module (logger :: Logger, baseDir :: File) => UserRepository where
-- ad-hoc syntax to show that UserRepository is
-- an argument to the function at call-site, but
-- it's looked up "implicitly" from the context
-- within the definition
load :: (UserRepository) => ID -> IO User
load id = loadFrom baseDir id
where loadFrom = ...
save :: (UserRepository) => User -> IO ()
save = ...
and then import UserRepository (load, save)
main = do
let baseDir = File "/some/Directory"
let logger = Logger "/some/Directory/logfile.log"
let repo = UserRepository baseDir logger
user <- load repo 1
...
Maybe PFP-ers wouldn't find that useful, but I think I would find it very readable while essentially only amounting to "syntactic sugar" without having to break language semantics. I don't want to claim that Haskell should do this, though. :)I also didn't want to claim that languages like Haskell don't have any way of doing this, just that, from a readability / simplicity point of view I find this "OOP" feature to be very useful and I haven't yet seen anything in the Haskell world that seems comparable _to me_ ergonomics-wise.