Remeber that OO method calling† is nothing but syntactic sugar for supplying an extra this
/ self
argument to the function:
-- OO ┃ functional/procedural
Client c = ...; │ c = ... :: Client
... │ ...
main() {print(c.getFoo());} │ main = print(getFoo c)
It is thus quite possible, and often useful, to go this route, both in a procedural language like C and in an FP language.
data Client {
url :: String
, ...
}
getFoo :: Client -> String
getFoo (Client{url = u}) = ...
Yes, that requires you to explicitly pass the Client
object around, but this isn't necessarily a bad thing – provided you have properly distinguished types, it can be pretty obvious what needs to be passed as which argument of what function, and this approach actually scales better than OO methods because you can have multiple objects as arguments, and each function can take just those that it needs.
Of course, there are situation where you do have a whole bunch of functions that all need the same object, and would like to have it happen under the hood without explicitly passing it everywhere. That can be done by hiding it in the result type.
type Reader c r = c -> r
getFoo :: Reader Client String
getBar :: Reader Client Int
getBaz :: Reader Client Double
This reader monad can be used with standard monad combinators:
quun = (`runReader`c) $ do
foo <- getFoo -- `c` argument implicitly passed
bar <- getBar
baz <- getBaz
return (calcQuun foo bar (2*baz))
This approach is particularly useful if you also have mutation in your methods, as is commonplace in OO. With explicit passing, this becomes very cumbersome indeed as you need to work with updated copies and need to be careful to pass the correct version to each function. With the state monad, this is handled automatically as if it were true mutation.
†I disregard inheritance here. If you call a method through a superclass pointer, there's an extra vtable lookup, but that can be modelled as just another field in the record type that tells you what subclass this object belongs to.