Live data from Hacker News

Simplify your code: Functional core, imperative shell

testing.googleblog.com

161–170 of 219 posts

Re: Simplify your code: Functional core, imperative shell

#161
Google 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.

Re: Simplify your code: Functional core, imperative shell

#162

I 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

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

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

Re: Simplify your code: Functional core, imperative shell

#163
post #108
post #85

Earlier quoted context omitted.

"Generic core, specific shell." Your advice is the opposite of "functional core, imperative shell". The FCIS principle has IS which is generic, to be simple, because it's usually hard to test (it deals with resources and external dependencies). So by being simple, it's more unit testable. On the other hand, FC is where the business logic lives, which can be complex and specific. The reason why you want that "function…

I disagree that these two pieces of advice are opposed. I think they are orthogonal at worst, and in agreement at best. "Functional core, imperative shell" (FCIS) is a matter of implementing individual software components that need to engage with side-effects --- that is, they have some impact on some external resources. Rather than threading representations of the external resources throughout the implementation, FC…

I think the "generic core" is often a SQL database or other such generic storage/analytics layer, while the "functional core" is the business logic that operates on the specific domain objects.

Re: Simplify your code: Functional core, imperative shell

#165
post #6

Earlier quoted context omitted.

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.

Not a single person in this thread commented on the use of Date.now() and similar - surely clock.now() - you never ever want to use global time in any code, how could you test it? clock in this case is a thing that was supplied to the class or function. It could just be a function: () -> Instant. (Setting a global mock clock is too evil, so don't suggest that!)

nit picking on the example code while missing the example the code was trying to demonstrate. I see why TAOCP used pseudocode

Re: Simplify your code: Functional core, imperative shell

#166
post #136

Earlier quoted context omitted.

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

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

In linq (C#)

    IEnumerable getExpiredUsers(DbSet users)
        => users.Where(u => u.ExpiresAt 
Such simple logical expressions (called expression trees) get converted to SQL queries

Re: Simplify your code: Functional core, imperative shell

#167

How does it fit with Tell Don't Ask https://martinfowler.com/bliki/TellDontAsk.html Or is it that the example in the article is a bit poor?

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

Re: Simplify your code: Functional core, imperative shell

#168

Earlier quoted context omitted.

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

> 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. Another factor, and perhaps the key factor, is that contrary to OP's extraordinary claim there is no such thing as objectively good code, or one single and true way of writing good code. The crispest definition of "good code" is that it's not obviously bad code from a spe…

> However, DDD has a strong object-oriented core

The original 2003 DDD book is very 2003 in that it is mired in object orientation to the point of frequently referencing object databases¹ as a state-of-the-art storage layer.

However, the underlying ideas are not strongly married to object orientation and they fit quite nicely in a functional paradigm. In fact, ideas like the entity/value object distinction are rather functional in and of themselves, and well-suited to FCIS.

[1]: https://en.wikipedia.org/wiki/Object_database

Re: Simplify your code: Functional core, imperative shell

#169

Earlier quoted context omitted.

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…

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

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.

Re: Simplify your code: Functional core, imperative shell

#170

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…

It also invites exceptions as error handling instead of a monadic (result) pattern. I usually do something more like

    Result userRes = getExpiredUsers(db);
    if(isError(userRes)) {
        return userRes.error;
    }

    /* This probably wouldn't actually need to return a Result IRL */
    Result emailRes = generateExpireyEmails(userRes.value);
    if(isError(emailRes)) {
        return emailRes.error;
    }

    Result sendRes = sendEmails(emailRes.value);
    if(isError(sendRes)) {
        return sendRes.error;
    }
    
    return sendRes; // successful value, or just return a Unit type.
This is in my "functional C++" style, but you can write pipe helpers which sort of do the same thing:

    Result result = pipe(getExpiredUsers(db))
        .then(generateExpireyEmails)
        .then(sendEmails)
        .result();

    if(isError(result)) {
        return result.error;
    }
If an error result is returned by any of the functions, it terminates immediately and returns the error there. You can write this in most languages, even imperative/oop languages. In java, they have a built in class called Optional with options to treat null returns as empty:

    Optional.ofNullable(getExpiredUsers(db))
        .map(EmailService::generateExpireyEmails)
        .map(EmailService::sendEmails)
        .orElse(null);
or something close to that, I haven't used java in a couple years.

C++ also added a std::expected type in C++23:

    auto result = some_expected()
        .and_then(another_expected)
        .and_then(third_expected)
        .transform(/* ... some function here, I'm not familiar with the syntax*/);
Post reply on HN