8 ms·
The author brings up the basically the canonical example of where OOP style design shines, and where functional programming will falter. The simple truth howev
by whizzter 2y ago
The author brings up the basically the canonical example of where OOP style design shines, and where functional programming will falter.
The simple truth however is that overly going into either functional or OOP camp will hurt because strict adherence becomes subscribing to a silver-bullet.
The middle road is simply a better engineering option, use a practical language that supports both paradigms.
Keep data transforms and algorithmic calculations in functional style because those tend to become hot messes if you rely overly on mutation (even if there is performance gains, correctness is far far easier to get right and write tests for with a functional approach), then there are other concerns where an OOP derived system with inheritance abstractions will make things easier.
- mrkeen 2y agoSemi-mutable, semi-private, semi-nullable, semi-determistic, and semi-statically-typed. Best of both camps.
- throwawaymaths 2y agoHow do functional styles falter? Erlang's telemetry/logger systems are absolutely fantastic.
- whizzter 2y agoWhile Erlang itself is pure, in practice it seems like a lot of practical mutation and state issues are tucked in other processes. Nothing wrong with it and it sidesteps the purity issues that the Haskell example in the article presents.
- throwawaymaths 2y agoIs this no true scottish languaging on what constitutes fp? More to the point with the original article, erlang is assuredly not OOP in the modern sense (no matter how much joe Armstrong tried to rope java shops in by saying that actors are actually the true objects) Also: > Erlang itself is pure Very not true. erlang is extremely impure fp and that's a big part of what makes it what it is.
- whizzter 2y agoRight, and that's my point about practical languages that promotes a functional style being a better choice. The requirement for purity in Haskell (due to lazy evaluation) makes anything but purity a problem, it seems sometimes that just because things are possible to do purely it's the only way to do things according to adherents.
- zelphirkalt 2y agoIn the mainstream sense of what people understand OOP to be, yes, but actually Joe had Alan Kay on his side for the claim, that Erlang might be the most OO language. It is even recorded on video about a dialog they had on stage. What speaks for it is the focus on passing messages to other actors, which act as if they are separate little computers. All very similar to Kay's descriptions of OOP.
- tome 2y agoThe supposed problems in the Haskell example presented in the article have nothing to do with purity. Logging is by definition an effect. Haskell supports effects just fine. (I'd personally say better than any other language I know.)
- zelphirkalt 2y agoI think it was Erik Meijer (or was it the actor systems guy?) or so, who said in one of his videos about FP or programming languages, that as soon as you have another process, that process acts like a store for state, which can be mutated etc.. So it seems natural, that in a language like Erlang some mutable things are outsourced into separate processes. Whether knowingly or not.
- pseudonamed 2y agoThe middle of the road option in a language like Clojure is almost always functional with a small amount of mutable state. Programming styles are just that, style; Language =/= style.
- heresie-dabord 2y agoLanguage and "style" can be hard to disassociate, though. Unless that kind of flexibility is part of the design ethos of the language. The word "style" generally means a superficial characteristic of appearance, but "style" in a discussion of programming languages could mean the appearance of the language, the loop/state paradigm, or the design ethos of the language itself. For example, the design ethos of Java is entirely bound to OOP. Python on the other hand has a celebrated syntactic style, an OOP foundation, and a flexible loop/state paradigm. Python has flaws, of course. But its development community has paid attention to syntactic style and to the loop/state paradigm. Java became a successful corporate language; Python entered the corporation because it has become a popular language.
- mattgreenrocks 2y agoI'm less an OOP fan and more an extreme late-binding fan. It is a really nice lever to use when you need it. You don't necessarily need a language built for it if you have sufficient infrastructure in place (such as dependency injection). That said, FP is a great default for most code, and I try not to write any new code in languages that don't support algebraic data types. You can express things much more precisely with them. Languages that let you use both styles (Kotlin/Swift) are probably my favorite, despite using Haskell in production at work. Haskell punishes needing to use mutability too harshly IMO.
- bitwize 2y ago"Late bind all the things" is pretty much what Alan Kay's idea of OO was. In particular it means that methods are invoked by name and selected based on message name by the receiving object. Any object can be sent any message; how that message is handled is determined by the object at invocation time. No vtbls or other such mechanisms used by statically typed OO languages are involved. Kay has spoken positively, for instance, of Erlang, which has nothing like Smalltalk's OO model.
- DeathArrow 2y agoBut late binding doesn't have performance implications? If the compiler doesn't know the type it can't do optimizations.
- TeMPOraL 2y agoIt has, but that can be overcome by thick enough runtime. My experience is with Common Lisp, which has a quite sophisticated object system (CLOS) with a metaobject protocol. Theoretically, pretty much everything there can be altered on the fly at runtime - classes and methods can be added, modified and deleted, inheritance hierarchy changed, method invocation rules arbitrarily altered, etc. In practice, efficient implementations like SBCL (which tries to eagerly compile everything down to machine code, including at runtime) tend to have a lot of logic for keeping track how how things currently are. 99.9% of the time, the current shape of the object model is fixed, so it's kept optimized in advance. For example, calling (my-method my-obj) can do anything, depending on what my-method, my-obj, (class-of my-obj), etc. are at the moment - however both you and the runtime know that, right now, my-method is only defined on class-foo, which uses standard object model rules, so the runtime ensures the call is just a fixed-address jump. Define my-method for another class, the runtime will make calls to my-method a simple lookup on a tag (or something equivalent). Mess with the method combination order, or class definitions, and the runtime will redo its own book-keeping and keep the calls as efficient as possible. So you still pay for the flexibility - but mostly just-in-time, in the 0.1% of the time you invalidate some optimization, forcing the runtime to re-optimize itself. I don't have as much experience with Smalltalk, but I hear that the story there is similar - you usually have a fat runtime with gnarly, stateful internals that re-optimizes itself on the fly to keep the flexibility performant. It's a nice benefit of image-based languages, where the compiler is an integral part of the runtime.
- bob1029 2y ago> The middle road is simply a better engineering option, use a practical language that supports both paradigms. Agreed - I would get more specific with this too. Which arrangement makes more sense: 1. A solution that has an OOP outer shell hosting an FP inner core 2. A solution that has an FP outer shell hosting an OOP inner core I argue that 1 makes way more sense - I look at the OOP/procedural code as the foundation layer upon which the FP code can be executed. This firewalls the messy outside world from the pure maths. For me this would be entry points like BusinessLogic.ExecuteRules() after having externally prepared all of the data contexts for execution. The results of this are then processed again by the external OOP code for downstream handling (writing to database, responding to web client, etc). The other way around feels nonsensical to me.
- zelphirkalt 2y agoMutation is infectious. It will take great care to have an OOP core and outer FP shell. The OOP core might have to become completely threadsafe and use mutexes and such, to be reliable. This might be more painful than sticking to either one paradigm.
- lou1306 2y agoMaybe OOP is a strong word, but it would make absolute sense to have an imperative core if you absolutely need the highest possible performance, or if the OOP/imperative core is pre-existing and you want to "regulate" access to it through the affordances of FP.
- TeMPOraL 2y ago> The other way around feels nonsensical to me. Wonder if that's because you've learned the pattern - "functional core, imperative shell"? I feel it's not a coincidence you used the terms "shell" and "core" in your comment :). Myself, I'm very much in favor on a theoretical basis. In practice, I've got bitten by this a couple times - keeping a purely functional core didn't play nice with the modularization and testing setup we had, using C++17 with CMake and GTest/GMock. I blame this on my inexperience, filing off sharp corners as I go along. I'd sure could use a detailed study of how to apply this pattern in modern C++, on an application scale, taking account the real tradeoffs - like how to "firewall" the core from outside mess, and allow tests to poke through that firewall, and not end up in weird linking hell, and not blow a single module into 10 tiny ones, and keep the whole thing debuggable in practice[0]. One thing I occasionally do even in the "FP inner core" is apply the idea I picked up from Clojure's "transient" data structures: there is no difference between purely-functional, immutable, referentially transparent code, and imperative code carrying mutable state, if no one on the outside can tell. Sometimes it's just easier to make a typical imperative procedural or OOP solution and isolate it in a functional interface. -- [0] - I used to like point-free coding; especially with functional style, it makes for nice, readable, highly-expressive one-liners. But each time I had to debug such code, I ended up regretting it. Now I'm starting to favor more explicit steps with intermediary variables storing results, simply to have good targets for potential breakpoints/tracepoints, and because such code is more convenient to step through. You could say I'm only doing it because the tools I use are limited, but frankly, almost all of "readable code" principles and even the shape of modern languages are driven by limitations of the tools we use, so ¯\_(ツ)_/¯.
- valcron1000 2y ago> The middle road is simply a better engineering option, use a practical language that supports both paradigms. I recommend reading "The curse of the excluded middle" paper by Erik Meijer on why this approach does not work.
- red_admiral 2y agoFunctional programming has found its way into Java, where it works ok for some tasks; scheme/kotlin might advertise themselves as FP in the sense of "even more FP-ish than Java" but they're really mixed paradigm too, and not just because of backwards compatibility with other JVM libraries. We've made the middle road work. It is not the magical 10x speed upgrade that some managers hoped for if you just sprinkle some FP on top of the project like icing, but it works quite well in practice because you have the right tools for dealing with different kinds of subproblems. Meijer's paper is what I would call "FP maximalist". (Or "Haskell maximalist", as even OcaML manages without hiding all IO away in monads.) It's a defensible opinion to have, but doesn't have anything to do with the work environments that I have experience with.
- raincole 2y agoIt was from 2014. In the past decade, we've seen "the middle way" has won. - Every mainstream OOP languages now support passing functions as parameters. - Optional type annotation became mainstream, e.g. Typescript and modern Python.
- the_af 2y ago> Optional type annotation became mainstream, e.g. Typescript and modern Python. It became mainstream indeed but, in my opinion, it's still a mess. Optional type annotations seem like the worst of every possible world to me, and it leads to unprincipled [1] developers to just not use them or use them so inconsistently it doesn't matter and we revert to "no type annotations at all". Just my experience, of course. ---- [1] which, in the wild, is the most common type of developer. And really, features should help with the most common developer, not the most principled ones.
- ndriscoll 2y agoThe example is a single-abstract-method class, i.e. a lambda function. You can define it as `type Logger = (String, Severity) => IO ()` `def empty: Logger = (_, _) => IO.unit` `def aboveSeverity(l: Logger)(minsev: Severity): Logger = (s, sev) => if sev >= minsev l(s, sev) else IO.unit` Or Curried if you like. In Scala, you might put these on a companion object so that you can give Logger.empty to your tests, etc. You don't need the type to remember which type of logger you had. The only thing you care about is that you can call it.
- tome 2y agoThis 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 https://hackage.haskell.org/package/bluefin 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.) {-# 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)) } )
- throwawaymaths 2y agoThat'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.
- librasteve 2y agohttps://raku.org https://raku.org is a great language if you want all the paradigms in a smooth way
- kamaal 2y agoHow is Perl 6/Rakudo coming along these days? I mean in terms of overall spec compliance and production use cases. Im guessing larger adoption would still be low unless Rakudo can run CPAN modules as is.
- librasteve 2y agoRaku has a healthy and stable community that is making steady progress in developing the language. Joint us on IRC or Discord (see https://raku.org https://raku.org for details) to get a feel for the activity level. For me it is an opportunity to hang out wiht very deep knowledgable compiler core folk and I have learned tons. The big effort going into v.6.e (ie the 5th major release of Perl6 == 6.e) is Raku AST which is now in PREVIEW and is a big boost for writing Slangs (Raku sub languages) since you can now use the built in Grammar parsing to generate AST code - and is the precursor to macros and improved JIT optimisation. The Raku spec is actually the ROAST test suite https://github.com/Raku/roast https://github.com/Raku/roast and Raku has been compliant to this since 6.c which was the first production release. Raku can run Perl5 CPAN modules (Inline::Perl5) and Python modules (Inline::Python) out of the box and it has deep FFI C NativeCall facilities. There are also a couple of Raku + Rust exemplars such as Dan::Polars (note: I am the author). Raku also has some very nice modules - Red is an ORM that leverages traits to make OO/ORM seamless, Cro is a web server framework that uses concurrent features such as Supplies to enable pluggable middleware (think WebSockets). As to larger adoption, I would say that this is still slow - there are some reputational concerns around the Perl to Raku transition and some points have been made about slow execution speed (although I would say that Raku is no slower than Python / Ruby were at this point in the development curve and of course there is no GIL limitation and many built in features for multi core such as hypers). At the moment, I would say the "killer app" for Raku is the use of Grammars in conjunction with LLMs (language meets language) - take a look at the work of antononcube for some great use cases... https://raku.land/zef:antononcube/LLM::Functions https://raku.land/zef:antononcube/LLM::Functions
- Nathanael_M 2y agoWould you have any advice or resources for learning paradigms and practical paradigm usage?
- piyush_soni 2y ago> Keep data transforms and algorithmic calculations in functional style What annoys me greatly though is kids coding various 'fancy' functional paradigms for data transformation without realizing their performance implications, still thinking they've actually done the 'smarter' thing by transforming it multiple times and changing a simple loop to three or four loops. Example : Array.map.filter.map.reduce. Also when talked about it, they have learned to respond with another fancy term : "that would be premature optimization". :|
- Gunax 2y agoAren't those all linear operations?
- piyush_soni 2y agoYes, wrote quickly without thinking. Even if it doesn't change the complexity, it's still three or four times the operations.
- kosmozaut 2y agoWhy would your example be O(n³)?
- piyush_soni 2y agoOh yes, sorry I meant to write 3 * O(n) which though doesn't change the order is still three times the operations. The example I was remembering was doing filters 'inside' maps.
- deleted 2y ago[deleted]
- ryandv 2y agoSo... O(n)? Leaving aside the fact that "3 * O(n)" is nonsensical and not defined, recall f(x) is O(g(x)) if there exists some real c such that f(x) is bounded above by cg(x) (for all but finitely many x). Maybe you can say that g(x) = 3n, in which case any f(x) that is O(3n) is really just O(n), because we have some c such that f(x) < c(3n) and so with d = 3c we have f(x) < dn. It's not the lower-order terms or constant factors we care about, but the relative rate of growth of space or time usage between algorithms of, for example, linear vs. logarithmic complexity, where the difference in the highest order dominates any other lower order terms or differences. What annoys me greatly is people imprecisely using language, terminology, and/or other constructs with very clearly defined meanings without realizing the semantic implications of their sloppily arranged ideas, still thinking they've done the "smarter" thing by throwing out some big-O notation. Asymptotic analysis and big-O is about comparing relative rates of growth at the extremes. If you're talking about operations or CPU or wall clock time, use those measures instead; but in those cases you would actually need to take an empirical measurement of emitted instruction count or CPU usage to prove that there is indeed a threefold increase of something, since you can't easily reason about compiler output or process scheduling decisions & current CPU load a priori.
- throwaway2037 2y agoI like your post. There is a long YouTube video of a dev doing a long breakdown about why FP "lost" to OOP. In reality, it didn't lose...or rather, it won by using the backdoor. OOP integrated many FP features into their own languages. As a result, you can do lots of FPish coding in modern OOP languages.
- davedx 2y agoHow on Earth did FP "lose"? Most systems I work on these days use TypeScript, it's taken over huge parts of the industry, and OOP in TS is like an after thought that most people forget is even there at all. All the TS codebases I work on are 99% functional, with most JS/TS devs leaning into it more rather than less.
- josephg 2y agoExcel is still arguably the world’s most popular programming language. And excel is much more functional than imperative.
- throwaway2037 2y agoBut VBA is OOP/imperative. And most Excel apps that I know use a combination of FP in the sheet, and OOP/imperative in the VBA.
- josephg 2y agoI suspect VBA is used in a tiny percentage of all spreadsheets.
- the_gipsy 2y agoElm has taught me that pure FP can work beautifully. But it didn't quite catch on, which says something (but not necessarily that this approach is wrong), and it's also clealry not suited for every type of programming (e.g. systems programming or other low level stuff). Pure OOP aka Java has been a mistake, even though it's probably the most popular.
- Nursie 2y ago> Pure OOP aka Java has been a mistake, even though it's probably the most popular. I mean, Java is hardly 'pure' OOP and these days contains a lot of functional type features. I've come to the opinion that sticking to paradigms over practicality is often the problem, and a language that helps you get things done regardless is preferable
- the_gipsy 2y agoJava was hardcore "everything is an object from a class" and basically forced inheritance for most dynamic dispatch for decades. It doesn't matter how much FP candy they slap on top of it now, too late, it will always be stuck in a legacy world of crappy inheritance and everything-nullability. Rust actually is in the sweet spot between both worlds with the best from each side.
- Nursie 2y agoI think whether it matters or not is very much your opinion. And whatever your opinion, Java was never pure OOP, in fact that was an early criticism of the language from OOP purists back in the day. It’s still used in a lot of places and having these features available makes that a much nicer experience. Whether I would choose it for a new project… maybe not. But we don’t always get that choice either.
- the_gipsy 2y agoYes smalltalk and whatnot are supposed to be "real OOP". But today, OOP is just Java's model for most people. Erlang probably got the message passing thing right, but it just doesn't make sense to declare it OOP, today. That is my opinion, that it makes sense to talk about OOP in those terms.
- ryandv 2y agoThe characterization of FP-style approaches to the problem makes me question that it is a canonical example at all; rather I would regard it as a strawman of FP style, or otherwise not idiomatic or awkwardly designed. If Haskell is indeed unergonomic for the purpose of supplying multiple different logger implementations, it would seem that it would also be unergonomic for the purpose of supplying multiple different exception/error handling implementations. But... it's not? See `catchE` [0] from Control.Monad.Trans.Except: catchE :: Monad m => ExceptT e m a -> (e -> ExceptT e' m a) -> ExceptT e' m a ... where the callsite-provided exception handler is the second parameter to the function, which can do basically anything (especially if the monad m is MonadIO). Hardly unergonomic or overcomplicated in my view, and no effects algebras or even type declarations in sight. There is already a canonical example and it is the "Expression Problem," [1], where in FP "you optimize for extensibility of the number of operations in your model, at the cost of reduced developer ergonomics when attempting to extend the number of types you can operate upon. In the latter, object-oriented approach, you have the inverse tradeoff." [2] [0] https://hackage.haskell.org/package/transformers-0.6.1.2/docs/Control-Monad-Trans-Except.html#v:catchE https://hackage.haskell.org/package/transformers-0.6.1.2/doc... [1] https://wiki.c2.com/?ExpressionProblem https://wiki.c2.com/?ExpressionProblem [2] https://news.ycombinator.com/item?id=41450851 https://news.ycombinator.com/item?id=41450851
- epolanski 2y agoI still feel like one missing crucial point is that you can mix both styles effectively. Scala, Elixir and some lisps are languages that make this nice. TypeScript has libraries like effect-ts where you can leverage OOP where it makes sense too.
- f1shy 2y agoFor me is hard to understand the opposition of OOP to functional. They are just not exclusive one of each other, neither is exclusive with all other paradigms.
- mejutoco 2y agoOOP puts state and behaviour togethet (tightly coupled) in objects. FP, in a language with expressive types, has well-defined data-only structures (types) and functions that operate on those structures, so state representation and behavior are separated. For me, OOP can be done right, but FP is intrinsically better for correctness (it incentivizes correctness)
- whstl 2y agoOOP puts data and behavior together in objects. Which is 100% compatible with functional programming. It is mutability that is incompatible with FP, but mutability is a property of imperative programming, not of OOP. It is even possible to have 100% purely functional Java, for example, if you just eschew mutability.
- baranul 2y agoProblem seems to come more from languages that force OOP usage and force their libraries to use it, versus it being really optional. Every solution doesn't require a class and everything in existence should not be defined as an object.