[HN Gopher] Simplify Your Code: Functional Core, Imperative Shell
       ___________________________________________________________________
        
       Simplify Your Code: Functional Core, Imperative Shell
        
       Author : reqo
       Score  : 107 points
       Date   : 2025-10-25 07:07 UTC (2 days ago)
        
 (HTM) web link (testing.googleblog.com)
 (TXT) w3m dump (testing.googleblog.com)
        
       | hinkley wrote:
       | Bertrand 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 wrote:
         | Command-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_separati...
        
           | jonahx wrote:
           | > 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 wrote:
             | Perhaps 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 wrote:
           | The 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.
        
             | Jtsummers wrote:
             | That's still not really answering my question for you,
             | which was less clear than intended. To restate it:
             | 
             | > The one place this advice falls down is security - having
             | functions that do things without verifying preconditions
             | are exploitable
             | 
             | My understanding of your comment was that "this advice" is
             | CQS. So you're saying that CQS commands do _not_ verify
             | preconditions and that this is a weakness in CQS, in
             | particular.
             | 
             | Where did you get the idea that CQS commands don't verify
             | preconditions? I've never seen anything in any discussion
             | of it, including my (admittedly 20 years ago) study of
             | Eiffel.
        
           | layer8 wrote:
           | In asynchronous environments, you may not be able to repeat
           | the same query with the same result (unless you control a
           | cache of results, which has its own issues). If some
           | condition is determined by the command's implementation that
           | subsequent code is interested in (a condition that _isn't_
           | preventing the command from succeeding), it's generally more
           | robust for the command to return that information to the
           | caller, who then can make use of it. But now the command is
           | also a query.
        
             | Jtsummers wrote:
             | > it's generally more robust for the command to return that
             | information to the caller, who then can make use of it. But
             | now the command is also a query.
             | 
             | You don't need the command to return anything (though it
             | can be more efficient or convenient). It can set state
             | indicating, "Hey, I was called but by the time I tried to
             | do the thing the world and had changed and I couldn't. Try
             | using a lock next time."                 if (query(?)) {
             | command(x)         result := status(x) //
             | ShouldHaveUsedALockError       }
             | 
             | The caller can still obtain a result following the command,
             | though it does mean the caller now has to explicitly
             | retrieve a status rather than getting it in the return
             | value.
        
               | layer8 wrote:
               | Where is that state stored, in an environment where the
               | same command could be executed with the same parameters
               | but resulting in a different status, possibly in
               | parallel? How do you connect the particular command
               | execution with the particular resulting status? And if
               | you manage to do so, what is actually won over the
               | command just returning the status?
               | 
               | I'd argue that the separation makes things worse here,
               | because it creates additional hidden state.
               | 
               | Also, as I stated, this is not about error handling.
        
       | rcleveng wrote:
       | If your language supports generators, this works a lot better
       | than making copies of the entire dataset too.
        
         | KlayLay wrote:
         | You don't need your programming language to implement
         | generators for you. You can implement them yourself.
        
         | akshayshah wrote:
         | Sometimes, 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 wrote:
       | I 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/
        
         | POiNTx wrote:
         | In 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 wrote:
           | bulk_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 wrote:
             | Not 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.
        
         | fedlarm wrote:
         | You could write the logic in a more straight forward, but less
         | composable way, so that all the logic resides in one pure
         | function. This way you can also keep the code to only loop over
         | the users once.
         | 
         | email.sendBulk(generateExpiryEmails(db.getUsers(),
         | Date.now()));
        
         | tadfisher wrote:
         | That's pretty hardcore, like you want to restrict the runtime
         | substitution of function calls with their result values? Even
         | Haskell doesn't go that far.
         | 
         | Generally you'd distinguish which function call introduces the
         | error with the function call stack, which would include the
         | location of each function's call-site, so maybe the "low-
         | effort" label is accurate. But I could see a benefit in
         | immediately knowing which functions are "pure" and "impure" in
         | terms of manipulating non-local state. I don't think it changes
         | any runtime behavior whatsoever, really, unless your runtime
         | schedules function calls on an async queue and relies on the
         | order in code for some reason.
         | 
         | My verdict is, "IDK", but worth investigating!
        
           | hackthemack wrote:
           | It has been so long since I worked on the code that had
           | chaining functions and caused problems that I am not sure I
           | can do justice to describing the problems.
           | 
           | I vaguely remember the problem was one function returned a
           | very structured array dealing with regex matches. But there
           | was something wrong with the regex where once in a blue moon,
           | it returned something odd.
           | 
           | So, the chained functions did not error. It just did
           | something weird.
           | 
           | Whenever weird problems would pop up, it was always passed to
           | me. And when I looked at it, I said, well...
           | 
           | I am going to rewrite this chain into steps and debug each
           | return. Then run through many different scenarios and that
           | was how I figured out the regex was not quite correct.
        
         | sfn42 wrote:
         | I would have written each statement on its own line:
         | 
         | var users = db.getUsers();
         | 
         | var expiredUsers = getExpiredUsers(users, Date.now());
         | 
         | var expiryEmails = generateExpiryEmails(expiredUsers);
         | 
         | email.bulkSend(expiryEmails);
         | 
         | This is not only much easier to read, it's also easier to
         | follow in a stack trace and it's easier to debug. IMO it's just
         | flat out better unless you're code golfing.
         | 
         | I'd also combine the first two steps by creating a DB query
         | that just gets expired users directly rather than fetching all
         | users and filtering them in memory:
         | 
         | expiredUsers = db.getExpiredUsers(Date.now());
         | 
         | Now I'm probably mostly getting zero or a few users rather than
         | thousands or millions.
        
           | hackthemack wrote:
           | Yeah. I did not mention what I would do, but what you wrote
           | is pretty much what I prefer. I guess nobody likes it these
           | days because it is old procedural style.
        
         | HiPhish wrote:
         | > email.bulkSend(generateExpiryEmails(getExpiredUsers(db.getUse
         | rs(), Date.now())));
         | 
         | What makes it hard to reason about is that your code is one-
         | dimensional, you have functions like `getExpiredUsers` and
         | `generateExpiryEmails` which could be expressed as composition
         | of more general functions. Here is how I would have written it
         | in JavaScript:                   const emails = db.getUsers()
         | .filter(user => user.isExpired(Date.now()))  // Some property
         | every user has             .map(generateExpiryEmail);  // Maps
         | a single user to a message              email.bulkSend(emails);
         | 
         | The idea is that you have small but general functions, methods
         | and properties and then use higher-order functions and methods
         | to compose them on the fly. This makes the code two-
         | dimensional. The outer dimension (`filter` and `map`) tells the
         | reader what is done (take all users, pick out only some, then
         | turn each one into something else) while the outer dimension
         | tells you how it is done. Note that there is no function
         | `getExpiredUsers` that receives all users, instead there is a
         | simple and more general `isExpired` method which is combined
         | with `filter` to get the same result.
         | 
         | In a functional language with pipes it could be written in an
         | arguably even more elegant design:
         | db.getUsers() |> filter(User.isExpired(Date.now()) |>
         | map(generateExpiryEmail) |> email.bulkSend
         | 
         | I also like Python's generator expressions which can express
         | `map` and `filter` as a single expression:
         | email.bulk_send(generate_expiry_email(user) for user in
         | db.get_users() if user.is_expired(Date.now())
        
           | hackthemack wrote:
           | I guess I just never encounter code like this in the big
           | enterprise code bases I have had to weed through.
           | 
           | Question. If you want to do one email for expired users and
           | another for non expired users and another email for users
           | that somehow have a date problem in their data....
           | 
           | Do you just do the const emails =
           | 
           | three different times?
           | 
           | In my coding world it looks a lot like doing a SELECT * ON
           | users WHERE isExpired < Date.now
           | 
           | but in some cases you just grab it all, loop through it all,
           | and do little switches to do different things based on
           | different isExpired.
        
             | rahimnathwani wrote:
             | If you want to do one email for expired users and another
             | for non expired users and another email for users that
             | somehow have a date problem in their data....
             | 
             | Well, in that case you wouldn't want to pipe them all
             | through generateExpiryEmail.
             | 
             | But perhaps you can write a more generic function like
             | generateExpiryEmailOrWhatever that understands the user
             | object and contains the logic for what type of email to
             | draft. It might need to output some flag if, for a
             | particular user, there is no need to send an email. Then
             | you could add a filter before the final (send) step.
        
       | taeric wrote:
       | This 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 wrote:
         | _whispers 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 wrote:
           | Right, 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?
        
       | zkmon wrote:
       | I 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 wrote:
         | Functional 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.
        
       | bitwize wrote:
       | I 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.
        
       | socketcluster wrote:
       | Even 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 wrote:
         | I'd love to know more, do you have any links to your articles?
        
           | CharlesW wrote:
           | "Specific on the surface, generic underneath" (Medium
           | paywalled): https://medium.com/tech-renaissance/generic-
           | internals-specif...
        
         | foofoo12 wrote:
         | > Even large companies are still grasping at straws when it
         | comes to good code
         | 
         | Probably many reasons for this, but what I've seen often is
         | that once the code base has been degraded, it's a slippery
         | slope downhill after that.
         | 
         | Adding functionality often requires more hacks. The alternative
         | is to fix the mess, but that's not part of the task at hand.
        
         | frank_nitti wrote:
         | These are great and succinct, yours and your teammate's.
         | 
         | I still find myself debating this internally, but one objective
         | metric is how smoothly my longer PTOs go:
         | 
         | The only times I haven't received a single emergency call were
         | when I left teammates a a large and extremely specific set of
         | shell scripts and/or executables that do exactly one thing. No
         | configs, no args/opts (or ridiculously minimal), each named
         | something like _run-config-a-for-client-x-with-dataset-3.ps1_
         | that took care of everything for one task I knew they'd need.
         | Just double click this file when you get the new dataset, or
         | clone /rename it and tweak line #8 if you need to run it for a
         | new client, that kind of thing.
         | 
         | Looking inside the scripts/programs looks like the opposite of
         | all of the DRY or any similar principles I've been taught (save
         | for KISS and others similarly simplistic)
         | 
         | But the result speaks for itself. The further I go down that
         | excessively basic path, the more people can get work done
         | without me online, and I get to enjoy PTO. Anytime i make a
         | slick flexible utility with pretty code and docs, I get the
         | "any chance you could hop on?" text. Put the slick stuff in the
         | core libraries and keep the executables dumb
        
         | veqq wrote:
         | > The more specific, the more brittle. The more general, the
         | more stable. Concerns evolve/decay at different speeds, so do
         | not couple across shearing layers. Notice how grammar/phonology
         | (structure) changes slowly while vocabulary (functions,
         | services) changes faster.
         | 
         | ...
         | 
         | > Coupling across layers invites trouble (e.g. encoding
         | business logic with "intuitive" names reflecting transient
         | understanding). When requirements shift (features,
         | regulations), library maintainers introduce breaking changes or
         | new processor architectures appear, our stable foundations,
         | complected with faster-moving parts, still crack!
         | 
         | https://alexalejandre.com/programming/coupling-language-and-...
        
       | semiinfinitely wrote:
       | this looks like a post from 2007 im shocked at the date
        
         | diamondtin wrote:
         | I 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:
        
       | johnrob wrote:
       | Functions can have complexity or side effects, but not both.
        
       | jackbravo wrote:
       | Reminds me of this clean architecture talk with Python explains
       | this very well: https://www.youtube.com/watch?v=DJtef410XaM
        
       | diamondtin wrote:
       | destory all software
        
       | postepowanieadm wrote:
       | Something like that was popular in perl world: functional core,
       | oop external interface.
        
       ___________________________________________________________________
       (page generated 2025-10-27 23:00 UTC)