@ondrejgr Hi Ondrej,
These are the same kinds of use cases I run into. An empty I/O operation instead of defer() has generally been the solution.
I generally approach this by requiring I/O operations to give their output in a new transaction, even when they are null operations.
If you require your I/O operation to give its output in a new transaction - and you don't let that out of scope (so it can't be used elsewhere), and you split() the result, then each split transaction is guaranteed not to be simultaneous with anything else.
It works like this: top-level transactions might be called "1", "2", "3", etc. A split() done in transaction 2 will create new transactions 2.1, 2.2, 2.3, ... which are guaranteed to be before "3". Similarly, a split in 2.2 will create new transactions 2.2.1, 2.2.2, 2.2.3, etc.
Further, separate defers that both occur in transaction 2 will both put their output value in 2.1, and those two output values will be simultaneous. I hope that's clear.
The issue with the I/O operation approach is, of course, that it introduces non-determinism. Maybe this is appropriate in "I/O" - but how this is justified conceptually, I am not sure!
The problem is that a deterministic system can't guarantee non-simultaneity unless it's done explicitly. It would be possible to write a "dont-be-simultaneous-with-S" operator where S is a set of streams, but it's not possible to write a "dont-be-simultaneous-with-anything-else" operator and remain deterministic.
A null I/O operation is how we achieve the desired operator, but it throws determinism away. If we are doing I/O anyway, then we haven't lost anything, because I/O is non-deterministic.