Live data from Hacker News

Simplify your code: Functional core, imperative shell

testing.googleblog.com

51–60 of 219 posts

Re: Simplify your code: Functional core, imperative shell

#51
post #24

Earlier quoted context omitted.

> email.bulkSend(generateExpiryEmails(getExpiredUsers(db.getUsers(), Date.now()))); What makes it hard to reason about is that your code is one-dimensional, you have functions like `getExpiredUsers` and `generateExpiryEmails` which could be expressed as composition of more general functions. Here is how I would have written it in JavaScript: const emails = db.getUsers() .filter(user => user.isExpired(Date.now())) //…

I guess I just never encounter code like this in the big enterprise code bases I have had to weed through. Question. If you want to do one email for expired users and another for non expired users and another email for users that somehow have a date problem in their data.... Do you just do the const emails = three different times? In my coding world it looks a lot like doing a SELECT * ON users WHERE isExpired but in…

since were just making up functions..

    myCoolSubroutine = do
      now 
The whole pipeline thing is a red herring IMO.

Re: Simplify your code: Functional core, imperative shell

#52
post #20

Earlier quoted context omitted.

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

I'm unclear what you're suggesting here. Are you suggesting you couldn't write a POS in Haskell, say?

Re: Simplify your code: Functional core, imperative shell

#53

Even large companies are still grasping at straws when it comes to good code. Meanwhile there are articles I wrote years ago which explain clearly from first principles why the correct philosophy is "Generic core, specific shell." I actually remember early in my career working for a small engineering/manufacturing prototyping firm which did its own software, there was a senior developer there who didn't speak very go…

These are great and succinct, yours and your teammate’s. I still find myself debating this internally, but one objective metric is how smoothly my longer PTOs go: The only times I haven’t received a single emergency call were when I left teammates a a large and extremely specific set of shell scripts and/or executables that do exactly one thing. No configs, no args/opts (or ridiculously minimal), each named something…

Yes I feel that when to apply certain techniques is frequently under-discussed. But I can't blame people for err-ing on the side of 'do everything properly' - as this makes life more pleasant in teams. Although I think if you squint, the principle still applies to your example. The further you get from the 'core' of your platform/application/business/what-have-you, the less abstract you need to be.

Re: Simplify your code: Functional core, imperative shell

#54

Even large companies are still grasping at straws when it comes to good code. Meanwhile there are articles I wrote years ago which explain clearly from first principles why the correct philosophy is "Generic core, specific shell." I actually remember early in my career working for a small engineering/manufacturing prototyping firm which did its own software, there was a senior developer there who didn't speak very go…

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

I've seen it many times. And then every task takes longer than the last one, which is what pushes teams to start rewrites. "There's never enough time to do it right, but always time to do it again."

Re: Simplify your code: Functional core, imperative shell

#55
post #24

Earlier quoted context omitted.

> email.bulkSend(generateExpiryEmails(getExpiredUsers(db.getUsers(), Date.now()))); What makes it hard to reason about is that your code is one-dimensional, you have functions like `getExpiredUsers` and `generateExpiryEmails` which could be expressed as composition of more general functions. Here is how I would have written it in JavaScript: const emails = db.getUsers() .filter(user => user.isExpired(Date.now())) //…

I guess I just never encounter code like this in the big enterprise code bases I have had to weed through. Question. If you want to do one email for expired users and another for non expired users and another email for users that somehow have a date problem in their data.... Do you just do the const emails = three different times? In my coding world it looks a lot like doing a SELECT * ON users WHERE isExpired but in…

> Question. If you want to do one email for expired users and another for non expired users and another email for users that somehow have a date problem in their data.... > > Do you just do the const emails = > > three different times?

If it's just two or three cases I might actually just copy-paste the entire thing. But let's assume we have twenty or so cases. I'll use Python notation because that's what I'm most familiar with. When I write `Callable[[T, U], V]` that means `(T, U) -> V`.

Let's first process one user at a time. We can define an enumeration for all our possible categories of user. Let's call this enumeration `UserCategory`. Then we can define a "categorization function" type which maps a user to its category:

    type UserCategorization = Callable[[User], UserCategory]
I can then map each user to a tuple of category and user:

    categorized_users = map(categorize, db.get_users())  # type Iterable[tuple[UserCategory, User]]
Now I need a mapping from user category to processing function. I'll assume we call the processing function for side effects only and that it has no return value (`None` in Python):

    type ProcessingSpec = Mapping[UserCategory, Callable[[User], None]
This mapping uses the user category to look up a function to apply to a user. We can now put it all together: map each user to a pair of the user's category and the user, then for each pair use the mapping to look up the processing function:

    def process_users(how: ProcessingSpec, categorize: UserCategorization) -> None:
        categorized_users = map(categorize, db.get_users())
        for category, user in categorized_users:
            process = how[category]
            process(user)
OK, that's processing one user a time, but what if we want to process users in batches? Meaning I want to get all expired users first, and then send a message to all of them at once instead of one at a time. We can actually reuse most of our code because how how generic it is. The main difference is that instead of using `map` we want to use some sort of `group_by` function. There is `itertools.groupby` in the Python standard library, but it's not exactly what we need, so let's write our own:

    def group_by[T, U](what: Iterable[T], key: Callable[[T], U]) -> Mapping[U, list[T]]:
        result = defaultdict(list)
        # When we try to look up a key that does not exist defaultdict will create a new
        # entry with an empty list under that key
        for x in what:
            result[key(x)].append(x)
        return x
Now we can categorize our users into batches based on their category:

    batches = group_by(db.get_users(), categorize)
To process these batches we need a mapping from batch to a function which process an iterable of users instead of just a single user.

    type BatchProcessingSpec = Mapping[UserCategory, Callable[[Iterable[User]], None]
Now we can put it all together:

    def process_batched_users(how: BatchProcessingSpec, categorize: UserCategorization) -> None:
        batches = group_by(db.get_users(), categorize)
        for category, users in batches:
            process = how[category]
            process(users)
There are quite a lot of small building block functions, and if all I was doing was sending emails to users it would not make sense to write these small function that add indirection. However, in a large application these small functions become generic building blocks that I can use in higher-order functions to define more concrete routines. The `group_by` function can be used for many other purposes with any type. The categorization function was used for both one-at-a-time and batch processing.

I have been itching to write a functional programming book for Python. I don't mean a "here is how to do FP in Python" book, you don't need that, the documentation of the standard library is good enough. I mean a "learn how to think FP in general, and we are going to use Python because you probably already know it". Python is not a functional language, but it is good enough to teach the principles and there is value in doing things with "one hand tied behind your back". The biggest hurdle in the past to learning FP was that books normally teach FP in a functional language, so now the reader has to learn two completely new things.

Re: Simplify your code: Functional core, imperative shell

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

> Indeed, the general idea of imperative assembly comes to mind as the ultimate "core" for most software. That's not what functional core, imperative shell means though. It's a given that CPUs aren't functional. The advice is for people programming in languages that have expressions - ruby, in the case of the original talk. The functional paradigm mostly assumes automatic memory management.

Right, I was just using that as "at the extreme" and how it largely exists to allow you to put a functional feel on top of the imperative below it.

I'm sympathetic to the idea, as you can see it in most instruction manuals that people are likely to consume. The vast majority of which (all of them?) are imperative in nature. There is something about the "for the humans" layer being imperative. Step by step, if you will.

I don't know that it fully works, though. I do think you are well served being consistent in how you layer something. Where all code at a given layer should probably stick to the same styles. But knowing which should be the outer and which the inner? I'm not clear that we have to pick, here. Feel free to have more than two layers. :D

Re: Simplify your code: Functional core, imperative shell

#57
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…

I'm unclear what you're suggesting here. Are you suggesting you couldn't write a POS in Haskell, say?

My idea here is that, in many domains, you will have operations that are somewhat definitionally in the imperative camp. OpenTransaction being the easy example.

Can you implement it using functional code? Yes. Just make sure you wind up with partial states. And often times you are best off explicitly not using the RAI pattern for some of these. (I have rarely seen examples where they deal with this. Creating and reconciling transactions often have to be separate pieces of code. And the reconcile code cannot, necessarily, fallback to create a transaction if they get a "not found" fault.)

Re: Simplify your code: Functional core, imperative shell

#58
post #20

Earlier quoted context omitted.

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 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 appear in a static piece of the code?

What do you mean by Monad trick? That's precisely the kind of thing the IO monad exists for. If you need to fetch things on an API: IO. If you need to read/save things on a DB: IO. DB Transaction: IO.

Post reply on HN