[Calculation Model] %90 Silver Bullet


The complexity of software is hierarchical, first of all the difficulties from Data-Set, Data-Relation, Algorithm, Procedure, they are an essential part of Software Entities.

"The Mythical Man-Month" discusses the core 10% - "There is no silver bullet...complexity, consistency, variability and invisibility, these cannot be avoided in modern software systems."

The remaining 90% comes from organizational disorganization: "[programmers] try to model these realities, building equally complex programs that actually hide, rather than resolve, these disorganized situations".

So where is the 90% silver bullet?

Is inherit (inheritance) a silver bullet? However, "The Java Programming Language, Second Edition" states: "The language does not prevent you from deeply nesting (inheriting) classes, but good taste should... Nesting more than two levels is a readability disaster, possibly forever Nesting should not be attempted." From this we can see that inheritance is an anti-pattern;

Is OO the silver bullet? But the complexity of OO will increase with the scale index level, and OO does not conform to the substitution model, and not all entities need unique identities (for example, structured entities such as tuples in databases are identified by their content rather than names of);

Is ADT with Stateful at all times a silver bullet? However, pure stateful transformation will make the semantics of objects distributed in time and space, making it extremely difficult to understand and reason;
is it a silver bullet to always use Supply-driven (eager execution)? However, the input may be an infinitely growing stream (streams);

Is having to use atomic operations (simple/reentrant lock, monitor, transaction, ACID) to keep thread safe a silver bullet? However, using dataflow execution and lazy execution, we can always achieve implict synchronization without having to deal with various complicated concurrency issues.

"Concepts, Techniques, and Models of Computer Programming" gives the answer to this question, which is the principle of minimum expression:

When programming components, the correct model of component computation is the least expressive model that produces natural programs.

Rule of least expressiveness When programming a component, the right
computation model for the component is the least expressive model that
results in a natural program.

Start with the Declarative Model

The so-called Declarative is to program by defining what (the result we want to achieve) without explaining how to achieve it (algorithms, etc., which need to achieve the result).

The main advantage of declarative programming is that it greatly simplifies system construction. Declarative components can be built and debugged independently of each other. The complexity of the system is then the sum of the complexities of its components.

Iterative 、Recursive 、 higher-order programming

The technologies for implementing Declarative include Iterative, Recursive, higher-order programming, etc.

There is no need to go into details about recursion, and iterative can be realized through a schema:

fun {Iterate Si}
	if {IsDone Si} then Si
	else Si+1 in
		Si+1={Transform Si}
		{Iterate Si+1}
	end
end

The power of this approach is that it can separate general control flow from specific usage.
Beyond that, there is a fundamental difference between declarative iteratives and imperative loops (i.e. loops in imperative languages ​​like C or Java). In an imperative loop, the loop counter is an assignable variable that is assigned a different value on each iteration. The imperative loop is quite different: on each iteration, it declares a new variable. All these variables are referenced by the same identifier. There is no destructive assignment at all.
This difference can have major consequences (that's side effects).

higher-order programming

Friends who have studied fp should know that we generally call higher-level abstractions/procedures first/higher, such as Higher kinded type, first class function, and so on.
Higher order programming has four basic operations:

  • Procedural abstraction: the ability to convert any statement into a procedural value.
  • Versatility: the ability to pass procedure values ​​as arguments to procedure calls.
  • Instantiation: The ability to return a procedure value from a procedure call.
  • Embedding: The ability to place process values ​​into data structures. Including: explicit lazy evaluation, modules (put a set of operations together), Software component (accept a set of modules as parameters and return new modules)

With these capabilities, we can thus encode declarative ADTs: such as stack, dictionary, etc.

control access

Since declarative has no state, it cannot define private/public/protected as in OO, so how does it perform access control?
The answer is read-only view and Capabilities, we can use Wrap/Unwrap to encrypt/decrypt when atd is outgoing/incoming. For example, in the E language, there is the concept of sealer/unsealer, and the sealer/unsealer performs this process through an asymmetric algorithm.
The concept of Capabilities refers to an unforgeable linguistic entity that entitles its owner to perform a given set of actions.
The only way "connectivity begets connectivity"
to get new functionality is to explicitly pass it through existing functionality.

Declarative Concurrency Model

Declarative Concurrency

Strictly speaking, Declarative Concurrency is not Declarative, because the basic principle of Declarative programming is that the output of a declarative program should be a mathematical function of its input. However, the input to the program may be a stream! for example:

fun {Double Xs}
	case Xs of X|Xr then 2*X|{Double Xr} end
end
Ys={Double Xs}

This program is Partial termination (partial termination), if the XS input continues, then this program will be able to continue to calculate.

So what is Declarative Concurrency? The following is a more formal definition:

A concurrent program is declarative if the following applies to all possible inputs. All executions with a given set of inputs have one of two outcomes: (1) none of them terminate, or (2) they all eventually partially terminate, giving logically equivalent results. (Different executions may introduce new variables; we assume the new variables at corresponding positions are equal.)

The so-called logical equivalence refers to:

For each variable x and constraint c, we define the value (x, c) as the set of all possible values ​​that x can have, provided that c remains constant. Then we define:
two constraints c1 and c2 are logically equivalent if: (1) they contain the same variables, (2) for each variable x, values(x,c1)=values(x,c2).
For example: x = foo(y w) ∧ y = zlogically equivalent tox = foo(z w) ∧ y = z

Streams、Transducers和bounded buffer

The most useful technique for concurrent programming in Declarative Concurrency is the use of streams to communicate between threads. A stream is a potentially infinite list of messages, that is, it is a list whose tail is an infinite data stream variable.
Send a message by expanding the stream to an element: bind the tail to a list pair containing the message and the new unbound tail. To receive a message is to read a stream element. A thread that communicates via a stream is a kind of "living object", which we call a stream object. Since each variable is bound by only one thread, no locks or mutexes are required.
Stream programming is a very general approach that can be applied in many fields. This is the basic concept of Unix pipes. Morrison used it to great effect in Unix, which he called "stream-based programming".
The simplest example is this:

fun {Generate N Limit}
	if N<Limit then
		N|{Generate N+1 Limit}
	else nil end
end
fun {Sum Xs A}
	case Xs
	of X|Xr then {Sum Xr A+X}
	[] nil then A
end
end
local Xs S in
	thread Xs={Generate 0 150000} end % Producer thread
	thread S={Sum Xs 0} end % Consumer thread
	{Browse S}
end

insert image description here

It consists of the simplest producer/consumer.
We can filter streams through Transducers, and at this time the two ends are called stream-source and stream-sink.

for example:

fun {Sieve Xs}
	case Xs
	of nil then nil
	[] X|Xr then Ys in
		thread Ys={Filter Xr fun {$ Y} Y mod X \= 0 end} end
		X|{Sieve Ys}
	end
end
local Xs Ys in
	thread Xs={Generate 2 100000} end
	thread Ys={Sieve Xs} end
	{Browse Ys}
end

If we want to achieve the effect of lazy evaluation, we can add another buffer:
insert image description here

In eager execution, the producer is completely free: there is no limit to how far it can override the consumer. In lazy execution, the producer is completely constrained: it cannot produce anything without an explicit request from the consumer. Both techniques have problems. We can use bounded buffer to overcome the problems of both. This technique is relatively common, so it is not shown here.

Demand-Driven

Inspired by lazy exection, we found that
the best way to build an application is often to build it in a data-driven way around a requirements-driven core.
This is what is often called demand-driven.
Demand-driven can be implemented through a technology called trigger, which follows the by-need protocol and is released when the variable is constrained.
insert image description here

Among them, the variable must have the property of monotonic: it can be assigned only when it is unbound+needed, and thus becomes the determined+needed state.

Declarative Computational Model and Its Expressive Capabilities

According to eager/lazy calculation and sequential/values/dataflow, the Declarative calculation model can be divided into six types
insert image description here

The so-called dataflow variables refer to variables that have not yet bound values.
in:

  • In eager+sequential, the declaration, assignment and calculation of variables are a process together
  • In eager+sequential/concurrent+dataflow variables, variable declarations are separate, while assignment and calculation are together
  • In lazy+sequential/concurrent+values+dataflow variables, declaration, assignment and calculation are all separate processes

The key to understanding this is expressiveness. lazy allows declarative computation with potentially infinite lists. lazy can implement many data structures as efficiently as explicit state, but still be declarative. Dataflow variables allow writing concurrent programs that are still declarative. Using both simultaneously allows writing concurrent programs consisting of stream objects communicating over potentially infinite streams.
In effect, dataflow variables and lazy add a weak form of state to the model. We constrain this state, so we can ensure that the model is still Declarative.

Disadvantages of Declarative

A program is valid if its performance differs by a constant from the performance of an assembly language program for the same problem. If very little code is required to deal with technical reasons unrelated to the problem at hand, then the program is natural. Let us consider efficiency and nature issues separately. There are three naturalness issues: modularity, uncertainty, and interfacing with the real world.
The Declarative model is always good unless naturalness issues or performance issues are serious. (Although the Declarative model is not always natural and efficient)

Use impedance matching

Impedance matching is to embed a stateless/stateful model in a stateful/stateless model.
For example:
• Use sequential components in a concurrent model. For example, an abstraction could be a serializer that accepts concurrent requests, delivers them in order, and returns responses properly.
• Use declarative components in a stateful model. For example, an abstraction could be a storage manager that passes its content to a declarative program and stores the result as its new content.
Impedance matching is used extensively in Ericsson's Erlang project, which takes declarative programs written in a functional language and makes them simultaneously stateful, concurrent, and fault-tolerant.

to sync

In a declarative, synchronization can be done by:
insert image description here

Message Passing Concurrency Model

NewPort and Send

Streams are both declarative and concurrent. But it has a limitation that it cannot handle observable uncertainties.
We can remove this limitation by extending the model with an asynchronous communication channel. Then, any client can send messages to the channel, and the server can read messages from the channel. We use a simple channel called a port with associated streams. Sending a message to a port causes the message to appear on the port's streams.
The extended model is called the Message-Passing Concurrency model. Since this model is non-deterministic, it is no longer declarative. A client/server program can give different results in different executions because the order in which the clients send is not determined.
A useful programming style for this model is to associate a port to each stream object. This object reads all its messages from the port and sends messages to other stream objects through its port. This style retains most of the benefits of the declarative model. Each stream object is defined by a declarative recursive procedure.
Another style of programming is to work directly with models, programming with ports, dataflow variables, threads, and procedures. This style is useful for building concurrency abstractions, but is not recommended for large programs because it is difficult to reason about.
A Message-Passing Concurrency can be realized by adding NewPort and Send in Declarative.
{NewPort SP} creates a Port through Stream S and entry P, and {Send PX} appends X to the Stream corresponding to Port P.
By adding these two primitives, we can map to each component and collect the results.

AL={Map PL fun {$ P} Ans in {Send P query(foo Ans)} Ans end}

Concurrent Component Programming

The first step in designing a concurrent application is to model it as a set of concurrent activities that interact in a well-defined way. Each concurrent activity is modeled by a concurrent component. Components can be Declarative (no internal state) or have internal state. The science of programming with components is sometimes called Multi-Agent Systems, often abbreviated as MAS.
Models have basic components and various methods for composing components. Basic components are used to create port objects.
Concurrent components communicate through interfaces, which consist of a pair of input and output, collectively called wires, which connect the input and output.
There are four basic operations in component programming:

  • instantiation. Used to create an instance of the component.
  • composition. Combine other components into new components.
  • linking. Combine components by linking inputs and outputs.
  • Restriction. Restricts component input and output visibility to a set of components.

Stateful Model

Explicit State

What is explicit state?
An explicit state is a state that exists in the life cycle of multiple procedures, but is not used as a parameter.
A Stateful Model can be realized by adding NewCell and Exchange in Declarative.
The semantics of NewCell is to create a cell for storing values, and Exchange can change the storage state of this cell.

The principle of abstraction

The principle of abstraction is the most successful principle of system construction for agents with limited thinking ability such as humans.
It breaks down any system into two parts: specification and implementation. What differentiates a specification/implementation is that a specification is usually easier to understand than an implementation.
What properties should a system have to best support the principle of abstraction?

  • Encapsulation package. Encapsulation should be able to hide the internals of the part.
  • Compositionality Composition. It should be possible to combine parts to make new ones.
  • Instantiation/invocation instantiation/invocation. Multiple instances of a part can be created from a single definition. These instances are "plugged" into their environment when they are created.
    These three properties define component-based programming.

Component-based programming

Three properties of encapsulation, composition, and instantiation define component-based programming. A component specifies a piece of program that has an internal and external, ie an interface.
Components exist in three forms:

  • Procedural abstraction We have seen the first example of components in the declarative computing model. This component is called a procedure definition, and its instances are called procedure calls. Process abstraction is the basis for more advanced component models that emerged later.
  • Functor (compilation unit) A particularly useful component is a compilation unit, that is, it can be compiled independently of other components. In this book, we refer to such components as Functors and their instance modules.
  • Concurrent components A system with independent, interacting entities can be viewed as a graph of concurrent components that send messages to each other.
    Yes, as you think,
    Procedural abstraction+Functor+Concurrent components=Object-based programming
    and Object-based programming + Inheritance = Object-oriented programming

Types of ADTs

According to ADT's security (open/secure), state (declarative/stateful), and whether it is bundled with data, it can be divided into eight types of ADT, and the following five types are commonly used:
insert image description here

An ADT created using stateless cannot modify its instance.
The advantage of this is that when an ADT instance is passed to a procedure, it can be determined exactly what value is being passed. Once created, an instance cannot be changed. This ensures transparency and facilitates program reasoning. On the other hand, this can lead to unmanageable instance surges. The program is also less modular, since instances must be passed around explicitly, even through parts that may not need the instance itself.
Stateful ADTs use explicit state internally. Examples of stateful ADTs are components and objects, which are often stateful. Using this approach, ADT instances can change over time. Without knowing the history of all procedure calls since the interface was created, it is impossible to determine what value is encapsulated in the instance. This makes the program more concise. The program is also potentially more modular, since parts that don't need an instance don't need to mention it.
ADTs that bundle data are heavier, and manipulating them will take more time.

Shared-State Concurrency Model

Shared-State Concurrency


A Shared-State Concurrency Model can be realized by adding NewCell and Exchange to Declarative Concurrency .

Why not stick to Declarative concurrency

Given the inherent difficulties of programming in the shared-state concurrency model, an obvious question is why not stick to the declarative concurrency model from Chapter 4? It is easier to program than the shared state concurrency model. Reasoning is almost as easy as with declarative models, which are sequential.
Let's briefly examine why the declarative concurrency model is so simple. This is because dataflow variables are monotonic: they can only be bound to one value.
Once bound, the value does not change. Thus, threads sharing a dataflow variable (such as a stream) can use the stream to perform computations as if it were a simple value.
This is in contrast to non-monotonic cells: they can be assigned an arbitrary number of mutually unrelated values. Threads sharing a cell cannot make any assumptions about its content: at any point, the content may be completely different from any previous content.
The problem with the declarative concurrency model is that threads must communicate in a "lockstep" or "shrink" fashion. Two threads communicating with a third thread cannot execute independently; they must coordinate with each other. This is due to the fact that models are still declarative and therefore deterministic.
We want to allow two threads to be completely independent, but communicate with the same third thread. For example, we want clients to independently query a common server, or increment shared state independently. To express this, we have to leave the realm of declarative models.
This is because two separate entities communicating with a third party introduce observable uncertainty. A simple solution to this problem is to add explicit state to the model. Ports and cells are two important ways to add explicit state.

Concurrent programming Model

In general, concurrency models can be divided into the following four categories:
insert image description here

It is worth noting that coroutines, coroutining pass explicit control transfer, so it is actually a sequential model.
The message-passing and shared-state models are equivalent in expressive power. This is because Port can be implemented with Cell and vice versa. In fact, the programming concepts of the two are completely different: message-passing is to treat the program as a coordinated activity entity. Shared-state is the program as a passive data repository that can be modified in a consistent manner.

How to decide the concurrency style?

  1. Stick to the minimal concurrency model that is appropriate for your program For example, if using concurrency does not simplify your program's architecture, then stick to the sequential model. If the program doesn't have any observable non-determinism, such as independent clients interacting with the server, then stick to the declarative concurrency model.
  2. When stateful and concurrent, prefer message-passing or shared-state approaches. Message-passing approaches are often the best approach for multi-agent programs, that is, programs composed of autonomous entities ("agents") communicating with each other. The shared-state approach is usually best suited for data-centric programs, that is, programs consisting of a large data repository ("database") that can be accessed and updated concurrently. These two methods can be used in different parts of the same application.
  3. Modularize the program and focus the concurrency aspects in as few places as possible Most of the time, most of the program can be sequential, or use declarative concurrency. This can also be done using impedance matching.

Using atomic actions

To introduce Shared-State Concurrency, you must use Atomic actions
. Commonly used Atomic actions are as follows. Atomic actions are often encountered in OO, so I won’t repeat them here.
insert image description here

Guess you like

Origin blog.csdn.net/treblez/article/details/127201435