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

I 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.





Great 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?


It's called dependency injection and there's loads written about it. It's a really powerful technique which is also used for dependency inversion, key for decoupling components. I really like how it enables simple tests without any mocking.

The book Architecture Patterns in Python by Percival and Gregory is one of the few books that talks about this stuff using Python. It's available online and been posted on HN a few times before.


Agree with zdragnar; this is not traditional DI which is generally focused on injecting objects.

The difference between the two is that when you inject an object, the receiving side must know a potentially large surface area of the object's behavior.

When injecting a function, now the receiving side only needs to know the inputs and outputs of the singular function.

This subtlety is what makes this approach more powerful.


I don't see the difference, but I do agree that DI is generally used to mean constructing systems. It's what you do in your main or "bootstrap" part of the program and there are frameworks to do it for you. But really it's the same thing. You're just composing functionality by passing objects (functions are objects) that satisfy an interface. It might be more acceptable to just say it's dependency inversion.

DI (generally) tends to point more towards constructing objects or systems. This would be a bit closer to a functional equivalent of the OO "template method" pattern: https://en.wikipedia.org/wiki/Template_method_pattern

You write a concrete set of steps, but delegate the execution of the steps to the caller by invoking the supplied functions at the desired time with the desired arguments.




Consider applying for YC's Winter 2026 batch! Applications are open till Nov 10

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

Search: