Live data from Hacker News

Simplify your code: Functional core, imperative shell

testing.googleblog.com

61–70 of 219 posts

Re: Simplify your code: Functional core, imperative shell

#61
I 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().

Re: Simplify your code: Functional core, imperative shell

#63

One of the core design principles at https://github.com/aperoc/toolkami

Spam

Not sure man, I specifically stated this in my README way before this post: 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 ...

Re: Simplify your code: Functional core, imperative shell

#64
post #14

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 ha…

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 ste…

(author here)

This is actually closer to the way the first draft of this article was written. Unfortunately, some readability was lost to make it fit on a single page. 100% agree that a statement like this is harder to reason about and should be broken up into multiple statements or chained to be on multiple lines.

Re: Simplify your code: Functional core, imperative shell

#65
post #58
post #20

Earlier quoted context omitted.

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 inserte…

> but even in those, you typically have the more imperative operations as the lower levels Yes, the monadic part is the functional core, and the runtime is the imperative shell. > 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…

I have not seen too many (any?) times where the monad trick is done in such a way that they don't combine everything in a single context wrapper and talk about the "abnormal" case where things don't complete during execution.

Granted, in trying to find some examples that stick in my memory, I can't really find any complete examples anymore. Mayhap I'm imagining a bad one? (Very possible.)

Re: Simplify your code: Functional core, imperative shell

#66
post #14

Earlier quoted context omitted.

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 ste…

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.

There's nothing procedural about binding return values to variables, so long as you aren't mutating them. Every functional language lets you do that. That's `let ... in` in Haskell.

Re: Simplify your code: Functional core, imperative shell

#67

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 ha…

I may have gotten nerd sniped here, but I believe all of these examples so far have some subtle errors. Using elixir syntax, I would think something like this covers most of the cases:

    expiry_date = DateTime.now!("Etc/UTC")

    query = 
          from u in User,
          where: 
            u.expiry_date > ^expiry_date 
            and u.expiry_email_sent == false,
          select: u

    MyAppRepo.all(query)
    |> Enum.map(u, &generate_expiry_emails(&1, expiry_date))
    |> Email.bulkSend()  # Returns {:ok, %User{}} or {:err, _reason}
    |> Enum.filter(fn 
      {:ok, _} -> true
      _ -> false
    end)
    |> Enum.map(fn {:ok, user} ->
      User.changeset(user, %{expiry_email_sent: true})
      |> Repo.update()
    end)

Mainly a lot of these examples do the expiry filtering on the application side instead of the database side, and most would send expiry emails multiple times which may or may not be desired behavior, but definitely isn't the best behavior if you automatically rerun this job when it fails.

----

Edit: I actually see a few problems with this, too, since Email.bulkSend probably shouldn't know about which user each email is for. I always see a small impedance mismatch with this sort of pipeline, since if we sent the emails individually it would be easy to wrap it in a small function that passes the user through on failure.

If I were going to build a user contacting system like this I would probably want a separate table tracking emails sent, and I think that the email generation could be made pure, the function which actually sends email should probably update a record including a unique email_type id and a date last sent, providing an interface like: `send_email(user_query, email_id, email_template_function)`

Re: Simplify your code: Functional core, imperative shell

#68
post #44

Earlier quoted context omitted.

> 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(?))…

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…

CQRS should really only guide you to designing separate query and command interfaces. If your processing is asynchronous then you have no choice but to have state about processing-in-flight, and your commands should return an acknowledgement of successful receipt of valid commands with a unique identifier for querying progress or results. If your processing is synchronous make your life easier by just returning the result. Purity of CQRS void-only commands is presentation fodder, not practicality.

(One might argue that all RPC is asynchronous; all such arguments eventually lead to message buses, at-least-once delivery, and the reply-queue pattern, but maybe that's also just presentation fodder.)

Re: Simplify your code: Functional core, imperative shell

#69
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.

Re: Simplify your code: Functional core, imperative shell

#70
post #14

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 ha…

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 ste…

Glad to see this. This style seems like it’s out of vogue now, but I find it much, much easier to reason about.
Post reply on HN