11 ms·
Simplify your code: Functional core, imperative shell
- foobarian 11mo agoThis sounds to me like the old hexagonal architecture [1] [1] https://en.wikipedia.org/wiki/Hexagonal_architecture_(software) https://en.wikipedia.org/wiki/Hexagonal_architecture_(softwa...
- Twisol 11mo agoYep! I remember the phrase "functional core, imperative shell" being popularized by Gary Bernhardt in ~2012 [0][1]; in his talk Boundaries [0] (around 31:00), he even mentions "hexagonal architecture" by name. [0]: https://www.destroyallsoftware.com/talks/boundaries https://www.destroyallsoftware.com/talks/boundaries [1]: https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell https://www.destroyallsoftware.com/screencasts/catalog/funct...
- ericmcer 11mo agoFamously how Doom was written and maybe part of why it was ported to so many platforms. Hex is kind of a PITA for ground up projects, but if you are doing something where you know multi-platform/cloud/device whatever is important it is cool.
- hinkley 11mo agoBertrand Meyer suggested another way to consider this that ends up in a similar place. For concerns of code complexity and verification, code that asks a question and code that acts on the answers should be separated. Asking can be done as pure code, and if done as such, only ever needs unit tests. The doing is the imperative part, and it requires much slower tests that are much more expensive to evolve with your changing requirements and system design. The one place this advice falls down is security - having functions that do things without verifying preconditions are exploitable, and they are easy to accidentally expose to third party code through the addition of subsequent features, even if initially they are unreachable. Sun biffed this way a couple of times with Java. But for non crosscutting concerns this advice can also be a step toward FC/IS, both in structuring the code and acclimating devs to the paradigm. Because you can start extracting pure code sections in place.
- Jtsummers 11mo agoCommand-Query Separation is the term for that. However, I find this statement odd: > having functions that do things without verifying preconditions are exploitable Why would you do this? The separation between commands and queries does not mean that executing a command must succeed. It can still fail. Put queries inside the commands (but do not return the query results, that's the job of the query itself) and branch based on the results. After executing a command which may fail, you can follow it with a query to see if it succeeded and, if not, why not. https://en.wikipedia.org/wiki/Command%E2%80%93query_separation https://en.wikipedia.org/wiki/Command%E2%80%93query_separati...
- jonahx 11mo ago> Why would you do this? Performance and re-use are two possible reasons. You may have a command sub-routine that is used by multiple higher-level commands, or even called multiple times within by a higher-level command. If the validation lives in the subroutine, that validation will be called multiple times, even when it only needs to be called once. So you are forced to choose either efficiency or the security of colocating validation, which makes it impossible to call the sub-routine with unvalidated input.
- Jtsummers 11mo agoPerhaps I was unclear, to add to my comment: hinkley poses this as a fault in CQS, but CQS does not require your commands to always succeed. Command-Query Separation means your queries return values, but produce no effects, and your commands produce effects, but return no values. Nothing in that requires you to have a command which always succeeds or commands which don't make use of queries (queries cannot make use of commands, though). So a better question than what I originally posed: My "Why would you do this?" is better expanded to: Why would you use CQS in a way that makes your system less secure (or safe or whatever) when CQS doesn't actually require that?
- hinkley 11mo agoThe example in the wiki page is far more rudimentary than the ones I encountered when I was shown this concept. Trivial, in fact. CQS will rely on composition to do any If A Then B work, rather than entangling the two. Nothing forces composition except information hiding. So if you get your interface wrong someone can skip over a query that is meant to short circuit the command. The constraint system in Eiffel I don’t think is up to providing that sort of protection on its own (and the examples I was given very much assumed not). Elixir’s might end up better, but not by a transformative degree. And it remains to be seen how legible that code will be seen as by posterity.
- rcleveng 11mo agoIf your language supports generators, this works a lot better than making copies of the entire dataset too.
- KlayLay 11mo agoYou don't need your programming language to implement generators for you. You can implement them yourself.
- akshayshah 11mo agoSometimes, sure - but sometimes, passing around a fat wrapper around a DB cursor is worse, and the code would be better off paginating and materializing each page of data in memory. As usual, it depends.
- hackthemack 11mo agoI never liked encountering code that chains functions calls together like this email.bulkSend(generateExpiryEmails(getExpiredUsers(db.getUsers(), Date.now()))); Many times, it has confused my co-workers when an error creeps in in regards to where is the error happening and why? Of course, this could just be because I have always worked with low effort co-workers, hard to say. I have to wonder if programming should have kept pascals distinction between functions that only return one thing and procedures that go off and manipulate other things and do not give a return value. https://docs.pascal65.org/en/latest/langref/funcproc/ https://docs.pascal65.org/en/latest/langref/funcproc/
- POiNTx 11mo agoIn Elixir this would be written as: db.getUsers() |> getExpiredUsers(Date.now()) |> generateExpiryEmails() |> email.bulkSend() I think Elixir hits the nail on the head when it comes to finding the right balance between functional and imperative style code.
- montebicyclelo 11mo agobulk_send( generate_expiry_email(user) for user in db.getUsers() if is_expired(user, date.now()) ) (...Just another flavour of syntax to look at)
- Akronymus 11mo agoNot sure I like how the binding works for user in this example, but tbh, I don't really have any better idea. Writing custom monad syntax is definitely quite a nice benefit of functional languages IMO.
- whichdan 11mo agoThe nice thing with the Elixir example is that you can easily `tap()` to inspect how the data looks at any point in the pipeline. You can also easily insert steps into the pipeline, or reuse pipeline steps. And due to the way modules are usually organized, it would more realistically read like this, if we were in a BulkEmails module: Users.all() |> Enum.filter(&Users.is_expired?(&1, Date.utc_today())) |> Enum.map(&generate_expiry_email/1) |> tap(&IO.inspect(label: "Expiry Email")) |> Enum.reject(&is_nil/1) |> bulk_send() The nice thing here is that we can easily log to the console, and also filter out nil expiry emails. In production code, `generate_expiry_email/1` would likely return a Result (a tuple of `{:ok, email}` or `{:error, reason}`), so we could complicate this a bit further and collect the errors to send to a logger, or to update some flag in the db. It just becomes so easy to incrementally add functionality here. --- Quick syntax reference for anyone reading: - Pipelines apply the previous result as the first argument of the next function - The `/1` after a function name indicates the arity, since Elixir supports multiple dispatch - `&fun/1` expands to `fn arg -> fun(arg) end` - `&fun(&1, "something")` expands to `fn arg -> fun(arg, "something") end`
- taeric 11mo agoThis works right up to the point where you try to make the code to support opening transactions functional. :D Some things are flat out imperative in nature. Open/close/acquire/release all come to mind. Yes, the RAI pattern is nice. But it seems to imply the opposite? Functional shell over an imperative core. Indeed, the general idea of imperative assembly comes to mind as the ultimate "core" for most software. Edit: I certainly think having some sort of affordance in place to indicate if you are in different sections is nice.
- agentultra 11mo agowhispers in monads It can be done "functionally" but doesn't necessarily have to be done in an FP paradigm to use this pattern. There are other strategies to push resource handling to the edges of the program: pools, allocators, etc.
- taeric 11mo agoRight, but even in those, you typically have the more imperative operations as the lower levels, no? Especially when you have things where the life cycle of what you are starting is longer than the life cycle of the code that you use to do it? Consider your basic point of sale terminal. They get a payment token from your provider using the chip, but they don't resolve the transaction with your card/chip still inserted. I don't know any monad trick that would let that general flow appear in a static piece of the code?
- garethrowlands 11mo agoI'm unclear what you're suggesting here. Are you suggesting you couldn't write a POS in Haskell, say?
- taeric 11mo agoMy idea here is that, in many domains, you will have operations that are somewhat definitionally in the imperative camp. OpenTransaction being the easy example. Can you implement it using functional code? Yes. Just make sure you wind up with partial states. And often times you are best off explicitly not using the RAI pattern for some of these. (I have rarely seen examples where they deal with this. Creating and reconciling transactions often have to be separate pieces of code. And the reconcile code cannot, necessarily, fallback to create a transaction if they get a "not found" fault.)
- zkmon 11mo agoI think it's just your way of looking at things. What if a FCF (functional core function) calls another FCF which calls another FCF? Or do we do we rule out such calls? Object Orientation is only a skin-deep thing and it boils down to functions with call stack. The functions, in turn, boil down to a sequenced list of statements with IF and GOTO here and there. All that boils boils down to machine instructions. So, at function level, it's all a tree of calls all the way down. Not just two layers of crust and core.
- skydhash 11mo agoFunctional core usually means pure functional functions, aka the return value is know if the arguments is known, no side effects required. All the side effects is then pushed up the imperative shell. You’ll find usually that side effect in imperative actions is usually tied to the dependencies (database, storage, ui, network connections). It can be quite easy to isolate those dependencies then. It’s ok to have several layers of core. But usually, it’s quite easy to have the actual dependency tree with interfaces and have the implementation as leaves for each node. But the actual benefits is very easy testing and validation. Also fast feedback due to only unit tests is needed for your business logic.
- deleted 11mo ago[deleted]
- bitwize 11mo agoI invented this pattern when I was working on a small ecommerce system (written in Scheme, yay!) in the early 2000s. It just became much easier to do all the pricing calculations, which were subject to market conditions and customer choices, if I broke it up into steps and verified each step as a side-effect-free, data-in-data-out function. Of course by "invented" I mean that far smarter people than me probably invented it far earlier, kinda like how I "invented" intrusive linked lists in my mid-teens to manage the set of sprites for a game. The idea came from my head as the most natural solution to the problem. But it did happen well before the programming blogosphere started making the pattern popular.
- deleted 11mo ago[deleted]
- socketcluster 11mo agoEven large companies are still grasping at straws when it comes to good code. Meanwhile there are articles I wrote years ago which explain clearly from first principles why the correct philosophy is "Generic core, specific shell." I actually remember early in my career working for a small engineering/manufacturing prototyping firm which did its own software, there was a senior developer there who didn't speak very good English but he kept insisting that the "Business layer" should be on top. How right he was. I couldn't imagine how much wisdom and experience was packed in such simple, malformed sentences. Nothing else matters really. Functional vs imperative is a very minor point IMO, mostly a distraction.
- benoitg 11mo agoI’d love to know more, do you have any links to your articles?
- CharlesW 11mo ago"Specific on the surface, generic underneath" (Medium paywalled): https://medium.com/tech-renaissance/generic-internals-specific-externals-95611ce1db13 https://medium.com/tech-renaissance/generic-internals-specif...
- xenophonf 11mo ago> While internal modules and libraries should be kept as generic as possible, external-facing components, on the other hand, are a good place to put business-specific domain logic. External-facing components here refer not only to views but also to any kind of externally-triggered handlers including external API endpoints (e.g. HTTP/REST API handlers). That goes against every bit of advice and training I've ever gotten, not to mention my experience designing, testing, and implementing APIs. Business logic belongs in the data model because of course the rules for doing things go with the things they operate on. API endpoints should limit themselves to access control, serialization, and validation/deserialization. Business logic in the endpoint handler—or worse, in the user interface—mixes up concerns in ways that are difficult to validate and maintain.
- semiinfinitely 11mo agothis looks like a post from 2007 im shocked at the date
- diamondtin 11mo agoI saw Gary posted his blog link on twitter, and I really like his article. I really didn't expect it to surface up at this moment (2025), and it's referred from a google blog. :shrug:
- vietvu 11mo agoMe too. Aren't we already doing this? This is the basic I have been taught first.
- semiquaver 11mo agoYeah, it’s based on an old post: https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell https://www.destroyallsoftware.com/screencasts/catalog/funct...
- mrkeen 11mo agoAnd "I call it my billion-dollar mistake. It was the invention of the null reference in 1965" is from 2009. Hopefully by 2045 these ideas will have gotten a little more traction.
- johnrob 11mo agoFunctions can have complexity or side effects, but not both.
- anttiharju 11mo agoAll pure functions have complexity?
- jackbravo 11mo agoReminds me of this clean architecture talk with Python explains this very well: https://www.youtube.com/watch?v=DJtef410XaM https://www.youtube.com/watch?v=DJtef410XaM
- diamondtin 11mo agodestory all software
- postepowanieadm 11mo agoSomething like that was popular in perl world: functional core, oop external interface.
- dominicrose 11mo agoWhen you're not relying on a compiler you just have to right good code. And it's easier if the code never has to change, only grow. I know a confident Perl programming who rarely changes his mind about anything. When he codes something he keeps it. I always feel like I have to "maintain" code so I usually get bored after 3k lines of code, but truth is code doesn't have to be maintained if we like it the way it is, which obviously includes all the functionality that comes with it.
- SafeDusk 11mo agoOne of the core design principles at https://github.com/aperoc/toolkami https://github.com/aperoc/toolkami
- BergAndCo 11mo agoSpam
- SafeDusk 11mo agoNot sure man, I specifically stated this in my README way before this post: https://github.com/aperoc/toolkami/blob/main/README.md#command-line-interface-cli https://github.com/aperoc/toolkami/blob/main/README.md#comma.... I mean it's not much, but the concept just resonates with me and I want to share it. Sad I can't share even simple opinion nowadays ...
- chairhairair 11mo agoThe for-loop is just better.
- wslh 11mo agoI don't really like the example (and it's from Google) because, beyond the general concept, it seems like the trigger for sending emails is calling bulkSend with Date.now() instead of the user actually triggering an email when it's really expired: user.subscriptionEndDate change to < Date.now().
- itsthecourier 11mo agothat's nice, so should I get all the db users and then filter them in app?
- lmm 11mo agoProbably. Or better yet move the code to run where the data is so you're not moving the data around.
- CharlieDigital 11mo agoI wrote our AI agents code with a functional core + imperative shell and I have to agree: this approach yields much faster cycle times because you can run pure unit tests and it makes testing a lot easier. We have tens of thousands of lines of code for the platform and millions of workflow runs through them with no production errors coming from the core agent runtime which manages workflow state, variables, rehydration (suspend + resume). All of the errors and fragility are at the imperative shell (usually integrations). Some of the examples in this thread I think get it wrong. db.getUsers() |> filter(User.isExpired(Date.now()) |> map(generateExpiryEmail) |> email.bulkSend This is already wrong because the call already starts with I/O; flip it and it makes a lot more sense. What you really want is (in TS, as an example): bulkSend( userFn: () => user[], filterFn: (user: User) => bool, expiryEmailProducerFn: (user: User) => Email, senderFn: (email: Email) => string ) The effect of this is that the inner logic of `bulkSend` is completely decoupled from I/O and external logic. Now there's no need for mocking or integration tests because it is possible to use pure unit tests by simply swapping out the functions. I can easily unit test `bulkSend` because I don't need to mock anything or know about the inner behavior. I chose this approach because writing integration tests with LLM calls would make the testing run too slowly (and costly!) so most of the interaction with the LLM is simply a function passed into our core where there's a lot of logic of parsing and moving variables and state around. You can see here that you no longer need mocks and no longer need to spy on calls because in the unit test, you can pass in whatever function you need and you can simply observe if the function was called correctly without a spy. It is easier than most folks think to adopt -- even in imperative languages -- by simply getting comfortable working with functions at the interfaces of your core API. Wherever you have I/O or a parameter that would be obtained from I/O (database call), replace it with a function that returns the data instead. Now you can write a pure unit test by just passing in a function in the test. I am very surprised how many of the devs on the team never write code that passes a function down.
- nickpsecurity 11mo agoGreat examples. We were taught to pass variables, scalar or compound, into API's. Most of us were never taught to pass functions. Even Python examples in trainings that look functional might not be. They put the function calls in as arguments. The beginner thinks the function returns some data, that would be in a variable, and they are implicitly passing that variable. Might as well, for readability, do the function call first to pass a well-named variable instead. That was my experience. That plus minimizing side effects in functions. I've yet to really learn functional programming where I'd think to pass a function in an API. What are the best articles or books for us to learn that in general or in Python?
- QuadmasterXLII 11mo agoI would argue that the real key is to have a distinct core and shell, and to hold the core to a much higher standard of quality than the shell. In this article, being "functional" is just serving as a proxy for code quality.
- ccortes 11mo ago> In this article, being "functional" is just serving as a proxy for code quality. It is not, it is being very specific about what it means and what it is referring to
- droningparrot 11mo agoHaskell practically encourages this style of programming. Any function that touches IO needs to wrap outputs with an appropriate monad. It becomes easier to push all IO out to the edges of your program and keep your core purely functional with no monads
- kaashif 11mo agoI wish that's what people did, some codebases I've seen are messes of monad transformer stacks the likes of which you've never seen. I mean, what if you want to do IO and have mutable data structures inside a do block? I'm afraid I'm going to have to prescribe you a monad transformer. Be careful of the side effects.
- kmeisthax 11mo agoMaybe JavaScript's colored functions[0] were trying to tell us something [0] https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
- lucifer153 11mo agoThis is same idea with onion architect in "Grokking Simplicity: Taming Complex Software with Functional Thinking Book by Eric Normand"
- metalrain 11mo agoI like the idea but the example doesn't make much sense. In what application would you load all users into memory from database and then filter them with TypeScript functions? And that is the problem with the otherwise sound idea "Functional core, imperative shell". The shell penetrates the core. Maybe some filters don't match the way database is laid out, what if you have a lot of users, how do you deal with email batching and error handing? So you have to write the functional core with the side effect context in mind, for example using query builder or DSL that matches the database conventions. Then weave it with the intricacies of your email sender logic, maybe you want iterator over the right size batches of emails to send at once, can it send multiple batches in parallel?
- edf13 11mo ago> In what application would you load all users into memory from database and then filter them with TypeScript functions? You’d be surprised! I have worked on a legacy PHP service which did something very similar
- bad_username 11mo agoI am surprised by this example, for the same reason. Generally, performance is a top cause of abstraction leaks and the emergence of less-than-beautiful code. On an infinitely powerful machine it would be easy and advisable to program using neat abstracrions, using purely "the language of" the business. Our machines are not infinitely powerful, and that is especially evident when larger data sets are involved. That's where, to achieve useful performance, you have to increasingly speak "the language of" the machine. This is inevitable, and the big part of the programmer's skill is to be able to speak both "languages", to know when to speak which one, and produce readable code regardless. Database programming is a prime example. There's a reason, for example, why ORMs are very messy and constitute such excellent footguns: they try to gap this bridge, but inevitably fail in important ways. And having and ORM in the example would, most likely, violate the "functional core" principle from the article. So it looks like the author accidentally presented a very good counterexample to their own idea. I like the idea though, and I would love to know how to resolve the issue.
- vivzkestrel 11mo agodb.getUsers() I am sorry what? Who in their right mind loads all users from the database and then filters out the expired subscription ones. Shouldn't the database query do this?
- ryangibb 11mo agoThe MirageOS project [0] is a great collection of functionality pure OCaml libraries that are useful outside of unikernels. I've used the DNS library with an effectful layer for various nameserver experiments [1]. [0] https://mirage.io/ https://mirage.io/ [1] https://ryan.freumh.org/eon.html https://ryan.freumh.org/eon.html
- sherinjosephroy 11mo ago[flagged]
- pjmlp 11mo agoAll nice ideas, that unfortunately don't get appreciated on the age of offshoring and vibe coding. Have to ship it non matter what.
- svat 11mo agoAnother good blog post that is IMO in the same vein: https://lambdaisland.com/blog/2022-03-10-mechanism-vs-policy https://lambdaisland.com/blog/2022-03-10-mechanism-vs-policy (“Improve your code by separating mechanism from policy”). This blends harmoniously with “functional core, imperative shell”—the "mechanism" code is the "functional core", and the "policy" code is the "imperative shell"—and also a little bit with John Ousterhout's idea in A Philosophy of Software Design of "deep modules" (in this context, don't put policy stuff, i.e. arbitrary decisions, inside the module).
- urxvtcd 11mo agoI have written a small system in Elixir adhering to FCIS. Not used to the approach, I was pretty slow and sometimes it felt like jumping through hoops set by myself, lol, but I loved it, the code was very clean, testable, and refactorable. Highly recommend it as an exercise, it was surprising just how much state and IO can be pushed out.
- procaryote 11mo agoI like the general idea, but unless you're assuming some very clever language or even more clever ORM that fixes things implicitly, wouldn't email.bulkSend(generateReminderEmails(getExpiredUsers(db.getUsers(), fiveDaysFromNow))); get all users and then filter out the few that will expire in 5 days, on a code level? That doesn't sound like it would scale
- bribri 11mo agoI agree. They could have picked a better example. Just db.getUsers() alone should set off alarm bells as soon as you see it.
- globular-toast 11mo agoIrrelevant. The "bad" code does it too. It's talking about something specific and not "let's fix all the problems with this code".
- procaryote 11mo agoIf you write one method poorly so you select too much and filter in a for loop, you just have a bad method you can fix. If you pick and recommend a pattern where filtering should happen separately from retrieving, all your code will be bad Give a man a fish / teach a man to fish, but bad.
- spoiler 11mo agoI think it's just a contrived example. They probably wanted to show more than a single thing composing in a very short post given it's from their Toilet series. Replace it with `getUsers(filters)` or even a specialised function, and it starts making more sense.
- ajusa 11mo ago(author here) It's exactly this - I do regret using "db" a bit now after reading all of the comments here, as it's taken away focus from the main point. But yes, the post had to fit on a single page, and I needed to pick something that most engineers would be familiar with.
- rockyj 11mo agoInterestingly, I have been harping on this for a while. Recently wrote a blog on how to separate business logic from infrastructure code and tie them together by composing functions together - https://rockyj-blogs.web.app/2025/10/25/result-monad.html https://rockyj-blogs.web.app/2025/10/25/result-monad.html I also see that lately "code quality" is the least concern of most (even software product) companies, just ask AI to write code in a single file / module / class - then launch feature and fix if you have to. I could see that in a few years things will be extremely messy (but who can say).
- kitd 11mo agoI never really got into Haskell in a big way, but one of the things I liked about the Haskell Wikibook [1] was how they presented Haskell code as being either in pure form or "do" form, and how the latter orchestrates the former, much as presented here. To a beginner like me not interested in monads etc, this was a very simple and explicit way of approaching coding in Haskell. [1] https://en.wikibooks.org/wiki/Haskell https://en.wikibooks.org/wiki/Haskell
- novoreorx 11mo agoWhile I largely agree with the philosophy, the example provided is not very practical. The code snippet `getExpiredUsers(db.getUsers(), Date.now())` is unlikely to occur in real-life scenarios. No one would retrieve all users and then filter them within the program. Instead, it should be `db.getExpiredUsers(Date.now())`. We should never be too extreme on anything, otherwise it would turn good into bad.
- doix 11mo ago> No one would retrieve all users and then filter them within the program. No one _should_ do that, but that's a common enough problem (that usually doesn't get found until code is running in production). I suspect with the rise of vibe coding, it's going to happen more and more.
- regularfry 11mo agoSometimes it's forced by using the wrong database in the first place, or the wrong data structure. It can be less pain to do a bit of post-processing in the application layer than to unpick either of those.
- _flux 11mo agoWith a good library you could do just that, by having the functions return only queries and then expand them to the actual values (by interacting with the DB) after applying the filtering to it?
- soulofmischief 11mo agoSo would you then have to do `getActualUsers(db.getUsers())` or `query(db.getUsers())`? Still smells like in such a case the developer avoids the complications of abstraction or OOP by making the user deal with it. That's bad API design due to putting ideology before practicality or ergonomics.
- bulatb 11mo ago
- lincpa 11mo ago[dead]
- fsmv 11mo agoGoogle writes articles like these every week and hangs them in the bathroom. It's meant to be a quick one page tip thing. That's why the example isn't super realistic, it has to be short. There's a link with more info at the top. I'm not sure why this one in particular made it to the front page of HN.
- smusamashah 11mo agoHow does it fit with Tell Don't Ask https://martinfowler.com/bliki/TellDontAsk.html https://martinfowler.com/bliki/TellDontAsk.html Or is it that the example in the article is a bit poor?
- crymer11 11mo agoFrom your linked article: > But personally, I don't use tell-dont-ask. I do look to co-locate data and behavior, which often leads to similar results. One thing I find troubling about tell-dont-ask is that I've seen it encourage people to become GetterEradicators, seeking to get rid of all query methods.