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.
Simplify your code: Functional core, imperative shell
161–170 of 219 posts
Re: Simplify your code: Functional core, imperative shell
#162I 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.
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
#163Earlier 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…
Re: Simplify your code: Functional core, imperative shell
#164Or is it that the example in the article is a bit poor?
Re: Simplify your code: Functional core, imperative shell
#165Earlier 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!)
Re: Simplify your code: Functional core, imperative shell
#166Earlier 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.
IEnumerable getExpiredUsers(DbSet users)
=> users.Where(u => u.ExpiresAt
Such simple logical expressions (called expression trees) get converted to SQL queriesRe: Simplify your code: Functional core, imperative shell
#167How 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?
> 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
#168Earlier 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…
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.
Re: Simplify your code: Functional core, imperative shell
#169Earlier 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…
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
#170I 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…
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*/);