Live data from Hacker News

Simplify your code: Functional core, imperative shell

testing.googleblog.com

11–20 of 219 posts

Re: Simplify your code: Functional core, imperative shell

#11
post #2

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

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…

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.

Re: Simplify your code: Functional core, imperative shell

#13
post #9

Earlier quoted context omitted.

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…

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

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?

Re: Simplify your code: Functional core, imperative shell

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

Re: Simplify your code: Functional core, imperative shell

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

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)

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.

Re: Simplify your code: Functional core, imperative shell

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

Re: Simplify your code: Functional core, imperative shell

#17
post #16

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

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.

Re: Simplify your code: Functional core, imperative shell

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

Re: Simplify your code: Functional core, imperative shell

#19
post #3

If your language supports generators, this works a lot better than making copies of the entire dataset too.

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.

Re: Simplify your code: Functional core, imperative shell

#20
post #16

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

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.

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?

Post reply on HN