guitarvydas / py0d

3 stars 0 forks source link

hsm.py vs state.py #2

Open RAbraham opened 1 year ago

RAbraham commented 1 year ago

They have similar methods but are slightly different?

guitarvydas commented 1 year ago

Correct.

HSMs are layered state machines.

A State Machine is composed of States.

A State can be a Leaf node, or, it can be another State Machine (recursively, hierarchically).

A Machine cannot contain code.

A State IS code.

A Machine provides a Send() operation that can be used by any one of its states.

An ė (pronounced "Eh" in ASCII) object is a unit of concurrency.  An ė object is a Machine that catpures all Sends() from it Children and GrandChildren (and so on) and places data on its own output queue.

An HSM is a layered piece of non-concurrent code - clockwork code, like what we call programming at present- that is used to implement a portion of the insides of an ė object.

State

A State is not just 1 piece of code.

A State has

That would be 3 λs, not 1.

At present, we try to flatten all 3 pieces of code into a single λ. This can be done using epicycles (AKA accidental complexity) and greater confusion (flattening is an optimization which tends to obfuscate the meaning of code).

It is "easy" to conceive of exiting a Machine by hierarchically calling the exit code (deepest first) of the current State and all of it ancestors.

Flattened code can do this, but causes hand-wringing, in the form of cleanup code language features like defer statements and finally and __del__() , etc.

Choosing the Right Weapon

I advocate using multiple notations within a single project.

Currently, we use a single notation - functions - to describe every kind of code.

I feel that StateCharts are more convenient than functions for describing sequencing of actions.

HSMs are derived from StateCharts.

I think that StateChart notation subsumes 2 kinds of code:

  1. hierarchical Machines and States (synchronous, clockwork code)
  2. concurrency (called Orthogonal States in StateChart lingo).

I like to separate the concepts into 2 different notations:

Atomic Principles

I like to discover the atoms of software construction that can be used to build up UXs for various parts of a problem solution.

Hence, my extreme interest in Ohm-JS. I use Ohm-JS to drape syntax skins over atomic concepts to create molecules and various kinds of programming UXs.

I've developed this attitude, iteratively, over several decades, starting with compilers that create Assembler, Lisp, and, now, Ohm-JS.

Concurrency

Every concurrent object is a state machine.

Think Actors.

Currently, ė-like objects are treated very specially, using bloatware in what we call processes in operating systems.

Note that operating systems, e.g. UNIX written in C, are simply Greenspun's 10th Rule[^gtr] applied to closures. All of that C code simply implements closures.

[^gtr]: "Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp." https://en.wikipedia.org/wiki/Greenspun's_tenth_rule

At present, we use functions and functional notation to describe and implement clockwork code inside of processes (ė-like objects).

At present, we write all "code" as functions, then wrap big, honking State Machines around them using processes and operating systems.

Functions have the unfortunate feature of blocking (caller waits for callee to execute a return), which causes the invention of epicycles called preemption. The preemption epicycle, causes further epicycles. We saw this very clearly in the Mars Pathfinder disaster which necessitated the invention of priority inheritance epicycles.

Flattening

Can you flatten the concept of Machines and States into one Class?  

Yes.  

Is this advisable?  

No.

Flattening is an optimization and usually makes things appear to be more complicated, or, dilutes the meaning of things.

I think that it is “simpler” to describe HSMs using 2 classes.

See Also

StateCharts https://publish.obsidian.md/programmingsimplicity/2022-10-24-Eh+and+HSMs+Again

Please keep asking me questions if I haven’t convinced you :-).

RAbraham commented 1 year ago

When the above says A State is not just 1 piece of code., you mean A Leaf State is not just 1 piece of code? I ask because it is also written, A State can be a Leaf node, or, it can be another State Machine (recursively, hierarchically). so the word State is used above both to refer to the abstract State interface and LeafState? So?,

State = LeafState | HierachicalState (or HSM)

So I should read everywhere above that you use State as LeafState?

RAbraham commented 1 year ago

"A Machine provides a Send() operation that can be used by any one of its states.". Is the send sent to each of the child states or do we explictly send to specific child states?

RAbraham commented 1 year ago

"I feel that StateCharts are more convenient than functions for describing sequencing of actions." in FBP, we can sequence actions too by putting one box before another? So when do I chose StateCharts over FBP?

guitarvydas commented 1 year ago

I agree that I have not provided enough detail in written prose form.

Let's see if I can draw a diagram of what I mean... begin: on branch dev - hsm.drawio open in diagrams.net (draw.io) and look at the tabs ['hsm overview', 'machine overview', 'machine signatures', 'machine control flow', 'state overview', 'state signatures', 'state control flow']

(ignore tabs "Copy of ..." - those are temporaries during dev to make drawing less painful)

guitarvydas commented 1 year ago

Is the send sent to each of the child states or do we explicitly send to specific child states?

It doesn't matter how this is implemented. States can contain code that calls Send() and those Send()s refer to the output queue of the containing concurrent element.

Concurrent elements are blobs of code that have input and output queues (FIFOs, not LIFOs).

The insides of concurrent elements are programmed in any manner you wish. I suggest using a hierarchy of state machines. All of the inner code is synchronous (stack-based, LIFO: call-return based using a shared call-stack (I'm not saying anything new here, this is the way that all call-return-function-based languages work)).

HSM suggests wrapping code in hierarchical machines. FP suggests wrapping code in Lambdas. FP is "assembler" for HSM - you can do all of this with FP, but HSM gives a "more structured" view of how to arrange functions and the order in which they should be invoked. (Likewise, Assembler is "assembler" for FP - you can do it all in Assembler, but FP gives a "more structured" view of how to arrange sequences of opcodes).

The important suggestion here is: use FIFOs (queues) for inter-component communication, and, use whatever you want for the insides of components. "Structuring" the insides of components makes the job of implementing them easier. I think that HSM provides better "structuring" of reactive code than other methods I've used.

It is important to recognize when to use asynchrony, and, when synchrony is "good enough". All common "general purpose" languages that I know of are geared towards building synchronous - clockwork - code. Asynchrony is supported in those languages using ad-hoc libraries of assembler-like primitives (usually called "threads").

Send() is for asynchrony. Call/Return is for synchrony.

guitarvydas commented 1 year ago

Correct.

I split (AKA divide and conquer) Control Flow from Object definition.

Sequencing in ė and in FBP, etc. is done by snapping components together (like LEGO blocks).

You can hook components up in series, or, you can hook components up in parallel.

With textual programming, you get only one choice - sequencing is defined by writing lines of code one after another. The lines can include calls to subroutines, but, those calls are sequenced by the top level sequence of lines.

It is possible to use textual programming languages to write asynchronous code, but, you are reduced to essentially using ad-hoc assembler, in the form of threading libraries and their APIs, and low-level concepts like semaphores, locks, thread safety, priorities, functions, etc., etc. You must be careful how you sequence this kind of code. Some people have a knack for getting this right, most don't. By default, we write totally synchronous code and drop that code into envelopes called "processes", hoping that all of our problems will be solved. In fact, that kind of solution only solves the problems that were deemed important by the developer(s) of the threading libraries and operating systems. If you want to do something different - like IoT, blockchain, robotics, internet - tough luck, you are on your own and you have to use ad-hoc assembler-level APIs for such ideas.

I think that HSMs + ė allow you to imagine these kinds of ideas and to snap components together to implement such ideas.

To me, the crux of the matter is 0D - lack of implicit synchronization. You can create synchronized components using 0D, but it is much harder to create 0D with synchronized components. The result of doing everything with implicitly synchronized components is brittle code, bugs and bloatware. Imagine an intricate Swiss watch with lots of tightly-coupled gears. Break one tooth off of one gear and the whole thing fails. In a non-sychronized system, say a bunch of independent people at a business meeting, if one person fails to show up, the meeting can go on. Reuse - can you pull one gear out of a Rolex and use it in a Swatch? Sometimes, yes, but, in general, no.

HSMs + ė provide a 0D toolkit.

RAbraham commented 1 year ago

thanks! I'm looking at the hsm.drawio. tab: machine control flow:

RAbraham commented 1 year ago

just an update. I think I grok at a high level but when I read in detail, I'm getting a bit lost. I tried a few times but I need a fresh mind and time which is a bit lacking right now. I'll keep coming back to this!

guitarvydas commented 1 year ago

The concept is very simple, although maybe the code isn't. I wonder if I might say this in a more-simple manner?

An ė component is a composition of 4 things:

  1. it can Send messages
  2. it can Receive messages
  3. it can be contained in another Component (Runnable - a backlink to the parent Container, set dynamically ATM) and
  4. it is a state machine.

State machines are susceptible to the "state explosion problem". Harel's StateCharts are one way to solve this problem (i.e. using nesting and Structuring). Each ė Component is asynchronous, but, might contain synchronous code in its internal implementation.

HSMs help organize the synchronous code (only the synchronous code). HSMs are like StateCharts minus the "othogonal state" stuff (concurrency). You don't NEED to use HSMs for the synchronous code, but HSMs are like Structured Programming - a suggestion for organizing synchronous code.

One Component is one ė Component. There is only one input queue and one output queue. The rest of the innards of the Component are "code" (synchronous code).

We are accustomed to writing code - synchronous code - by writing functions. Nothing is different about this kind of code. If the special function Send() is used, it reaches upwards and drops messages onto the enclosing ė Component's Output Queue.

All of the code we currently write is function-based. It is synchronous. We wrap such function-based code in Components called Processes (Threads, etc.). ė is just a way to make the wrappers smaller, using anonymous lambdas, inheritance, etc.

I view Processes as ad-hoc closures. ė "cleans that up" and makes the wrappers less ad-hoc.

To this end, Initialize is a multi-headed beast. You have to initialize the synchronous code and the asynchronous code. Then, you might want to Reset ė Components to snap them back to some "known state". I haven't looked, but I would bet that Erlang OTP does something like this. It should be possible to collapse all of the initialization stuff into one lump of code and to, additionally, provide a Reset method. I have chosen to keep the initializations separated in the hopes of making the purpose of the code more clear.

A goal is to make each stand-alone layer of code understandable and immutable (it always does what the code says it does). This goal causes HSMs to have reverse-inheritance - parental authority - instead of the usual Inheritance behaviour that we're used to having (Children can override methods defined in the Parent). Messages are received by the top-most ė Component and punted to children if the parent doesn't care to deal with the message (example: lamp - OFF/ON dealt with at the top-most level, INTENSITY dealt with by Children layers).

This goal, also, implies that a Parent must be able to drag all of its Children (recursively) out of whatever State they are in. The Exit() method is meant to be this recursive, depth-first dive.

Likewise, when an ė Component is activated, it must cause all of its Children (recursively) to Enter their default states. The Parent calls Enter() for this recursive, depth-first dive.

In Python, this is easy to write:

  1. on Enter(), the Component initializes its own data, then calls its Children to Enter()
  2. on Exit(), the Component calls its Children to Exit(), then does its own clean-up (note that the order of calls in Enter() and Exit() is subtly different)

Reset() is simply a call to Exit(), then, a call to Enter() on the Default State.

Note that an HSM "remembers" what its Default State is supposed to be, so that it can Reset() back to it on demand.

Initialization happens only once at the beginning of time. Reset() can happen more than once. This combination allows an ė Component to remember history instead of wiping the slate clean. In electronics, Initialization is called cold-start and Reset() is called warm-start. Harel's StateChart notation uses a History (H) bubble to access history. ė lets you be more ad-hoc (that's probably not a good thing and should be addressed in the future) by using mutable variables.

This kind of thinking goes beyond what we are currently accustomed to doing with (only) functions. Actors and Processes nip at the heels of this kind of thinking. StateCharts are a more-structured way of thinking this way. Note that StateCharts are VPLs.

Aside: Structured Programming gave us a way to nest (layer) control flow. HSMs give a way to nest control flow.

Aside: You can draw sensible drawings of ė Component networks. You can't draw sensible drawings of synchronous code. The big (but subtle) difference is 0D - using FIFOs (queues) instead of LIFOs (stacks). To draw technical diagrams, you need to use FIFOs. You can use 0D for textual code and skip the technical drawings, if you wish. I claim that after people get used to the idea of 0D, they will choose to draw programs instead of coding programs up as text. We'll see... (Aside: all Real Engineering professions use technical drawings - blueprints, molecule diagrams, electronics schematics, etc.)

duplicated in: https://publish.obsidian.md/programmingsimplicity/2022-11-18-ė+Principles

RAbraham commented 1 year ago

This is great! can a function change the values of the free variables in it's closure? I think they can according to SICP but I wanted to confirm with you too. What's your definition of closure? (perhaps a blog post just for the definition of closure would be good as it may have different subtle variations in meaning? https://en.wikipedia.org/wiki/Closure_(computer_programming)) if a function can can change the values of the free variables in it's closure, then a closure and an bject are the same?

It's interesting how going from LIFO to FIFO can simplify programming. That's something I'll have to mull over more deeply.

I think the next step for me would be to build a small application with py0d. What's a good simple use case to play with? Would you want to package py0d as a python library to be installed in pip pip install py0d or is this a prototype/throwaway project?

guitarvydas commented 1 year ago

can a function change the values of the free variables in it's closure?

No. If you want to change a value, that is "mutation". Use another notation, don't try to butcher the FP mutation to add "mutation".

I would not be surprised if adding "mutation" to FP is the cause of bloatware.

Mutation is necessary, but, ad-hoc mutation is bad.

Pick off mutation use-cases and give them a unique syntax.

For example, HSMs are a use-case for mutation. A "state" is a valid reason for needing mutation. HSMs wrap "state" with syntax that allows "state" but puts a constraint on what you can do with "state", e.g. update it, check it/case on it (making sure that you've addressed every possible angle)). The "update" operation can check that only "state" is being updated and does not allow more ad-hoc use of variable mutation. The case-on-state operation can insist that you list every possible state and explicitly write code for each (including "pass"), instead of something more ad-hoc like "if-then-else".

...What's your definition of closure? ...

I use the terms "closure" and "anonymous function" interchangeably. The correct meaning of closure is more restricted than "anonymous function" - closures are arranged so that they can be pre-compiled, whereas "anonymous functions" cannot be - in general - pre-compiled.

McCarthy's Lisp 1.5 invented "anonymous functions" (he called the Lambdas). McCarthy's "anonymous functions" had to - in general - be interpreted. Later, it was found that a certain use-case of "anonymous functions" could be pre-compiled (pre-compiled ≣ "static") and everyone jumped on that bandwagon.

I am not so sure that pre-compiling is always a good thing. For example, I find it to be OK to use interpreted languages to give me "more power" during development. I wouldn't want to leak interpretation out to end-users, so I would want to compile developed code into a smaller/faster product for end-users. I argue that worrying about pre-compilation too early in the development process is bad, it is "premature optimization" and stunts what your imagination. The pre-compiler might be some kind of barnacle that ingests working code and spits out smaller/faster working code. Barnacles might be invented for helping developers, e.g. type checkers and linters. Twisting a language design to appease only pre-compilation is not OK in my book. At the moment, all of our programming languages are compiler-appeasement languages and insist that developers waste time (and imagination) on dealing with compiler-appeasement and pre-compilation issues, long before the program works.

Barnacle-like pre-compilation was researched in the mid-1900s with work like Fraser/Davidson peephole technologies^rtl. This was called RTL and formed the basis of gcc. Cordy's Orthogonal Code Generator is another work along these lines.

Peepholing is so easy that I built a peepholer using AWK^awk for my Small-C-like compiler (for the 8080).

Cordy's OCG defines a declarative tree structure, called MIST, for choosing code sequences that line up with what the target is capable of doing (declarative portability).

RTL and OCG work in 2 passes. In pass 1, generate code for a ficticious architecture. Then in pass 2, clean up the code for a specific architecture. RTL treats everything as a register in pass 1, Cordy's OCG uses something more general (but equally abstract) called Data Descriptors and Condition Descriptors.

Data Descriptors embody allocation information in the data structure, i.e. is the datum a function parameter?, is it a temporary?, is it on the heap?, should it be put in a registers?, etc., etc. Compilers need to figure this kind of stuff out anyway. DDs embody such hard-won knowledge in the DD notation itself. In my mind, this leads to simplification, e.g. if you want to deal with the issue of allocation, you simply concentrate writing code only for that issue and augment the DDs. Allocation becomes a filter that tweaks DDs. The allocator breathes in generic DDs and spits out DDs containing more information. Further, if your DD notation is syntax-based (a DSL), then the job devolves into a job of text manipulation.

I, currently, think that all programming languages should be based on orthogonal principles:

  1. operations (syntax)
  2. operands (OO objects+methods). I think that OPLs (Orthogonal Programming Languages) would simplify language design and make it easy to create SCNs (little DSLs) in an afternoon.

I believe that lifting developers out of the 1950s and into the 2022s will leak into the writing of better apps for end-users (the trickle-down theory of Computer Science :-).

People who know me, know that I champion the use of diagrammatic programming languages.

Yet, the above discussion appears not to be related to diagrams.

Why?

It's because diagrams - in my mind - ar just syntax.

When I want to write a = b + c, I write it in text form.

When I want to write a wrapper that encloses a piece of code, I want to draw a box or a circle. I don't want to write this kind of thing in text (e.g. lambda).

guitarvydas commented 1 year ago

It's interesting how going from LIFO to FIFO can simplify programming.

Yes, it is so simple that I resisted the concept. I was sure that it had to be more complicated than that.

I've been thinking about this for years, and, to see the concepts reduce to a simple change from LIFOs to FIFOs was almost discouraging.

The reality is that I love /bin/*sh, and I love concurrency and I am very interested in FBP. All of these technologies have one thing in common - they use FIFOs for inter-component communication.

Functions, though, are based on the use of LIFOs (stacks). Pure FP is stack-based - call a function, push a return-bookmark on the stack. (Aside: that's mutation, btw. And, if your hands aren't sufficiently tied behind your back, realize that the call-stack is a global variable baked into hardware)

It is possible to write code for communicating components using functions, e.g. the invention of "operating systems", but this make the result less-simple and more-complicated than necessary.

For me, reading Harel's original paper on StateCharts^harel was a life-changing event. The paper shows that it is possible to write serious code as technical diagrams, and, it is possible to think about code using something other than inheritance as we have come to know it. Harel doesn't point out either of these issues, I had to let them ooze into my psyche over time.

guitarvydas commented 1 year ago

I think the next step for me would be to build a small application with py0d. What's a good simple use case to play with?

Simple example:

  1. a Lamp
  2. it has 2 toggle buttons
    1. power ON/OFF
    2. intensity (low, medium, high)

When you hit the power button, it turns the lamp on. When you hit the power button again, it turns the lamp off.

When the lamp is ON, hitting the INTENSITY button cycles between LOW-MEDIUM-HIGH intensity.

When the lamp turns OFF, the inner INTENSITY state is reset (to the "default"). You can imagine more than this, e.g. Harel's History notation, but this is enough for a simple example.

guitarvydas commented 1 year ago

Would you want to package py0d as a python library to be installed in pip pip install py0d or is this a prototype/throwaway project?

I don't know how to make pip installable code. I don't know what the implications are.

Py0d is probably useful in its own right, but, I see it as a gateway drug to more-general ė concepts, such as drawware.

If py0d is not too broken, it should be made available.

RAbraham commented 1 year ago

Barnacles might be invented for helping developers, e.g. type checkers and _lint_ers. Twisting a language design to appease only pre-compilation is not OK in my book. At the moment, all of our programming languages are compiler-appeasement languages and insist that developers waste time (and imagination) on dealing with compiler-appeasement and pre-compilation issues, long before the program works.

Barnacle-like pre-compilation was researched in the mid-1900s with work like Fraser/Davidson peephole technologies1. This was called RTL and formed the basis of gcc. Cordy's Orthogonal Code Generator is another work along these lines.

I just wanted to understand the sentiment. Is it compiler appeasement also if we design our language to make it easy to build type checkers and linters? or the ideal programming language is dynamic (with linters and type checkers that can be invoked dynamically)

guitarvydas commented 1 year ago

Barnacles might be invented for helping developers, e.g. type checkers and _lint_ers. Twisting a language design to appease only pre-compilation is not OK in my book. At the moment, all of our programming languages are compiler-appeasement languages and insist that developers waste time (and imagination) on dealing with compiler-appeasement and pre-compilation issues, long before the program works.

Barnacle-like pre-compilation was researched in the mid-1900s with work like Fraser/Davidson peephole technologies1. This was called RTL and formed the basis of gcc. Cordy's Orthogonal Code Generator is another work along these lines.

I just wanted to understand the sentiment. Is it compiler appeasement also if we design our language to make it easy to build type checkers and linters? or the ideal programming language is dynamic (with linters and type checkers that can be invoked dynamically)

  1. Type checkers are helpers for development. Similar to parsing and syntax checkers. Type checkers are - currently - more difficult to write than syntax checkers. There was a time when syntax checking was not well understood and was difficult and was written in an ad-hoc manner. Early FORTRAN had syntax that we would consider weird today. Early FORTRAN did not treat spaces specially and went for the shortest match (I think). For example IF and IFX were both parsed as the beginning of an IF statement. Later, it was discovered that making spaces "special" would help in making parsers and would help in cleaning up silliness like IF and IFX. At the time, the character set consisted of ASCII (well, there was EBCDIC, championed by IBM, but, IBM was hated even more than Microsoft and Apple are hated today, so EBCDIC was mostly avoided by non-IBMers). The fact that ASCII only has 128 characters to choose from (some 32 "unprintables" must be subtracted from this count) made for silly decisions like denoting strings with the same beginning and ending quote (which makes parsing more difficult) and not-allowing spaces to be embedded in names. With Unicode, we have many more choices, but, we remain stuck with decisions made to appease 1950s hardware. Aside: in 2022, we have hardware that can handle vector graphics and overlapping graphical elements, e.g. windows and very-small windows ("buttons", "widgets"), but, we are stuck with decisions made to appease 1950s hardware. I argue that we should be building languages based on vector graphics instead of non-overlapping characters. SVG is a simple example of something that might work on this front (rectangles, ellipses, lines, text, groups). Aside: "declaration before use" is a result of 1950s thinking (save CPU time by making compilers 1-pass), even though, in 2022, we could easily burn CPU cycles to figure out "declaration after use". Aside: declaration-checking (before or after use) is only a helper for developers. The machine doesn't care if you make a typo. "Declaration-checking" is an app to help developers stamp out simple errors (like typos). Demanding that programmers rearrange their code so that the declarations ALL come before the code is compiler-appeasement (based on 1950s hardware).
  2. The best way to write a type checker is to use a Relational Language (like PROLOG, miniKanren, Datalog, etc., etc.). Relational languages are shining examples of languages that don't appease compilers. In a relational language, you write relations ("truth") and let the underlying system figure out how to implement the machinery for matching up the relations. Other technologies that bark up this same tree: declarative languages and ML. (Aside: oh my, HTML is a declarative language. But, HTML needs to lean on JavaScript to allow imperative break-outs).
  3. There is no "ideal language". The notation you use depends on the problem you are trying to solve. A simple example would be the idea of Spreadsheets vs. Lambda Calculus. Accountants and financial analysts like Spreadsheets. Programming rigor analysts like Lambda Calculus. Accountants would not want to use Lambda Calculus and rigor-ists would not want to use Spreadsheets. Another example, closer to my heart, is the difference between using Language Theory to generate parsers and using PEG to generate parsers. Language Theory-based parsers cannot do what PEG-based parsers can do (for example, parse balanced parentheses). Trying to force-fit language theory onto parsing has stagnated the field. Most languages look the same, with minor differences. (Aside: PEG is Parser Theory, not Language Theory, despite the superficial simliarities in syntax).
  4. "Dynamic" languages are "good" for fast turnaround on ideas, but are "bad" for producing end-user apps which are cost-optimized. "Static languages" are "good" for Production Engineering apps, but, are "bad" for inventing new products. Trying to force-fit all use-cases into one language results in a watered-down notation which isn't particularly good for either use-case. (Aside: at the moment, efforts to force-fit all use-cases into one language favour the Production Engineering side over the Design side of things, and, this is what I call "compiler appeasement". When programmers have to stop and rearrange their inventions to help the compiler figure out how to optimize, they are appeasing the compiler). (Aside: if Physicists ALL worshipped functional notation, we wouldn't have Feynman Diagrams, nor Polyani's "Order Out of Chaos", etc.).