Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Functional core, imperative shell. This seems to be a good approach, especially when you push pretty hard to stuff as much as possible into the functional core.

Writing tests is much easier and requires much less code, and bugs are much easier to track down (since you have less surface area of "what might have modified this object I now hold?").

Since a functional approach also goes well with single-responsibility-principle and composability, you can end up with multiple times more actual functions. They're all (ideally) very conceptually simple, but the larger number of more specialized functions means you have to work a lot harder on choosing meaningful names. And to write meaningful names of specific functions, you can end up with some quite long names. This is actually quite ok as it makes things much more readable, but some people have some strong negative reactions to seeing 4-6 word function names. But at least in this case (Java being the most famous counter scenario) each function probably has more real value as it's not just OO layer artefacts.



One thing that I have seen in the Julia ecosystem, where multiple dispatch is pervasive, is that function names can get away with only being verbs. Without dispatch, you often see an approximation by baking in (Hungarian notation) the name of a type into the function.

This is most clearly seen with conversation functions between types defined in different libraries. A collection class might have a method like "from_Array".


Related to this pattern:

I recently wondered why Casey Muratori (RAD Game tools, Handmade Hero etc.) seems to write C++ in a very C like fashion plus a few select features.

Here's a discussion: https://hero.handmade.network/forums/code-discussion/t/453-w...

One of the takeaways is that he prefers the simplicity of C but uses operator overloading for similar reasons as you describe. One example includes addition of vectors, which is certainly a thing you do often in game and graphics related code.


Rich Hickey calls these protocols. In Haskell or Rust you get them via traits.


> Functional core, imperative shell.

You can find this pattern in a lot of places if you are a little bit flexible with your definition of "functional".

For example, why would we not consider some combination like SQL and PHP to be hinting at this exact same kind of archetype? You've got a crusty outer shell of yucky hackarounds (PHP) that talks to this (ideally) well-normalized & clean data store (SQL). Assuming you "stuff as much as possible" into core part of the solution, you could swap the outer shell for anything you desire without much headache.


PHP is more like “imperative core, functional shell”. After all, a PHP request is a pure stateless function call. The only way it can modify state anywhere is with IO (eg store something in the database, write something to the session store, etc) and all data you accumulate in your PHP code gets purged after each request.


Throwing away what you know with every request is orthogonal to being functional. You really have to squint and turn your head sideways to think of PHP+SQL as being functional, even though PHP is stateless.

From the outside it's stateful, because well you are interacting with a DB.

From within the PHP codebase, it's imperative and requires discipline and extended knowledge of any API you call to understand whether something mutates.

The only time you can view it as actually stateless and somewhat functional is when you test or debug an integration from idempotent requests to responses without interleaving mutations.


They specifically said PHP, not PHP + SQL.


> From the outside it's stateful, because well you are interacting with a DB.

This holds for a Haskell webapp too.


And SQL is all about mutation... functional take on the database would be something like Datomic.


That's a hard disagree. The absolute VAST MAJORITY of SQL complexity is in the select queries (joins, groups, aggregation, window functions, cursors, filtering, procedures...), which are mostly not about mutation.


You’re right about SQL the language, but I think the role of MySQL in a PHP application is much more to manage state than to provide interesting SELECT queries. The popularity of ORM in this world shows that what 95% of applications are looking for is native-ish collections that can be mapped to disk and updated concurrently. PHP’s shared nothing architecture means it must externalize this responsibility, and MySQL/Postgres are the customary choices.

With actual SQL, most people don’t get beyond JOIN, which they mainly need in order to deserialize nested or pointer structures from normal form. I learned aggregation, grouping, and windowing working with Spark and Hive on warehouse replicas of my DBs/topics to troubleshoot and analyze my stuff. Never used one in an actual request handler.


IMO you need to see how the SELECT queries are 100% functional to understand properly the power of it which is based on its relational algebra and the closure property of that algebra. Then the basic relational algebra was augmented with a lot of stuff but it's still functional in the end.


This is my perspective as well. I look at SQL as a defacto functional-relational programming language precisely as advertised in Out of the Tar Pit - Chapter 9.


Mutation is fine in pragmatic functional programming and IMO is not the defining feature what makes it so good for industrial use cases. Functional programming must allow easy immutable programs, and you can do that in SQL by never erasing anything (which will require a bit more bookkeeping for sure).


I don't think it is "all about mutation". Indeed it has create/update/delete, but there are a ton of functional capabilities. So much of what SQL offers is related to data shaping. You can have a huge read-only data store and do amazing things with it.


SQL is a weird beast. Very much on the contrary, the language has a relational core (note that functional is a special case of relational), at least in principle, but it’s been grafted with so many imperative-style hacks over time.


This approach works very well. Side effects are pushed out of core and no mocks are required for end to end tests. It's quite hard to "sell" to fellow devs, but when it clicks, the code is very clean.


Can anyone explain why not "functional core, functional shell"? Is this partly because the shell has to mostly interact with things like UI APIs, file system APIs, network APIs and database APIs that are usually implemented in an imperative way already so you'd make your life harder this way? What if the APIs were written in a functional style?

For example, I find in TypeScript, most libraries and the language itself are not written with immutability in mind so there's constant gotchas. So while I strongly prefer immutable data structures, going against the grain to use immutability everywhere usually isn't worth the frustration. That for me doesn't mean immutability is a bad idea though, it's more that the libraries and languages support is lacking.


> Can anyone explain why not "functional core, functional shell"? Is this partly because the shell has to mostly interact with things like UI APIs, file system APIs, network APIs and database APIs that are usually implemented in an imperative way already so you'd make your life harder this way? What if the APIs were written in a functional style?

Fundamentally the user isn't functional - if you ask them what they want to do they'll say different things at different times. You can have a 100% purely functional programming language that works like a calculator (with no memory) - the user can put in expressions and the language will evaluate them - but generally users want their language to be able to do things that are non-idempotent and non-time-symmetric and so on. You can, and should, push that part to the edge, but it needs to be there somewhere.


What you say is true, but FP has many different ways of dealing with user input, environment, IO, that are no more complex than imperative procedures (arguably the FP equivalent is simpler).


Such as? Haskell-style IO types have no real denotational semantics (they don't even have a notion of equality), they're useful for jamming imperative instructions into an expression model but they don't actually make them any less imperative, and the only other models of user input I've seen are even more obtuse and harder to work with. If something can only be understood operationally, representing it as an imperative procedure is probably the least bad approach IME (and diving into a "the C language is purely functional" style tarpit is far worse; look at SBT for where that leads).


Not an issue with F#. It’s trivial to exit ’purely functional’ domain, write it like ’pythonic C#’ and still end up with a program that is correct out-of-the-box.


1) IMO as a community we still haven’t quite figured out the best abstractions for functional UIs. Various FRP and Elm-like libraries are good, but they have drawbacks.

2) Not all frontends have functional APIs, you may be stuck writing a lot of FFI glue code.

3) Front end code often gets pushed to devs with different experience and skill sets, in my experience they’re probably less likely to have functional or hard CS backgrounds.

4) Harder recruiting if all the devs need to be proficient with FP.

I personally don’t find any of these reasons compelling enough to negate all of the benefits of FP.

Answers saying FP isn’t suited for IO or for interaction with users are rhetorically interesting but contradict my experience.


Your comments suggest a frontend perspective. But on the backend there's a ton of code (including business logic, which theoretically should be the most important bit).

I wouldn't be qualified to argue the merits of FP on frontend, but on the backend it really seems to have value.


The shell is where you put all your side effects. Things like user input or writing to files.

That way you can use pure functions on the inside. Pure functions don't allow for side effects.


Pure functions can't perform side effects, but they can describe them.

For a functional shell, your program's entry point would be composed of pure functions describing effectful actions to take. The necessary computations get bubbled up to the entry point during evaluation, at which point the runtime / compiled program executes them.

(At least, this is the abstracted perspective you should view the purely functional source code from. The final program itself in practice is, of course, effectful throughout).


Why does "imperative" imply "side effects" here though? Pure functional languages have ways of modelling side effects.


They do but it's a bit of a square peg round hole. It's just easier not to use things like the IO monad.


It's also fun to note that the IO monad in some perspectives is an abstraction for "functional core, imperative shell" moving as much of the imperative "order matters" computations to an outer "shell". The IO monad in some ways is the way of mathematically representing the "shell" of an application.


The problem with monads is that they're a poor abstraction. Need to output: WriterMonad. Need input: ReaderMonad. Need to store something: StateMonad. Local mutation: MonadST. IO/global mutation: MonadIO. Transactions: MonadSTM. And to combine them: a stack of monad transformers. How delightful!

This is why there's now more focus on algebraic effects as an alternative for keeping a functional core, imperative shell. Monads/transformers are just an ugly solution that even hardcore functional programmers don't want to use.


Sounds more like an unfamiliar peg than an inappropriate one, IMO.


Yes, that may well be the case.


Yeah, it’s the ecosystem, not the paradigm.

In .net, most UI stuff is easy on C# and semi-painfull on F# because of limited tooling and examples. I don’t see any theoretical reason why a usable functional language could not be used everywhere. The ecosystem maturity and tooling are the limiting factors.


Your functional core also includes an imperative core inside it. Most of your “pure” functions modify the process page table and GC roots. Basically all the runtime is imperative. Finally some of your functions are written in something equivalent to the ST monad to have reasonable performance.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: