[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)