Temporal Programming, a new name for an old paradigm
github.com
github.com
The first synchronous programming languages were, AFAIK, Statecharts and Esterel, the former designed by David Harel while he was working alongside Amir Pnueli, who would later win the Turing Award for introducing temporal logic to computer science. https://www.wisdom.weizmann.ac.il/~harel/papers/Statecharts....
Eve also combined synchronous programming with logic programming: https://witheve.com
They were initially programmed using Ladder Diagrams, then STX (Structured text) a Pascal like language and IL (Instruction List), Sequential function chart, or Function block diagram. These all use Common Elements (IEC 61131 standard) so, you can mix languages.
https://www.microsoft.com/en-us/research/publication/program...
There are precious few programs for which a|b ~ a;b is true for all atomic programs a,b where:
| is a parallel composition operator
; is the normal sequential composition operator (defined in the completely standard way in terms of updates to a global state or heap/stack-like structure if you want to be fancy)
~ is any sort of equivalence relation on reasonable and realistic semantics.
More-over, there are precious few programs that easily decompose into parts A,B,... such that A|B ~ A;B is true for each A and B in the set of parts. You can give theoretic characterizations of this fact, and many have, but the pragmatic point is more convincing.
Associativity and even reflexivity often fail as well.
The allure of this line of thought is promising, but alas... The "just make a simple algebra and arrange your programs to fit it" research program died early on for a reason. In the context of parallel and concurrent programs, it is hard/impossible to solve the decomposition problem at the language level of abstraction.
That pretty much summarizes outcome of a big chunk of program analysis research from the 70s to early 90s in a nutshell, unfortunately.
Let me briefly illustrate using OP's example:
`a' = b + 1; b' = a + 1`
from which I've deleted the errant training semicolon (it's `1+1` not `1+1+`!)
Suppose we instead have the program:
a' = a + b + 1; b' = a + b + 1
Now, the ordering does matter! No worries, so we order.
Additionally, suppose we put these programs into an outer while(true) loop and that's our whole program. Now what have we bought ourselves? Not much.
In this case we can solve that problem by... well, by solving some sort of integer recurrence equations that give us a program without loops which we can then think about algebraically. But of course this gets difficult or impossible very fast. (And, btw, the algebra part here did not buy us much. It was solving integer recurrence equations that saved the idea in this example). We haven't even added real data structures or external state yet.
Anyways, looks like OP is building something around this idea. Good luck! I'll be curious to see how you handle loops inside of blocks that contain @'d variables with recursively defined quantities :)
a' = a + b + 1 and b' = a + b + 1
should be
a' = a + b' + 1 and b' = a' + b + 1
Or alternatively they can have non-primed right hand sides but occur in a loop and you have the same problem even though the rhs's are at-free. A syntactic constraint won't work unless it includes "and also no loops in at-mentioning blocks" which... well, yeah, we know how to reason about NFAs :)
Sorry again, trying to make too many points with a single example
"Now what have we bought ourselves? Not much."
Well of course I disagree with that statement. My work on Metron suggests that we've bought ourselves a way to write programs that can execute on both CPUs and FPGAs, which is a valuable thing. The ability to more directly translate the behavior of our programs into a proof framework like TLA is also quite useful, and I think folks who are deep into functional programming also like the lack of "side effects" since the process of computing x' is guaranteed not to change x (or any other observable property of the previous program state).
So John Backus not only suggested "Applicative State Transition" in his paper. He participated in ALGOL creation. Which influenced most contemporary languages.
We have this style of programming in our Pascal and Java programs.
Please no new names. Semicolon is semicolon.
Sorry, I can't find this blog or paper by Peyton Jones. Leaving that as an exercise for the reader :) And of course, I can misremember something.
And indeed, the Backus publication even has a section "14.2 The Structure of Algol Compared to That of AST Systems".
Semicolon is semicolon, but in Pascal/Java you have lots of tiny semicolons and in a temporal program you have one giant semicolon. The practical implications of that are more wide-ranging than you think, and quite interesting.
https://en.wikipedia.org/wiki/Lucid_(programming_language)
They don't use x', but rather fby x, but otherwise the ideas seem very similar.
Of course, they called it dataflow, and Lucid inspired the "synchronous dataflow" languages like Esterel (imperative) and Lustre (functional). Which in turn inspired the non Conal Elliott variants of "FRP".
It's all related...
The wikipedia link describing it is overly arcane but here it is anyway: https://en.wikipedia.org/wiki/Operational_semantics
The idea behind STM is to specify some block of code as being one big fat atomic operation, and then the runtime will magically make it so. The idea behind SSA form is to only assign variables once, as doing so makes imperative optimization as easy as functional.
I wonder if the author knows about these things, or if they're coming in at a different angle.
STM certainly makes temporal programming easier, though it's not universally available and I'm not sure to what extent it will scale up to be performant enough for large-scale use - how many megabytes can I commit in a transaction before I hit some hardware limit.
SSA is a very valuable formalism for describing the interrelation of changing local variables inside an imperative function, but it's difficult to apply program-wide when you're dealing with large data structures and complex call stacks.
Are people interpreting using their own experience, or is there a big overlap?
STM provides (compositional) thread-safety within a single process. It requires FP but other than that I don't see the comparison.
But, well, I'm not really sure people mean that when they talk about it, because it's left unespecified. And when people implement FRP they always seem to take obvious "shortucts" with significant downsides and that avoid implementing something like this.
This sounds a lot like graph computation but on an extremely granular level.
You need a data base with full history (transaction time), try datomic. Additionally if you need full history AND domain time (bi-temporal) included in your data base, try or xtdb.
Functions.
> x = x + 1 would not be valid, while x' = x + 1 would be.
Yep.
void tick() {
a@ = b + 1;
b@ = a + 1;
}
Void, oh no!void is the unit type, not the uninhabited type. (I'm really not sure what Haskell was smoking when it messed that[0] up, but it's probably too late to fix now.)
0: Calling it Data.Void rather than something like Data.Noreturn or whatever.
void foo(int x) {
printf("%i\n",x);
return; /* <- inhabited */ }
0: Give or take some pedantry about `TYPE 'LiftedRep`.It's been a good 30 years since I've written any C, and I know its type-system is compromised at best, but I'm fairly sure you can't do something like this?
void x = new void();
Or, using your foo example: void x = foo();
If you can then void is inhabited (and incorrectly named). I realise you could create void* and other elaborate means of claiming you have an inhabited void, but really can you legally construct one, not hack any underlying value to pretend to have one - there is a difference. If you can construct one and assign it to a variable, then it's inhabited.Railing against Haskell when its implementation actually works correctly is a very strange position to take. The clue really is in the name empty-set = void, singleton-set = unit.
For example
always @(posedge clk) begin A <= B; B <= A; end
swaps A and B on each positive clock edge.
You don’t need a functional language to do this. However it does require more discipline when the language doesn’t enforce purity.
And once I started thinking about software this way I realised that events are important and should be the core of any software design. Basically the application is a single function that takes an event and the current state, and computes a new state and zero or more events.
The IO type in haskell is defined,
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))* simulation variables (presumably because the separation into x/x' is similar to the now/next calculation of state in a lot of simulations).
* SSA (although there is a subtle difference as x' becoming x in the next period isn't really SSA without some extra edges in the graph).
I wonder how phi nodes would fit into the author's scheme?
https://www.microsoft.com/en-us/research/publication/compili...
Regarding phi nodes, they represent a "merge" of two possible execution branches. In C++ you evaluate one side of the if() branch and ignore the other based on the predicate and the "phi" (sometimes called "phony") function then copies the result from the selected branch to the new SSA variable. In a language like Verilog, you evaluate _both_ branches and the phi node is effectively a "mux gate" that selects between the two branch results based on the predicate.
i call this "wave" calculation - as the algo to find the sequence after each input change/stimulus is spreading changes like wave from-the-point-of-change-outwards, in topological sort. There might be (direct or indirect) recursion allowed, up to some predefined level, or minimum-change-diff thresholds or whatever.
Writing a calculator for such set of rules is relatively easy, from memory they were like 1000 lines in c++, 100 lines python, 200 lines js.. it more depends on the "syntax" and conveniencies for the programmers, than actual engine.
Now, Turning that into full language .. will be really interesting. Kind of turning inside-out the relation wavecalc <--> general-language-of-choice..
If you write your program this way, how does it interact with the environment (e.g user input, output, network…)? Something is missing.
And if it doesn’t interact with the environment, then your program is just one big pure function, in which case you get all of the benefits in OP’s proposal anyway, without having to worry about transforming old state to new state, you just gotta transform input to output.
The answer is, of course, monads[0]. Probably.
Firstly, we define a place (temporal x y) such that when this place is accessed, it denotes x, and when it is assigned, the assignment goes to y:
(defplace (temporal-place var var*) body
(getter setter
^(macrolet ((,getter () ,var)
(,setter (val) ^(set ,',var* ,val)))
,body))
(setter
^(macrolet ((,setter (val) ^(set ,',var* ,val)))
,body)))
(define-place-macro temporal (var var*) ^(temporal-place ,var ,var*))
(defmacro temporal (var var*) var)
We can then use that as a building block whereby we define an apparent variable v as a symbol macro which expands to (temporal vold vnew). All accesses to v get the value of vold, but assignments to v do not affect vold; they go to vnew.Here is a let-like macro which sets up the specified variable as temporal. The initial values propagate into both the old and new location.
Then before the last form of the body is evalulated, the new values are copied to the old values.
(defmacro temporal-let (vars . body)
(let ((olds (mapcar (ret (gensym)) vars))
(news (mapcar (ret (gensym)) vars)))
^(let* (,*(zip olds [mapcar cadr vars])
,*(zip news olds))
(symacrolet ,(zip [mapcar car vars]
[mapcar (op list 'temporal) olds news])
,*(butlast body)
(set ,*(weave olds news))
,*(last body)))))
Test. e.g: 1> (temporal-let ((x 1) (y 2))
(set x y)
(set y x)
(list x y))
(2 1)
The expansion is: 2> (expand '(temporal-let ((x 1) (y 2))
(set x y)
(set y x)
(list x y)))
(let* ((#:g0024 1)
(#:g0025 2)
(#:g0026 #:g0024)
(#:g0027 #:g0025))
(sys:setq #:g0026
#:g0025)
(sys:setq #:g0027
#:g0024)
(progn (sys:setq #:g0024
#:g0026)
(sys:setq #:g0025
#:g0027))
(list #:g0024 #:g0025))
Let's see how it compiles: 2> (disassemble (compile-toplevel '(temporal-let ((x 1) (y 2))
(set x y)
(set y x)
(list x y))))
data:
0: 1
1: 2
syms:
0: list
code:
0: 20020002 gcall t2 0 d1 d0
1: 04010000
2: 00000400
3: 10000002 end t2
instruction count:
2
#<sys:vm-desc: 903ec50>
Oops, it got compiled to a single call instruction invoking (list 2 1). Let's try opt-level zero: 1> (let ((*opt-level* 0))
(disassemble (compile-toplevel '(temporal-let ((x 1) (y 2))
(set x y)
(set y x)
(list x y)))))
data:
0: 1
1: 2
syms:
0: list
code:
0: 04020004 frame 2 4
1: 2C800400 movsr v00000 d0
2: 2C810401 movsr v00001 d1
3: 2C820800 movsr v00002 v00000
4: 2C830801 movsr v00003 v00001
5: 2C820801 movsr v00002 v00001
6: 2C830800 movsr v00003 v00000
7: 2C800802 movsr v00000 v00002
8: 2C810803 movsr v00001 v00003
9: 20020002 gcall t2 0 v00000 v00001
10: 08000000
11: 00000801
12: 10000002 end t2
13: 10000002 end t2
instruction count:
12
#<sys:vm-desc: 9de1b50>
TXR Lisp's compiler performs frame elimination optimization whereby the v registers get reassigned to t registers, eliminating the frame setup. The t registers are subject to some decent data flow optimizations and can disappear entirely, like in this case.For more general packaging on my division something like a macro with establishes a temporal contour. The effects are settled when this contour terminates. Within that contour we have special constructs which indicates that certain variables have temporal semantics. We clearly need some way to and close multiple temporal blocks so that they behave as a unit.
In a temporal language this contour which binds together the blocks will disappear because part of the semantics of any block, such as a function body. We can make some dedicated helper constructs like defun-temporal, whose body is a temporal contour.
Assignments to subfields are an interesting problem. I made the simplifying assumption that a temporal place binds together two symbolic places: the old and new. I could quite easily redefine this place so that its arguments, or rather it's left argument, is an arbitrary place. Reading the number location will access that place. Writing the place will go to the temporary variable, which eventually is committed to the underlying location. When the place is just a variable this will generate the same code as it does now.