mirror of
https://github.com/fluencelabs/aqua-book
synced 2024-12-04 15:20:19 +00:00
Merge branch 'main' into alpha
This commit is contained in:
commit
a803cbcafa
@ -4,9 +4,3 @@
|
||||
|
||||
In addition to the language specification, Aqua provides a compiler, which produces Aqua Intermediary Representation \(AIR\) and an execution stack, Aqua VM, that is part of every Fluence node implementation to execute AIR.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,2 +1 @@
|
||||
# Language
|
||||
|
||||
|
@ -4,9 +4,9 @@ While [Execution flow](flow/) organizes the flow from peer to peer, Abilities &
|
||||
|
||||
Ability is a concept of "what is possible in this context": like a peer-specific trait or a typeclass. It will be better explained once abilities passing is implemented.
|
||||
|
||||
{% embed url="https://github.com/fluencelabs/aqua/issues/33" %}
|
||||
{% embed url="https://github.com/fluencelabs/aqua/issues/33" caption="" %}
|
||||
|
||||
### Services
|
||||
## Services
|
||||
|
||||
A Service interfaces functions \(often provided via WebAssembly interface\) executable on a peer. Example of service definition:
|
||||
|
||||
@ -27,41 +27,43 @@ Some services may be singletons available on all peers. Such services are called
|
||||
-- Built-in service has a constant ID, so it's always resolved
|
||||
service Op("op"):
|
||||
noop()
|
||||
|
||||
|
||||
func foo():
|
||||
-- Call the noop function of "op" service locally
|
||||
Op.noop()
|
||||
Op.noop()
|
||||
```
|
||||
|
||||
#### Service Resolution
|
||||
|
||||
|
||||
A peer may host many services of the same type. To distinguish services from each other, Aqua requires Service resolution to be done: that means, the developer must provide an ID of the service to be used on the peer.
|
||||
|
||||
```haskell
|
||||
service MyService:
|
||||
noop()
|
||||
|
||||
|
||||
func foo():
|
||||
-- Will fail
|
||||
MyService.noop()
|
||||
|
||||
|
||||
-- Resolve MyService: it has id "noop"
|
||||
MyService "noop"
|
||||
|
||||
|
||||
-- Can use it now
|
||||
MyService.noop()
|
||||
|
||||
|
||||
on "other peer":
|
||||
-- Should fail: we haven't resolved MyService ID on other peer
|
||||
MyService.noop()
|
||||
|
||||
|
||||
-- Resolve MyService on peer "other peer"
|
||||
MyService "other noop"
|
||||
MyService.noop()
|
||||
|
||||
|
||||
-- Moved back to initial peer, here MyService is resolved to "noop"
|
||||
MyService.noop()
|
||||
```
|
||||
|
||||
There's no way to call an external function in Aqua without defining all the data types and the service type. One of the most convenient ways to do it is to generate Aqua types from Wasm code in Marine.
|
||||
|
||||
|
||||
|
@ -20,18 +20,18 @@ Stream is a kind of [collection](types.md#collection-types) and can be used in p
|
||||
func foo(peer: string, relay: ?string):
|
||||
on peer via relay:
|
||||
Op.noop()
|
||||
|
||||
|
||||
-- Dirty hack for lack of type variance, and lack of cofunctors
|
||||
service OpStr("op"):
|
||||
identity: string -> string
|
||||
|
||||
|
||||
func bar(peer: string, relay: string):
|
||||
relayMaybe: *string
|
||||
if peer != %init_peer_id%:
|
||||
-- To write into a stream, function call is required
|
||||
relayMaybe <- OpStr.identity(relay)
|
||||
-- Pass a stream as an optional value
|
||||
foo(peer, relayMaybe)
|
||||
foo(peer, relayMaybe)
|
||||
```
|
||||
|
||||
But the most powerful use of streams pertains to their use with parallel execution, which incurs non-determinism.
|
||||
@ -41,34 +41,33 @@ But the most powerful use of streams pertains to their use with parallel executi
|
||||
A stream's lifecycle can be separated into three stages:
|
||||
|
||||
* Source: \(Parallel\) Writes to a stream
|
||||
* Map: Handling the stream values
|
||||
* Sink: Converting the resulting stream into a scalar
|
||||
* Map: Handles the stream values
|
||||
* Sink: Converts the resulting stream into a scalar
|
||||
|
||||
Consider the following example:
|
||||
|
||||
```haskell
|
||||
func foo(peers: []string) -> string:
|
||||
resp: *string
|
||||
|
||||
-- Go to all peers in parallel
|
||||
|
||||
-- Will go to all peers in parallel
|
||||
for p <- peers par:
|
||||
on p:
|
||||
-- Do something
|
||||
resp <- Srv.call()
|
||||
|
||||
|
||||
resp2: *string
|
||||
|
||||
|
||||
-- What is resp at this point?
|
||||
for r <- resp par:
|
||||
on r:
|
||||
resp2 <- Srv.call()
|
||||
|
||||
|
||||
-- Wait for 6 responses
|
||||
Op.identity(resp2!5)
|
||||
-- Once we have 5 responses, merge them
|
||||
r <- Srv.concat(resp2)
|
||||
<- r
|
||||
|
||||
```
|
||||
|
||||
In this case, for each peer in peers, something is going to be written into `resp` stream.
|
||||
@ -82,4 +81,3 @@ And then the results are sent to the first peer, to call Op.identity there. This
|
||||
When the join is complete, the stream is consumed by the concatenation service to produce a scalar value, which is returned.
|
||||
|
||||
During execution, involved peers have different views on the state of execution: each of the `for` parallel branches have no view or access to the other branches' data and eventually, the execution flows to the initial peer. The initial peer then merges writes to the `resp` stream and to the `resp2` stream, respectively. These writes are done in conflict-free fashion. Furthermore, the respective heads of the `resp`, `resp2` streams will not change from each peer's point of view as they are immutable and new values can only be appended. However, different peers may have a different order of the stream values depending on the order of receiving these values.
|
||||
|
||||
|
@ -2,16 +2,16 @@
|
||||
|
||||
Aqua supports branching: you can return one value or another, recover from the error, or check a boolean expression.
|
||||
|
||||
### Contract
|
||||
## Contract
|
||||
|
||||
* The second arm of the conditional operator is executed if and only if the first arm failed.
|
||||
* The second arm has no access to the first arm's data.
|
||||
* A conditional block is considered "executed" if and only if any arm was executed successfully.
|
||||
* A conditional block is considered "failed" if and only if the second \(recovery\) arm fails to execute.
|
||||
|
||||
### Conditional operations
|
||||
## Conditional operations
|
||||
|
||||
#### try
|
||||
### try
|
||||
|
||||
Tries to perform operations, or swallows the error \(if there's no catch, otherwise after the try block\).
|
||||
|
||||
@ -24,7 +24,7 @@ try:
|
||||
x <- foo()
|
||||
```
|
||||
|
||||
#### catch
|
||||
### catch
|
||||
|
||||
Catches the standard error from `try` block.
|
||||
|
||||
@ -44,7 +44,7 @@ data LastError:
|
||||
peer_id: string -- On what peer the error happened
|
||||
```
|
||||
|
||||
#### if
|
||||
### if
|
||||
|
||||
If corresponds to `match`, `mismatch` extension of π-calculus.
|
||||
|
||||
@ -53,21 +53,21 @@ x = true
|
||||
if x:
|
||||
-- always executed
|
||||
foo()
|
||||
|
||||
|
||||
if x == false:
|
||||
-- never executed
|
||||
bar()
|
||||
|
||||
|
||||
if x != false:
|
||||
-- executed
|
||||
baz()
|
||||
baz()
|
||||
```
|
||||
|
||||
Currently, you may only use one `==`, `!=` operator in the `if` expression, or compare with true.
|
||||
|
||||
Both operands can be variables.
|
||||
|
||||
#### else
|
||||
### else
|
||||
|
||||
Just the second branch of `if`, in case the condition does not hold.
|
||||
|
||||
@ -75,12 +75,12 @@ Just the second branch of `if`, in case the condition does not hold.
|
||||
if true:
|
||||
foo()
|
||||
else:
|
||||
bar()
|
||||
bar()
|
||||
```
|
||||
|
||||
If you want to set a variable based on condition, see Conditional return.
|
||||
|
||||
#### otherwise
|
||||
### otherwise
|
||||
|
||||
You may add `otherwise` to provide recovery for any block or expression:
|
||||
|
||||
@ -91,7 +91,7 @@ otherwise:
|
||||
y <- bar()
|
||||
```
|
||||
|
||||
### Conditional return
|
||||
## Conditional return
|
||||
|
||||
In Aqua, functions may have only one return expression, which is very last. And conditional expressions cannot define the same variable:
|
||||
|
||||
@ -99,7 +99,7 @@ In Aqua, functions may have only one return expression, which is very last. And
|
||||
try:
|
||||
x <- foo()
|
||||
otherwise:
|
||||
x <- bar() -- Error: name x was already defined in scope, can't compile
|
||||
x <- bar() -- Error: name x was already defined in scope, can't compile
|
||||
```
|
||||
|
||||
So to get the value based on condition, we need to use a [writeable collection](../types.md#collection-types).
|
||||
@ -111,7 +111,7 @@ try:
|
||||
resultBox <- foo()
|
||||
otherwise:
|
||||
resultBox <- bar()
|
||||
|
||||
|
||||
-- now result contains only one value, let's extract it!
|
||||
result = resultBox!
|
||||
|
||||
|
@ -6,6 +6,7 @@ In Aqua, two operations correspond to it: you can call a service function \(it's
|
||||
|
||||
### `for` expression
|
||||
|
||||
|
||||
In short, `for` looks like the following:
|
||||
|
||||
```haskell
|
||||
@ -13,13 +14,13 @@ xs: []string
|
||||
|
||||
for x <- xs:
|
||||
y <- foo(x)
|
||||
|
||||
|
||||
-- x and y are not accessible there, you can even redefine them
|
||||
x <- bar()
|
||||
y <- baz()
|
||||
y <- baz()
|
||||
```
|
||||
|
||||
### Contract
|
||||
## Contract
|
||||
|
||||
* Iterations of `for` loop are executed sequentially by default.
|
||||
* Variables defined inside `for` loop are not available outside.
|
||||
@ -27,7 +28,7 @@ y <- baz()
|
||||
* `for` can be executed on a variable of any [Collection type](../types.md#collection-types).
|
||||
|
||||
### Conditional `for`
|
||||
|
||||
For can be executed on a variable of any [Collection type](../types.md#collection-types).
|
||||
You can make several trials in a loop, and break once any trial succeeded.
|
||||
|
||||
```haskell
|
||||
@ -50,11 +51,11 @@ xs: []string
|
||||
for x <- xs par:
|
||||
on x:
|
||||
foo()
|
||||
|
||||
|
||||
-- Once the fastest x succeeds, execution continues
|
||||
-- If you want to make the subsequent execution independent from for,
|
||||
-- mark it with par, e.g.:
|
||||
par continueWithBaz()
|
||||
par continueWithBaz()
|
||||
```
|
||||
|
||||
The contract is changed as in [Conditional](conditional.md#contract) flow.
|
||||
@ -71,9 +72,9 @@ return: *string
|
||||
for x <- xs par:
|
||||
on x:
|
||||
return <- foo()
|
||||
|
||||
|
||||
-- Wait for 6 fastest results -- see Join behavior
|
||||
baz(return!5, return)
|
||||
baz(return!5, return)
|
||||
```
|
||||
|
||||
### `for` on streams
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
Parallel execution is where Aqua fully shines.
|
||||
|
||||
### Contract
|
||||
## Contract
|
||||
|
||||
* Parallel arms have no access to each other's data. Sync points must be explicit \(see [Join behavior](parallel.md#join-behavior)\).
|
||||
* If any arm is executed successfully, the flow execution continues.
|
||||
* All the data defined in parallel arms is available in the subsequent code.
|
||||
|
||||
### Implementation limitation
|
||||
## Implementation limitation
|
||||
|
||||
Parallel execution has some implementation limitations:
|
||||
|
||||
@ -19,7 +19,7 @@ Parallel execution has some implementation limitations:
|
||||
|
||||
These limitations might be overcome in future Aqua updates, but for now, plan your application design having this in mind.
|
||||
|
||||
### Parallel operations
|
||||
## Parallel operations
|
||||
|
||||
#### par
|
||||
|
||||
@ -38,7 +38,7 @@ on "peer 1":
|
||||
x <- foo()
|
||||
par on "peer 2":
|
||||
y <- bar()
|
||||
|
||||
|
||||
-- Once any of the previous functions return x or y,
|
||||
-- execution continues. We don't know the order, so
|
||||
-- if y is returned first, hello(x) will not execute
|
||||
@ -53,7 +53,7 @@ par hello(y)
|
||||
|
||||
`par` works in an infix manner between the previously stated function and the next one.
|
||||
|
||||
#### co
|
||||
### co
|
||||
|
||||
`co` , short for `coroutine`, prefixes an operation to send it to the background. From π-calculus perspective, it's the same as `A | null`, where `null`-process is the one that does nothing and completes instantly.
|
||||
|
||||
@ -64,7 +64,7 @@ co foo()
|
||||
-- Do something on another peer, not blocking the flow on this one
|
||||
co on "some peer":
|
||||
baz()
|
||||
|
||||
|
||||
-- This foo does not wait for baz()
|
||||
foo()
|
||||
|
||||
@ -79,7 +79,7 @@ bar()
|
||||
bax(x)
|
||||
```
|
||||
|
||||
### Join behavior
|
||||
## Join behavior
|
||||
|
||||
Join means that data was created by different parallel execution flows and then used on a single peer to perform computations. It works the same way for any parallel blocks, be it `par`, `co` or something else \(`for par`\).
|
||||
|
||||
@ -90,16 +90,16 @@ In Aqua, you can refer to previously defined variables. In case of sequential co
|
||||
on peer1:
|
||||
-- Go to peer1, execute foo, remember x
|
||||
x <- foo()
|
||||
|
||||
|
||||
-- x is available at this point
|
||||
|
||||
|
||||
on peer2:
|
||||
-- Go to peer2, execute bar, remember y
|
||||
y <- bar()
|
||||
|
||||
-- Both x and y are available at this point
|
||||
-- Use them in a function
|
||||
baz(x, y)
|
||||
baz(x, y)
|
||||
```
|
||||
|
||||
Let's make this script parallel: execute `foo` and `bar` on different peers in parallel, then use both to compute `baz`.
|
||||
@ -109,9 +109,9 @@ Let's make this script parallel: execute `foo` and `bar` on different peers in p
|
||||
on peer1:
|
||||
-- Go to peer1, execute foo, remember x
|
||||
x <- foo()
|
||||
|
||||
|
||||
-- Notice par on the next line: it means, go to peer2 in parallel with peer1
|
||||
|
||||
|
||||
par on peer2:
|
||||
-- Go to peer2, execute bar, remember y
|
||||
y <- bar()
|
||||
|
@ -2,16 +2,16 @@
|
||||
|
||||
By default, Aqua code is executed line by line, sequentially.
|
||||
|
||||
### Contract
|
||||
## Contract
|
||||
|
||||
* Data from the first arm is available in the second branch.
|
||||
* The second arm is executed if and only if the first arm succeeded.
|
||||
* If any arm failed, then the whole sequence is failed.
|
||||
* If all arms executed successfully, then the whole sequence is executed successfully.
|
||||
|
||||
### Sequential operations
|
||||
## Sequential operations
|
||||
|
||||
#### call arrow
|
||||
### call arrow
|
||||
|
||||
Any runnable piece of code in Aqua is an arrow from its domain to the codomain.
|
||||
|
||||
@ -31,7 +31,7 @@ z <- Op.identity(y)
|
||||
|
||||
When you write `<-`, this means not just "assign results of the function on the right to variable on the left". It means that all the effects are executed: [service](../abilities-and-services.md) may change state, the [topology](../topology.md) may be shifted. But you end up being \(semantically\) on the same peer where you have called the arrow.
|
||||
|
||||
#### on
|
||||
### on
|
||||
|
||||
`on` denotes the peer where the code must be executed. `on` is handled sequentially, and the code inside is executed line by line by default.
|
||||
|
||||
@ -39,19 +39,19 @@ When you write `<-`, this means not just "assign results of the function on the
|
||||
func foo():
|
||||
-- Will be executed where `foo` was executed
|
||||
bar()
|
||||
|
||||
|
||||
-- Move to another peer
|
||||
on another_peer:
|
||||
-- To call bar, we need to leave the peer where we were and get to another_peer
|
||||
-- It's done automagically
|
||||
bar()
|
||||
|
||||
|
||||
on third_peer via relay:
|
||||
-- This is executed on third_peer
|
||||
-- But we denote that to get to third_peer and to leave third_peer
|
||||
-- an additional hop is needed: get to relay, then to peer
|
||||
bar()
|
||||
|
||||
|
||||
-- Will be executed in the `foo` call site again
|
||||
-- To get from the previous `bar`, compiler will add a hop to relay
|
||||
bar()
|
||||
|
@ -21,7 +21,5 @@ Everything defined in the file is imported into the current namespace.
|
||||
|
||||
The `use` expression makes it possible to import a subset of a file, or to alias imports to avoid namespace collisions.
|
||||
|
||||
{% embed url="https://github.com/fluencelabs/aqua/issues/30" %}
|
||||
|
||||
|
||||
{% embed url="https://github.com/fluencelabs/aqua/issues/30" caption="" %}
|
||||
|
||||
|
@ -33,7 +33,7 @@ func foo(arg: i32, log: string -> ()):
|
||||
|
||||
## Return values
|
||||
|
||||
You can assign the results of an arrow call to a name, and use this returned value in the code below.
|
||||
You can assign the results of an arrow call to a name and use this returned value in the code below.
|
||||
|
||||
```haskell
|
||||
-- Imagine a Stringify service that's always available
|
||||
|
@ -8,7 +8,7 @@
|
||||
* chain-forward pattern
|
||||
* Note on Marine, Wasm IT
|
||||
|
||||
Given an abundance of active and abandoned programming languages, why create another one ? The need for Aqua arises from the desire to maximize the potential afforded by peer-to-peer networks as a distributed hosting environment for services composable into applications and backends.
|
||||
Given an abundance of active and abandoned programming languages, why create another one ? The need for Aqua arises from the desire to maximize the potential afforded by peer-to-peer networks as a distributed hosting environment for services composable into applications and backends.
|
||||
|
||||
Figure x: need one new graphic to illustrate both aspects
|
||||
|
||||
@ -19,11 +19,11 @@ That is, Aqua provides the capabilities necessary to implement and execute a "fu
|
||||
* Programmable network requests
|
||||
* Extensible beyond peer-native services to Web2 resources
|
||||
|
||||
At the heart of the peer-to-peer programming model -- is this Fluence or Aquamarine ?
|
||||
At the heart of the peer-to-peer programming model -- is this Fluence or Aquamarine ?
|
||||
|
||||
* _particle_
|
||||
* _particle_
|
||||
|
||||
### A Taste Of Aqua
|
||||
## A Taste Of Aqua
|
||||
|
||||
or a different example?
|
||||
|
||||
@ -38,5 +38,3 @@ func greeter(name: string, greet: bool, node: string, service_id: string) -> str
|
||||
<- res
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user