This is strange to me because I don't see FP faltering here at all. I suppose it depends on precisely what you mean by "OOP" and "FP". Below is an example implementation in my Haskell effect system Bluefin. It defines a Logger interface (that can log a message with severity) and then instantiates it with two implementations
The file logger also brackets the opening of the file so that if abnormal termination is encountered then the file handle is guaranteed to be cleaned up. This is similar to RAII.
I really like this solution! It's just programming against an interface, and then instantiating the interface in different ways. I think an solution using inheritance would be worse, because it would use a special language concept (inheritance) rather than a standard one (function definition).
Perhaps this is "OOP" and not "FP"? That's fine by me! But in that case I conclude Haskell is an excellent OOP language. (I already conclude that it's an excellent imperative language.)
{-# LANGUAGE GHC2021 #-}
import Bluefin.Compound
import Bluefin.Eff
import Bluefin.IO
import System.IO
import Prelude hiding (log)
newtype Logger e =
-- Log a message with a severity
MkLogger {log :: String -> Int -> Eff e ()}
withStdoutLogger ::
(e1 :> es) =>
IOE e1 ->
(forall e. Logger e -> Eff (e :& es) r) ->
Eff es r
withStdoutLogger io k =
useImplIn
k
MkLogger
{ log =
\msg sev ->
effIO io (putStrLn (show sev ++ ": " ++ msg))
}
withFileLogger ::
(e1 :> es) =>
FilePath ->
IOE e1 ->
(forall e. Logger e -> Eff (e :& es) r) ->
Eff es r
withFileLogger fp io k =
bracket
(effIO io (openFile fp ReadMode))
(effIO io . hClose)
( \handle -> do
useImplIn
k
MkLogger
{ log =
\msg sev ->
effIO io (hPutStr handle (show sev ++ ": " ++ msg))
}
)
That's fine, but suppose you wanted to swap out loggers (or add an extra logger target) at runtime. Maybe someone wants transiently hooks in an observer by logging into a webpage that shoots the logs at that users browser. I don't know enough Haskell (and it's hard enough to read) that I can't tell if this code can deal with that case.
Swapping out loggers at runtime is a separate concern (inheritance and OOP don't make that any easier), and is more to do with control flow or whether there's an indirection when accessing the logger.
Polymorphism definitely makes it much easier, and it is a core concept in OOP. I am not advocating for or against OOP, but “swapping behaviour at runtime” is one of the things it’s good at.
If you want to be able to swap out implementations, personally I’d much rather rust’s trait system (or Java or typescript’s interfaces) over what “OO languages” like C++ give you. I basically never want a strict tree of object types with inheritance.
This isn’t an OO idea. I’m pretty sure FP languages like Haskell or ocaml have something very similar.
I don’t know about Rust, but interfaces as in Java (or things like protocols in Objective-C) are fundamentally object-oriented features. It is at the core of the concept. Java is much more object-oriented than C++.
You can do something similar with overloaded functions and functions interface, at which point you’re re-implementing OOP without encapsulation and worse language support.
It’s certainly not a feature that is exclusive to OO languages. Haskell supports type classes which can do the same thing. Or ocaml’s module types. Rust traits. Etc. I don’t think anyone will accuse those languages of being object oriented.
Frankly C++’s abstract superclasses have, in my opinion, the worst syntax of them all to support the same feature.
I really don’t see this kind of polymorphism as a weakness of non-OO languages. If you wanna play “who wore it better”, Haskell and rust get my vote. Rust traits are strictly more powerful than Java interfaces since you can say things like impl MyTrait for T where T: Iterator<…> {}
It doesn't. Inheritance is about defining interfaces. It doesn't give you runtime swapability or anything like that; you need to wire that up yourself.
For example, here[0] I have a concise way to define two logger implementations as functions. You could also do it by defining those two implementations as classes. In either case, the implementation is essentially provided by defining a constructor. In FP world, the constructor is the class.
Swapping is done elsewhere through some layer of indirection. So in OOP maybe you'd have a singleton class that hands out and can update the current logger. In FP, you might pass a `() => Logger` or `IO Logger` to your application, which does the same thing. You could define your FP "singleton" as
Then you pass your getter to your main application, and your setter to whatever portion can do the setting (e.g. an HTTP handler). Your getter/setter might have more going on in practice (e.g. flushing and closing the old logger when swapping), but that's the basic skeleton. If you wanted, you could also put the two functions into a LoggerManager record type, and then it would be pretty much exactly the same as the OOP solution but without the incantations around classes and inheritance and "overriding" things.
Sure, it could handle that case. You'd have to write a `withSwappableLogger` function which produces a logger that listens for updates telling it where to log to in future.
https://hackage.haskell.org/package/bluefin
1. a logger that logs to stdout
2. a logger that logs to a file
The file logger also brackets the opening of the file so that if abnormal termination is encountered then the file handle is guaranteed to be cleaned up. This is similar to RAII.
I really like this solution! It's just programming against an interface, and then instantiating the interface in different ways. I think an solution using inheritance would be worse, because it would use a special language concept (inheritance) rather than a standard one (function definition).
Perhaps this is "OOP" and not "FP"? That's fine by me! But in that case I conclude Haskell is an excellent OOP language. (I already conclude that it's an excellent imperative language.)