3 ms·
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.
by taeric 1y ago
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.
- agentultra 1y agowhispers 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.
- taeric 1y agoRight, 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?
- garethrowlands 1y agoI'm unclear what you're suggesting here. Are you suggesting you couldn't write a POS in Haskell, say?
- taeric 1y agoMy 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.)
- whstl 1y ago> 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.
- taeric 1y agoI 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.)
- whstl 1y agoYou would deal with this problem in the same way you would with, say, in a REST API. If the transaction object is serializable you can just store it in a DB, for example. If it's some C++ pointer from some 3rd-party library that you can't really serialize and gotta keep open, you gotta keep it in memory and manage its lifetime explicitly, be it a REST web server, in Haskell or in a C++ app.
- taeric 1y agoRight, I think a better way of stating my main assertion here is that you have to be able to partially work in a transaction. If your "shell" pretends that you can always complete the full transaction, either successfully or with a failure, then it is a brittle shell. Sometimes, you can simply make progress on an presumed open transaction.
- 1y ago
- garethrowlands 1y ago> 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.
- taeric 1y agoRight, 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
- mrkeen 1y ago> This works right up to the point where you try to make the code to support opening transactions Indeed. It's all well and good to impart some kind of flavour into your code and call it functional, but transactions do not give a crap about style. A transaction needs to be able to 'back out' to fulfill 'all-or-nothing' semantics. Side effects are what make this impossible.
- garethrowlands 1y agoSurely transactions are a pretty good example of where functional core / imperative shell is a good guide. You really don't want to be doing arbitrary side effects inside your transaction because those can't be backed out. Check out STM in Haskell for a good example.
- mrkeen 1y ago> You really don't want And that's what this thread is filled with, and that's what I'm pushing back against. > the RAI pattern is nice > indicate if you are in different sections is nice Style doesn't matter, flavour doesn't matter, wants don't matter, code "quality" (whatever that means) doesn't matter, niceness doesn't matter. A transaction can be rolled back. If it can't, it's not a transaction.
- taeric 1y agoI'd go a little further, though? Transactions have to be able to fail at the very last step. That could just be the "commit" stage. Everything up to that point could have been perfectly fine, but at commit it fails. More, the time of execution of that commit could be fairly far removed from the time of starting the transaction. To that end, any style that tries to move those two time periods closer together in code is almost doomed to have some either hard to reason about code, or some tough edge cases that are hard to specify. (Granted, I'll note that most transactions that people are dealing with on a regular basis probably do open and close rather close to each other.)
- 1y ago