Computation Against a Neighbour: Addressing Large-Scale Distribution and Adaptivity with Functional Programming and Scala

Recent works in contexts like the Internet of Things (IoT) and large-scale Cyber-Physical Systems (CPS) propose the idea of programming distributed systems by focussing on their global behaviour across space and time. In this view, a potentially vast and heterogeneous set of devices is considered as an"aggregate"to be programmed as a whole, while abstracting away the details of individual behaviour and exchange of messages, which are expressed declaratively. One such a paradigm, known as aggregate programming, builds on computational models inspired by field-based coordination. Existing models such as the field calculus capture interaction with neighbours by a so-called"neighbouring field"(a map from neighbours to values). This requires ad-hoc mechanisms to smoothly compose with standard values, thus complicating programming and introducing clutter in aggregate programs, libraries and domain-specific languages (DSLs). To address this key issue we introduce the novel notion of"computation against a neighbour", whereby the evaluation of certain subexpressions of the aggregate program are affected by recent corresponding evaluations in neighbours. We capture this notion in the neighbours calculus (NC), a new field calculus variant which is shown to smoothly support declarative specification of interaction with neighbours, and correspondingly facilitate the embedding of field computations as internal DSLs in common general-purpose programming languages -- as exemplified by a Scala implementation, called ScaFi. This paper formalises NC, thoroughly compares it with respect to the classic field calculus, and shows its expressiveness by means of a case study in edge computing, developed in ScaFi.


Introduction
Pervasive computing, Internet of Things (IoT), Cyber-Physical Systems (CPS), Smart Cities and related initiatives, all point out a trend in informatics envisioning a future where computation is fully pervasive and ubiquitous, and is carried on by a potentially huge and dynamic set of heterogeneous devices deployed in physical space. To address the intrinsic complexity of these settings, a new viewpoint is increasingly emerging: a large-scale network against a neighbour", used to express interaction in field-based coordination by entirely replacing the notion of neighbouring value. The key idea is to allow the evaluation of a certain sub-expression of the aggregate program to depend on a recent outcome of the evaluation of the same subexpression in a neighbour. Such a dependency is hence expressed fully declaratively, without escaping the functional paradigm of field-based computing, and is then internally implemented by asynchronous message exchange across neighbours. To present this mechanism and study its implications, in this paper we: (1) define syntax, typing, operational semantics of a foundation calculus, called neighbours calculus (NC); (2) investigate the properties of the NC and its relationship with the field calculus; (3) show the advantages that NC brings in term of smooth embedding into Scala-host language chosen for its hybrid object-oriented/functional nature, flexibility, and suitability for internal DSL development [AHKY15, CRD + 17]. In particular, the last contribution is based on the implementation of the NC computational model in ScaFi 1 (Scala Fi elds). ScaFi is a Scala [Slo08] aggregate programming toolkit comprising an internal DSL for aggregate programming, which has been used in a variety of applications [CVA + 21, CTVD19, CV19,CAV18]. Hence, in addition to formalising the "computation against a neighbour" mechanism, NC also serves as formalisation of the core of ScaFi.
The remainder of this paper is structured as follows. Section 2 provides background information about Aggregate Computing and field computations, as well as motivation for this work. Section 3 describes syntax, typing, and operational semantics of NC. Section 4 provides a detailed account on the properties of NC, by comparison with the field calculus. Section 5 presents the ScaFi Scala-based NC implementation, and the key advantage of the approach over other implementations approaches for field calculi. Section 6 describes an edge computing case study built on NC/ScaFi. Finally, Section 7 summarises related works, and Section 8 ends up the paper with a wrap-up and discussion of future works.

Background and Motivation
In this section, we recap Aggregate Computing (Section 2.1) and its formal embodiment into a computational model based on a field abstraction (Section 2.2), as a response to the need of engineering the collective adaptive behaviour of large-scale distributed and physically situated systems. Then, we describe how Aggregate Computing can be used to express the paradigmatic example of self-healing channels (Section 2.3), to give an early idea of how the approach works and its main benefits (e.g., composability). Finally, we motivate the work developed in this paper in terms of goals, decisions, and technical challenges (Section 2.4).
2.1. Aggregate Computing. Aggregate Computing is an approach to CAS development that abstracts from the traditional, device-centric viewpoint (where the programs describe what a device should do) in favour of a "holistic stance" where the target of design/development is the whole collection of situated and interacting devices that compose the system, seen as a single programmable, distributed, computational body [BPV15]. The core idea is to provide a single specification -which we call an aggregate program -of how the system globally behaves. This is done without even mentioning the existence of individuals, and hence independently of the shape and size of the set of devices: it is "under-the-hood" that the local behaviours to be executed by the individual devices are derived. This approach may be labelled as a macro-to-micro one, to differentiate it from the more classical (microto-macro) approach where the components are individually implemented in order to produce the intended system-level behaviour by more or less "controlled" emergence. Hence, from the programming perspective, the key advantage is the ability of declaratively expressing the logic of an ensemble, avoiding to directly solve the generally intractable local-to-global mapping problem and clearly separating the concerns of overall aggregate behaviour from the device-specific ones.
The general idea of programming at the macro level dates back to works such as [NW04,BMG08] in the context of wireless sensor networks, but it has recently received considerable renewed interest (see, e.g., the surveys [BDU + 13,Cas22a] and the many related approaches mentioned in Section 7) with the emergence of the IoT. In this research context, the key contribution of Aggregate Computing lies in supporting abstract and resilient composition of collective adaptive behaviours. Since an aggregate program is expressed in a way that is independent of the actual number and dislocation of devicesand the resulting computation is a repetitive, gossip-like process of data distribution and computation -adaptation to changes and faults is inherent, and can be controlled by relying on specific programming patterns [VBDP15], often biologically inspired. Then, at this level, compositionality refers to the capability of combining behaviours in such a way that the result of the combination, and its properties, are in several cases fully predictable [BVPD17,VAB + 18,VCP16a]. In particular, self-stabilisation is retained by a large class of aggregate programs: it makes an aggregate system able to tolerate circumscribed failures and react to disruptive changes in the environment in order to re-establish, after some transient adjusting phase, the proper operation.
2.2. Computing with Fields. The computational framework incarnating the ideas and goals of Aggregate Computing is based on the notion of computational field (or simply field) [VBD + 19]. Intuitively, as shown in Figure 1, a computational field is an abstraction that represents a distributed data structure mapping points in space-time (the field domain) to values produced (by some device) as computation result at those points. Programming field computations hence encourages to reason in terms of evolving global structures that continuously express both the result of computation and the relationships between individuals.
Field computations generally comprise mechanisms for: (i) lifting standard values and computations, which are "local", to work as whole fields by flatly applying them to each space-time point of the domain; (ii) expressing the dynamics of fields, namely, how fields evolve over time; (iii) exchanging values across neighbours, such that information can flow beyond localities and interaction can unfold to realise complex patterns such as, e.g., outward propagation of local events; and (iv) branching fields into spatio-temporally-isolated sub-fields, in order to organise a computation into multiple parts co-existing in different space-time regions.
Inspired by minimal core calculi such as λ-calculus [Chu32] and FJ [IPW01], the above mechanisms have been formalised by the field calculus [AVD + 19]. The field calculus relies on the basic functional programming model abstractions (i.e., first-class functions and functional composition) to support composability of distributed behaviour. In a nutshell, expressions denote whole fields; a specific construct, called rep, deals with field dynamics; another construct, called nbr, declaratively expresses interaction with neighbours; finally, Figure 1. Dynamic view of a computational field: a field maps devices to values over time; devices can enter and exit a given space-time domain freely (openness); devices fire asynchronously (possibly at varying rates), execute a computational round upon fires producing the local value of the field, and then "sleep" in between fires. At a given time, a snapshot of the most-recent value at each device gives a field value (this view of an external, global observer is conceptually useful but global fields are not to be computationally manipulated in a direct fashion). A device can only directly communicate with a subset of other devices which is known as its neighbourhood (e.g., for δ 2 , represented by the green area), which usually (but not necessarily) reflects spatial proximity in situated systems.
higher-order function call captures behaviour activation as well as branching (as a "field of functions" can be invoked through the call operator, space-time regions are naturally defined by where/when the same function is actually called).
As suggested in Figure 1, field computations assume a set of networked devices that run at asynchronous or partially synchronous rounds of execution. It is such an iterative execution that, combined with repetitive local sensing and device-to-device interaction, enables intrinsic adaptation to environmental perturbations and progressive steering of the system towards the desired states-much like in swarm and computational collective intelligence [BDT99,Szu01]. In each computation round, a device: (i) determines its local context, by retrieving any previous state and collecting sensor values as well as messages potentially received from its neighbours; (ii) locally executes a field computation expression, in a contextual fashion and according to the local semantics, producing an output and an export value which provides information for collective coordination; (iii) shares the export with its neighbours, through a conceptual broadcast; and finally (iv) feeds actuators, with the produced output. In other words, this execution model implements a sort of distributed, where isSource and isDestination are assumed to be sensors available in every device. The self-healing channel algorithm can be implemented through a function channel that reuses existing building blocks of collective behaviour, such as gradient(s) (computing the self-healing field of minimum distances from the source s) and distanceBetween(a,b) (spreading in the network the distance between a and b, which can be built over gradients). It works by exploiting the triangle inequality principle: given a device at position p, the sum of the distance d 1 from endpoint1 to p (computed by the first call to gradient) with the distance d 2 from endpoint2 to p (computed by the second call to gradient) must be greater than (or equal to, if we consider degenerate triangles) the distance d 1↔2 from endpoint1 to endpoint2 (which is computed by the call to distanceBetween). So, the devices belonging to the channel are those for which d 1 + d 2 ≤ d 1↔2 , i.e., the devices that lie in the path from endpoint1 to endpoint2. Then, the execution is decentralised, progressive, and open-ended. Given the round-based execution model mentioned previously, the expression corresponding to the channel function application is to be repeatedly evaluated by every device, so that up-to-date local information (perceived by sensors or obtained from recent messages from neighbours) is properly incorporated to progressively adjust the collective result. So, local changes (e.g., a device un/becomes a source, as perceived by isSource, or the distance to neighbours changes as a result of mobility, which is perceived internally in gradients) will affect the local evaluation of the involved gradients, and such effects will propagate to neighbours, eventually affecting the rest of the network and the global channel result. Furthermore, it can be shown that this computation is self-stabilising [VAB + 18], which means that it would eventually converge to the correct global result once the environmental inputs stop changing. It turns out that gradient can be implemented in many ways [ACDV17], but a simple implementation leverages a combination of rep and nbr-this will be described in detail and incrementally in Section 3 (cf. Examples 3.1 and 3.10). Notice that the use of nbr means that an evaluation of gradient will cause pertinent information to be exchanged with neighbours. The point of this example is not to fully understand the technical details involved, which will be clarified in Section 3, but to give the intuition of the compositionality of Aggregate Computing, where functions expressing collective adaptive behaviour can be combined to express more complex collective adaptive behaviour.  Figure 2. A pictorial view of the self-healing channel. The devices where the channel yields true are shown as cyan dots, whereas the blue and green dots denote the two endpoints of the channel, and red dots denote non-working devices (e.g., as a result of a blackout). Through the two snapshots, we see that a channel eventually self-adjust itself reacting to the perturbation induced by the blackout.
2.4. Motivation. Given the background on Aggregate Computing and field computations, we are now able to provide the motivation for this work. This is done in three parts: (i) we first motivate the usefulness of internal or embedded DSLs for field computations; then, (ii) we motivate the choice of Scala as our target host language; finally, (iii) we explain technical issues that have driven the design of NC in the attempt to embed field computations with a small set of requirements at the host side.

Embedded DSLs for Field Computations.
A key issue for the practical applicability of the aggregate programming model enabled by field calculi is to find ways to smoothly integrate with the standard development practice-languages, processes and tools. We observe that an aggregate program is generally a small part of an overall application or system, addressing issues like advanced coordination and adaptivity, and interacting with other subsystems to gather inputs or provide system services [CV18]. Most former aggregate programming languages such as Proto [BB06] and Protelis [PVB15] are standalone DSLs. Also called external DSLs [Rit18], these languages have their own syntax and semantics, and are either interpreted or compiled to an intermediate language of some target platform (e.g., Java and the JVM, as in Protelis). Despite the flexibility and the support provided by modern language workbenches (cf. Xtext [Bet16]), effort in development and maintenance of an external DSL is typically large, high expertise is required, and, additionally, integration with other development languages and tools tends to be cumbersome.
A prominent, modern approach to address this problem is to devise an internal DSL [Voe13] (also called embedded DSL [Gho11]) that provides mechanisms to support the new features on top of an adequate and well-known host programming languagethrough a very expressive API. This provides easy reuse of functionality and tools of the host language, for a smooth adoption. Given a source DSL and a target language, DSL embedding is the problem of implementing the DSL internally to the target language.
Field calculi deal with fields, i.e., collections of values, which are to be treated as firstclass entities. However, common GPLs are subject to functional and typing constraints when dealing with "collections" so that point-wise manipulation typically requires widespread, explicit mapping operations which make the programs verbose. So far, this approach has been followed by the aggregate programming language FCPP [Aud20], a C++ internal DSL. The difficulties of implementing HFC as an internal DSL are partially addressed in FCPP by operator overloading, template metaprogramming techniques and macros; however, several low-level details are still left to the responsibility of the programmer.
2.4.2. Scala as Host Language. Both the syntax and the semantics of an embedded DSL are limited by the constraints exerted by its host language. Therefore, the choice of the host language is led by requirements and desiderata for the DSL. In our case these include: • pragmatism (supporting easy reuse of existing programming structures and mechanisms), • reliability (intercepting errors concerning syntax and types at compile-time), and • expressivity (offering a concise and eloquent syntax, minimising the accidental language complexity induced by the environment); which led us to select the Scala programming language. Indeed, Scala is a premier language for implementing embedded DSLs [AHKY15, Dub11, CDM + 10, CRD + 17]. It runs on the Java Virtual Machine (JVM) and thus enables straightforward interaction and reuse of libraries in the Java ecosystem. It has a flexible syntax, powerful type system, and convenient features (such as by-name parameters, type members, traits etc.) for building type-safe, expressive APIs. Additionally, its smooth integration of the object-oriented and functional paradigms [OR14], as well as its ecosystem of libraries for distributed computing, make it a convenient tool for in-large software development of frameworks. Functional programming support is especially important for implementing field calculi; however, compared with languages such as Haskell, it is more pragmatic and versatile. On the other hand, compared with very practical languages such as Kotlin, it is also (like Haskell) oriented towards programming language research and a philosophy of scalable abstractions [OZ05].

Technical Issues in Embedding Field
Computations in a Host Language. A first key problem in embedding field computations in a host language lies in the potential mismatch between the representation of local values (values of local, standard types) and the representation of field expressions. For the former, one would seek for the natural representation of the host language, e.g., literal 1 representing a local value of type Int. For the latter, one has to combine the natural representation with the additional constructs manipulating fields; namely, as in field calculus, literal 1 (or some variation) should represent a computational field equal to 1 in all space-time points of the local device's neighbourhood: this should in principle be given a type Field[Int], inheriting all the operations one would use over the local counterpart Int (+, -, and so on), since in field calculus 1+2 is the sum of two fields giving a field of 3 in all space-time points. As a consequence, standard types such as numbers, Booleans, characters, objects, and the corresponding operators, need to be coherently lifted in order to work under the field interpretation. That is, given any type T, . Ideally, the type system should continue to do its job with field types, and field expressions should be written in a notation which is analogously simple as the local counterpart. A second key problem stems from the declarativeness and compositionality of field computations: the host language should provide the means for defining blocks of code and for controlling when and how these are executed, mainly by deferring their evaluation until a later time and properly contextualising them to the local information available in each device. These mechanisms should also be lightweight so as to keep the impact on the user-side of the DSL as little as possible. For instance, using Java as host language would require many expression to be wrapped in a 0-ary lambda, which would clutter code.
The third, key technical difficulty is to properly deal with the interaction model of field computations, which is neighbour-driven. Field computations are equipped with a (logical or physical) notion of neighbourhood that basically expresses the boundary of direct communication, i.e., what devices can be reached by a given device through a communication act occurring in a certain position of computation. The observation of values that a device reads from its neighbours is typically modelled by reification into a neighbouring value (a map from neighbours to values), which can be manipulated functionally until being collapsed back to a local value by means of some folding operator-e.g., computing the minimum value. This requires one to explicitly differentiate, syntactically and semantically, the two classes of types, neighbouring types and local types, raising the issue of how to lift standard local operators to neighbour types. Thanks to the features of the Scala programming language, and as detailed in this paper, the problem could be handled as follows: • the same type, say Int, is used both for local types and neighbouring types; • the notion of computation over neighbouring values is semantically turned into a notion of computation "against" a neighbour (namely, a computation whose outcome depends on recent result of computation in that neighbour), hence there is no longer need of two kinds of type; • folding operations are the triggers for a universal quantification process, iterating computations against all pertinent neighbours. Such changes may impact theory and practice of field computations. On a practical level, they enable expressive and smooth integration with Scala programming. On a theoretical side, however, we need to investigate how these changes affect the expressiveness with respect to the field calculus [AVD + 19].

The neighbours calculus
This section presents the neighbours calculus (NC), a minimal core calculus that models Aggregate Computing via computations against a neighbour, instead of the neighbouring values used in the higher-order field calculus (HFC) [AVD + 19]-a thoughtful comparison between NC and HFC is presented in Section 4.
Devices undergo computation in rounds. When a round starts, the device gathers information about messages received from neighbours (only the last message from each neighbour is actually considered), performs an evaluation of the program, and finally emits a message to all neighbours with information about the outcome of computation. The scheduling policy of such rounds is abstracted in this formalisation, so any (synchronous or   asynchronous) scheduling policy is admissible, even though in many aggregate programming settings it is typically assumed fair 2 and non-synchronous.
Section 3.1 presents the syntax of NC; Section 3.2 presents its type system; Section 3.3 presents an operational semantics for the computation that takes place on individual devices; and Section 3.4 presents an operational semantics for the evolution of whole networks.
3.1. Syntax. The syntax of NC is given in Figure 3. Following [IPW01], the overbar notation denotes metavariables over sequences and the empty sequence is denoted by •; e.g., for expressions, we let e range over sequences of expressions, written e 1 , e 2 , . . . e n (n ≥ 0). NC focusses on aggregate programming constructs: hence, it is parametric in the set of built-in data constructors and functions. In the examples, we consider the set of built-in data constructors and functions listed (with their types) in Section 3.2. In following explanations, we use symbol δ (and variants as δ , δ 1 , . . .) to identify devices. For convenience, we also summarise meta-variables and symbols commonly used throughout the paper in Table 1.
A program P consists of a sequence F of function declarations and a main expression e. A function declaration F defines a (possibly recursive) function; it consists of a name d, 2 An infinite scheduling sequence is fair if every device appears infinitely many times in the sequence. A value can be either a data value c(v) or a functional value f. A data value consists of a data constructor c of some arity m ≥ 0 applied to a sequence of m data values v = v 1 , ..., v m . For readability, the parenthesis may be omitted for arity m = 0, writing c() as c. According to the data constructors listed in Figure 5, examples of data values are: the Booleans True and False, numbers, pairs (like Pair(True, Pair(5, 7))) and lists (like Cons(3, Cons(4, Null))).
Functional values f comprise: • declared function names d; • closed anonymous function expressions (x) => {e} (i.e., such that FV(e) ⊆ {x}); • built-in functions b, which can in turn be: pure operators o, such as functions for building and decomposing pairs (pair, fst, snd) and lists (cons, head, tail), the equality function (=), mathematical and logical functions (+, &&, ...), and so on; sensors s, which depend on the current environmental conditions of the computing device δ, such as a temperature sensor; relational sensors r, which in addition depend also on a specific neighbour device δ (e.g., nbrRange, which measures the distance with a neighbour device). In case e is a binary built-in function b, we write e 1 b e 2 for the function call b(e 1 , e 2 ) whenever convenient for readability of the whole expression in which it is contained.
The key constructs of the calculus are: • Function call: e(e 1 , . . . , e n ) is the main construct of the language. The function call evaluates to the result of applying the function value f produced by the evaluation of e to the value of the parameters e 1 , . . . , e n relatively to the aligned neighbours, that is, relatively to the neighbours that in their last execution round have evaluated e to a function value with the same name of f. Hence, calling an expression e of function type acts as a branch, where each function f obtainable from e is applied only on the subspace of devices which evaluated e to a (syntactically) identical function f. • Time evolution: rep(e 1 ){e 2 } is a construct for dynamically changing fields through the "repeated" application of the functional expression e 2 . At the first computation round (or, more precisely, when no previous state is available-e.g., initially or at re-entrance after state was cleared out due to branching), e 2 is applied to e 1 , then at each other step it is applied to the value obtained at the previous step. For instance, rep(0){(x) => {x + 1}} counts how many rounds each device has computed (from the beginning, or more generally, since that piece of state was missing).
• Neighbourhood interaction: foldhood(e 1 , e 2 , e 3 ) and nbr{e} model device-to-device interaction, and are at the core of the "computation against a neighbour" mechanism. The foldhood construct evaluates expression e 3 against every aligned neighbour (excluding the device itself), then aggregates the values collected through binary operator e 2 together with the initial value e 1 . The interaction pattern is thus a common "map (expression e 3 to each neighbours' data) and reduce (folding through e 2 )". Following standard practice, data aggregation is performed through binary folding, which smoothly covers most application scenarios. 3 Used inside e 3 , the nbr construct tags a sub-expression e signalling that, when evaluated against a neighbour δ, it should not be actually evaluated as usual, but should give as result the one obtained by evaluating e in δ. Put in other words, when evaluated against δ, nbr{e} means "observing" the recent resulting value of e in δ-and also let later δ observe the local valued of e, conversely. Such behaviour requires to effectively broadcast 4 the values evaluated for e, in order to make them accessible to neighbour devices. Subexpressions of e 3 not containing nbr are evaluated as usual instead, i.e., with no gathering of information from neighbours.
Additional syntactic sugar. To facilitate specification of examples, we write if(e 1 ){e 2 }{e 3 } as a shorthand for mux(e 1 , () => {e 2 }, () => {e 3 })(), where the multiplexer function mux selects between its second and third argument based on the value of the first, in formulas: Additionally, in order to improve readability, we also sometimes write let x = e 1 in e 2 as syntactic sugar for ((x)=> {e 2 }) (e 1 ).

3.2.
Typing. We now present a type system for NC. Since the type system is a customisation of the Hindley-Milner type system [DM82], there is an algorithm (not presented here) that, given an expression e and type assumptions for its free variables, either fails (if the expression cannot be typed under the given type assumptions) or returns its principal type, i.e., a type such that all the types that can be assigned to e by the type inference rules can be obtained from the principal type by substituting type variables with types. The syntax of type and type schemes is presented in Figure 4 (top), where B ranges over the built-in types provided by the host language (e.g., num, bool, pair(T 1 , T 2 ) for any T 1 and T 2 , list(T) for any T). We do not expand on the structure of built-in types and type operators, as it is not needed to define NC typing. So, a type T is either a built-in type B, a function type (T) → T, or a type variable t. A type scheme has syntax ∀t.T, and denotes a type quantifying over type variables. The set of type variables occurring in a type T is denoted by FTV(T). Type variables t occurring in a type T can be substituted with concrete types T through the common bracket-notation T[t := T].
.n) D n ; ∅ e : T F 1 · · · F n e : T Type environments, ranged over by A and written x : T, are used to collect type assumptions for program variables (i.e., formal parameters of functions). Type-scheme environments, ranged over by D and written v : TS, are used to collect the type schemes for built-in constructors and built-in operators together with the type schemes inferred for user-defined functions. In particular, the distinguished built-in type-scheme environment B associates a type scheme to each built-in constructor c and to each built-in function b- Figure 5 shows the type schemes for the built-in constructors and built-in functions used throughout this paper.
The typing judgement for expressions is of the form "D; A e : T", to be read: "e has type T under the type-scheme assumptions D (for built-in constructors and for built-in and user-defined functions) and the type assumptions A (for the program variables occurring in e), respectively". As a standard syntax in type systems [IPW01], given T = T 1 , . . . , T n and e = e 1 , . . . , e n (n ≥ 0), we write D; A e : T as short for D; A e 1 : T 1 · · · D; A e n : T n .
The typing rules for expressions are presented in Figure 4  = num, where n is a number or PositiveInfinity  Example 3.1 (Typing). Consider the following simple implementation of a self-healing gradient [ACDV17] (i.e., the field of minimum distances from source devices) that also circumvents obstacles. Its semantics will be presented later (see Examples 3.10 and 3.11); now, we merely consider its typing. The types of the gradient function and of the main expression inferred by the type system are inserted above as comments. By rule [T-APP] and assumptions on built-in mux, the type system infers that the third argument of the mux expression must be num, since the second argument is also num. It follows that distance must be of type num (rule 3.3. Operational Semantics: Device Semantics. This section presents a formal semantics of device computation as happens in NC. Starting from NC syntax as previously described, we assume a fixed program P. We say that "device δ fires", to mean that the main expression of P is evaluated on δ. Remark 3.2 (On termination of device computation). As NC allows recursive functions, termination of a device firing is not decidable. In the rest of the paper we assume that only terminating programs are considered.
3.3.1. Device semantics: overall picture and preliminary definitions. We model device computation by a big-step operational semantics where the result of evaluation is a value-tree θ (see Figure 6, first frame), which is an ordered tree of values, tracking the result of any evaluated subexpression. Intuitively, the evaluation of an expression at a given time in a device δ is performed against the recently-received value-trees of neighbours, namely, its outcome depends on those value-trees. The result is a new value-tree that is conversely made available to δ's neighbours (through a broadcast) for their firing; this includes δ itself, so as to support a form of state across computation rounds (note that an implementation may massively compress the value-trees, storing only enough information for expressions to be aligned). In other words, a value-tree is a data structure that embeds all of the following: (i) the final and partial results of a local computation; (ii) state resulting from a previous computation; (iii) incoming data from neighbours; and (iv) outcoming data to be sent to neighbours. Trees are in general a suitable representation for denoting programs (statically) and computations (dynamically); notice that value-trees may differ in structure if different sub-computations are chosen e.g. due to branching (cf. if). In particular, state and communication data are located at specific nodes -corresponding to rep and nbr construct applications -which are the only ones that may be actually stored and transmitted, but whose position is important for the evaluation semantics. A value-tree environment Θ is a map from device identifiers to value-trees, a local view of the outcomes of the last evaluations on neighbours, as it is locally reconstructed through received messages (no global knowledge is ever assumed). This is written δ → θ as short for δ 1 → θ 1 , . . . , δ n → θ n . The syntax of value-trees and value-tree environments is given in Figure 6 (first frame). In the following, for sake of readability, we sometimes write the value v as shorthand for the value-tree v . Following this convention, the value-tree θ 1 is shortened to True < , −2, 5, True , and the value-tree θ 2 is shortened to 4 f, 3, 4 +, 3, 1, 4 .
We assume that prior to execution each anonymous function sub-expression (x)=> {e} of a program e main is automatically annotated as (x) τ => {e} with a tag τ which uniquely identifies the expression. 5 The tag serves as a name for anonymous functions, allowing to consider function values to be equal if they share the same name. This notion of syntactic equality will later be exploited for the definition of the semantics of function calls.
Remark 3.4 (Function Equality). Notice that behavioural equivalence of function expressions is not decidable in a Turing-complete language, and thus cannot be used in a formal semantics of a (practically executable) programming language. Pure syntactic equality could instead be used, but it would fail to correctly relate multiple occurrences of an anonymous function capturing a variable, whenever that gets substituted for different values. The proposed naming system prevents those issues, while also avoiding unintended interferences between functions that happen to be syntactically identical, but were written in different units of a program and were not meant to be integrated with each other. Figure 6 (second frame) defines: the auxiliary functions ρ and π for extracting the root value and a subtree of a value-tree, respectively (further explanations about function π will be given later); the extension of functions ρ and π to value-tree environments; and the auxiliary functions name, args and body for extracting the name, formal parameters and body of a (user-defined or anonymous) function, respectively.
Example 3.5 (Auxiliary functions on value-trees). Consider the following value-trees.
The following picture graphically shows the nodes denoted by auxiliary functions working on individual value trees.
The computation that takes place on a single device is formalised by the big-step operational semantics rules given in Figure 6 (fourth frame). The derived judgements are of 5 For example, the tag could be generated as τ = (emain, n) where n is the index of the occurrence of the => keyword in emain. the form δ, δ ; Θ; σ e ⇓ θ to be read "expression e evaluates to value-tree θ on device δ with respect to the neighbour δ , value-tree environment Θ and sensor state σ", where: (i) δ is the identifier of the current device and δ is either equal to δ or is one of its neighbours; (ii) Θ is the field of the value-trees produced by the most recent evaluation of (an expression corresponding to) e on δ and its neighbours, which were received by δ through messages; (iii) e is an expression; (iv) the value-tree θ represents the values computed for all the expressions encountered during the evaluation of e-in particular ρ(θ) is the result value of e.
The operational semantics rules are based on rather standard rules for functional languages, extended so as to be able to evaluate a subexpression e of e with respect to the value-tree environment Θ obtained from Θ by extracting the corresponding subtree (when present) in the value-trees in the range of Θ. This process, called alignment, is modelled by the auxiliary function π, defined in Figure 6 (second frame). Function π has two different behaviours (specified by its subscript or superscript): π i (θ) extracts the i-th subtree of θ, if it is present; and π f (θ) extracts the last subtree of θ, if it is present and the root of first subtree of θ is equal to f.
When a device δ fires, its main expression e is evaluated with respect to δ itself. That is, by means of a judgement where δ = δ: A key aspect of the semantics is that, if e is a foldhood-expression foldhood(e 1 , e 2 , e 3 ) then its body e 3 is evaluated with respect to each of the devices δ (if any) in dom(Θ) \ {δ}. Because of alignment (see above), it might happen that a sub-expression e of e 3 is evaluated by a judgement δ, δ ; Θ; σ e ⇓ θ where δ = δ ∈ dom(Θ) and, if the evaluation of e exploits the device δ , then the evaluation of e 3 with respect to δ fails and the evaluation of the foldhood-expression foldhood(e 1 , e 2 , e 3 ) does not consider the neighbour δ . The evaluation rule for foldhood-expressions, [E-FOLD], formalises failure of evaluation with respect to a neighbour δ by means of the auxiliary predicate δ, δ ; Θ; σ e fail to be read "expression e fails to evaluate on device δ against neighbour δ with respect to value-tree environment Θ and sensor state σ", which is formalised by the big-step operational semantics rules given in Figure 7. 3.3.2. Device semantics: rules for expression evaluation. We start by explaining the rules in Figure 6 (fourth frame), then we will explain the rules in Figure 7.
Rule [E-VAL] implements the evaluation of an expression that is already a value. For instance, evaluating the expression 1 produces (by Rule [E-VAL]) the value-tree 1 , while evaluating the expression + produces the value-tree + .
Rules [E-B-APP] and [E-D-APP] model function application e(e 1 · · · e n ). In case e evaluates to a built-in function b, rule [E-B-APP] is used, whose behaviour is driven by the special auxiliary function b Θ,σ δ,δ (operational interpretation of b), whose actual definition is abstracted away. Value-trees and value-tree environments: Auxiliary functions: Rules for expression evaluation: for all i ∈ 1, ..., m δ, δ ; Θ; σ foldhood(e 1 , e 2 , e 3 ) ⇓ ρ(θ m+1 ) θ 1 , θ f , θ 0 Auxiliary rules for expression evaluation failure: Example 3.6 (Built-in function application). Evaluating the expression < (−2, 5) produces the value-tree θ 1 = True <, −2, 5, True introduced in Example 3.3. The operational interpretation < Θ,σ δ,δ of < is the following (notice that this interpretation does not depend on Θ, σ, δ, δ , since < is a pure mathematical operator): The value of the whole expression, True (the root of the last subtree of the value-tree), has been computed by using rule [E-B-APP] to evaluate the application < Θ,σ δ,δ (−2, 5) of the less-then operator < (the root of the first subtree of the value-tree) to the values −2 (the root of the second subtree of the value-tree) and 5 (the root of the third subtree of the value-tree).
In case e evaluates to a user-defined or anonymous function f, rule [E-D-APP] is used: it performs domain restriction π f (Θ) (thus discarding devices that did not apply the same function f, for which no consistent information on the application of f is present), then continues the evaluation by substituting the arguments into the body of f. We remark that we do not assume that Θ is empty whenever it does not contain δ. In fact, in any round where e evaluates to a function f for the first time on a device, f(e) will be evaluated with respect to an environment not containing δ but possibly containing other devices (whose e evaluated to f in their previous round of computation). Rule [E-REP] implements internal state evolution through computational rounds: on the first firing of a device, rep(e 1 ){e 2 } evaluates to e 2 (e 1 ), then it evaluates to e 2 (v) where v is the value calculated in the previous round. The overall result of the firing is the root 4 of θ. Any subsequent firing of the device δ is performed with respect to a value-tree environment Θ that associates to δ the outcome θ of the most recent firing of δ. Therefore, evaluating rep(3){f} at the second firing produces the value-tree θ = 5 4, θ 2 where θ 2 = 5 f, 4, 5 +, 4, 1, 5 is the value-tree produced by evaluating the expression f(4), where 4 is the root of θ. Hence, the results of the firings are 4, 5, 6, and so on.
Rules [E-NBR] and [E-NBR-LOC] model device interaction (together with [E-FOLD] which we shall consider later). When an nbr-expression is not evaluated against a neighbour (that is, δ = δ), by Rule [E-NBR-LOC] the nbr operator is discarded and the evaluation continues. Whenever instead an nbr-expression is evaluated against a neighbour (that is, δ = δ), by Rule [E-NBR] the expression directly evaluates to Θ(δ ) (which is the value-tree calculated by device δ in its last computational round for the same expression). Notice that it could be possible that δ is not in the domain of Θ due to alignment operations performed in subexpressions of the enclosing instance of foldhood. In this case, no rule is applicable and the nbr-expression fails, causing δ to be ignored by the enclosing foldhood operator (see Rule [E-FOLD] implements collection and aggregation of results from neighbours, proceeding in the following steps: • Evaluate the initial value e 1 with respect to the current device obtaining the value-tree θ 1 . • Evaluate the aggregator e 2 with respect to the current device obtaining θ f with root f. • Evaluate the body e 3 with respect to the current device δ 0 = δ, obtaining θ 0 which constitutes the third branch of the overall resulting value-tree, then with respect to every one of the n neighbours, δ ∈ dom(π 3 (Θ)) \ {δ}, and consider only the subset of m neighbours (0 ≤ m ≤ n) δ 1 , ..., δ m for which the evaluation does not fail (cf. discussion above and Figure 7), obtaining the value-trees θ 1 , ..., θ m , respectively. 7 We remark that 6 In the first firing, no messages have been yet received, and therefore the environment is empty. This fact will be later formalised and discussed in the next section on network semantics. 7 Usually, the aggregator f is associative and commutative, so that the result of the aggregation does not depend on the order in which the neighbours δ ∈ dom(π3(Θ)) \ {δ} are considered. To ensure determinism even in the unlikely case of the aggregator f being not associative and commutative, we assume that the neighbours are considered according to any given total order on device identifiers. Such an order may be, e.g., lexicographical order of the bit representations of the unique device identifiers δ (which may be, e.g., MAC addresses). the evaluation with respect to neighbours δ is performed locally, with respect to the locally-available information in Θ. • Aggregate the values ρ(θ i ) (1 ≤ i ≤ m) computed above together with the initial value ρ(θ 1 ) via function f, obtaining the final outcome ρ(θ m+1 ). Notice that when m = 0, the final outcome is the result ρ(θ 1 ) of e 1 . The aggregation is performed with respect to the current device and the empty environment, since the value-trees of the aggregation process cannot be meaningfully related with one another (and thus are not stored in the final outcome of the computation). In other words, the aggregator f is forced to be a "pure" function independent of the current device and environment (even though the expression e 2 as a whole might depend on the environment). The values aggregated by foldhood exclude the value of e 3 in the current device δ. However, an inclusive folding operation foldhoodPlusSelf can be encoded as We also remark that a sequence of nested foldhood-operators (not interleaved by nbroperators) can lead to an evaluation time which is exponential in the evaluation tree depth. 8 Failure of evaluation against a neighbour is formalised by means of the auxiliary judgement δ, δ ; Θ;  The roots of value-trees θ 1 and θ 2 are then combined through operator +, together with the initial value 2, for a total result of 2 + 10 + 5 = 17 which is the root of the final value-tree 8 In actual implementations, the outcome of foldhood and rep subexpressions can be "memoised" in order to prevent subsequent re-evaluation (since such expressions are independent of the neighbour against which are evaluated). This addresses the performance issues of nested foldhood-operators. We say that a neighbour is considered by the evaluation of a foldhood-expression to mean that it contributes to the result of the expression. Because of the interplay between neighbourhood interaction and branching (i.e., function call) only a subset of the neighbourhood of a device might be considered by a foldhood-expression. The gradient function computes the field of minimum distances (according to metric) from devices where source is True. It uses rep to keep track of the local gradient value, which is computed by looking at the corresponding value in the neighbourhood. If source is locally True, then the value is merely 0.0, since it means that the device is a source; otherwise, the gradient value is obtained by folding over neighbours (via foldhood) to collect the minimum value of nbr{distance}+metric(). The repeated application of such a function, together with actual communication (formally covered in Section 3.4) consisting of the nbr evaluations of neighbours, makes the output field eventually converge to the correct value (minimum distances from sources). The gradient is a fundamental building block for collective adaptive behaviour. It is amenable to various implementations [ACDV17] and plays a crucial role in higher-level patterns [CPVN19], as also shown in the case study of Section 6.
Example 3.11 (Neighbourhood interaction and branching). In order to illustrate the alignment process, guiding neighbour interaction through branching statements, consider the expression for a gradient avoiding obstacles, introduced in Example 3.1.
Expanding the syntactic sugar, the if statement corresponds to the execution of a different anonymous function depending on the value of isObstacle: where isObstacle is true in δ 2 and false on the other devices. Thus, the execution of the mux statement produces The evaluation of the main expression is performed through rule [E-D-APP]. First, the function to be applied is computed as the result of the mux expression. Then, the body gradient(isSource) is computed with respect to the environment π f ⊥ (Θ) = {δ 0 → π 2 (θ 0 ), δ 1 → π 2 (θ 1 )}: the value-tree of device δ 2 is removed since it corresponded to the evaluation of f . The evaluation of gradient(isSource) will then require the evaluation of the foldhood expression, in which only devices δ 0 and δ 1 will be considered (since δ 2 has already been discarded).
3.4. Operational Semantics: Network Semantics. We now provide an operational semantics for the evolution of whole networks, namely, for modelling the distributed evolution of computational fields over time. The semantics is given as a nondeterministic, small-step transition system on network configurations N . This semantics has already been given for HFC in [ABD + 20], with the only difference of referring to the HFC device semantics instead of the NC device semantics. Figure 8 (top) defines key syntactic elements to this end: • Ψ is a computational field (called value-tree field ) that models the overall state of the computation as a map from device identifiers to the value-tree environments that are locally stored in the corresponding devices. • α is an activation predicate specifying whether each device is currently activated (i.e., is performing a computation round). • Stat (a pair of value-tree field and activation predicate) models the overall computation status. • models network topology as a directed neighbouring graph, i.e. a reflexive neighbouring relation ⊆ D × D so that δ δ for each δ ∈ D. • Σ models (distributed) sensor state, as a map from device identifiers to (local) sensors representations (i.e., sensor name/value maps denoted as σ). • Env (a pair of topology and sensor state) models the network environment.
• N (a pair of status and environment) models a whole network configuration. We use the following notation for maps. Let Environment well-formedness: Transition rules for network evolution: old stored value-trees from Ψ(δ), implicitly based on space/time tags. 9 Notice that this mechanism allows messages to persist across rounds.
We define network operational semantics in terms of small-steps transitions N act − − → N of three kinds: firing starts on a given device (for which act is δ+ where δ is the corresponding device identifier), firing ends and messages are sent on a given device (for which act is δ−), and environment changes, where act is the special label env. This is formalised in Figure 8 (bottom).
Rule [N-COMP] (available for sleeping devices, i.e., with α(δ) = false, and setting them to executing, i.e., α(δ) = true) models a computation round at device δ: it takes the local value-tree environment filtered out of old values Θ = F δ (Ψ(δ)); then by the single device semantics it obtains the device's value-tree θ, which is used to update the system configuration of δ to Θ = Θ [δ → θ]. Notice that expression e main is always evaluated against the device δ itself (that is, against no neighbour), and that local sensors Σ(δ) are used by the auxiliary function b Θ,Σ(δ) δ,δ that gives the semantics to the built-in functions. Furthermore, although this rule updates a device's system configuration instantaneously, it models computations taking an arbitrarily long time, since the update is not visible until the following rule [N-SEND]. Notice also that all values used to compute θ are locally available 9 For example, the filter may remove value-trees that were stored before t − ∆t, where t is the time of the current firing and ∆t is a decay parameter of the filter. Rule [N-ENV] takes into account the change of the environment to a new well-formed environment Env -environment well-formedness is specified by the predicate WFE(Env) in Figure 8 (middle)-thus modelling node mobility as well as changes in environmental parameters. Let δ be the domain of Env . We first construct a value-tree field Ψ 0 and an activation predicate α 0 associating to all the devices of Env the empty context ∅ and the false activation. Then, we adapt the existing value-tree field Ψ and activation predicate α to the new set of devices: Ψ 0 [Ψ], α 0 [α] automatically handles removal of devices, mapping of new devices to the empty context and false activation, and retention of existing contexts and activation in the other devices. We remark that this rule is also used to model communication failure as topology changes.
Remark 3.12 (Locality of interactions). We remark that although the network semantics is given from a global perspective, it only allows for interactions that are local in nature. Rule [N-COMP] can be understood as happening on device δ, with respect to locally-available knowledge and without interactions with other devices. Rule [N-SEND] represents the broadcast of a value locally available on δ, which is then received by neighbour devices and stored in their local storage. Rule [N-ENV] does not represent an interaction at all, but only a change in the set of possible interactions.
Example 3.13 (Network evolution). Consider the program in Example 3.9: foldhood(2, +, min(nbr{temperature()}, temperature())) and let θ n = n min, n n temperature, n , n temperature, n , n be the result of evaluation of min(nbr{temperature()}, temperature()) in a device where temperature() = n (with respect to the device itself as neighbour).

Type Preservation in NC.
In this section we show that the evaluation rules for NC are deterministic and preserve types, provided that the value-tree environment used for the evaluation is coherent with the expression being evaluated according to the following definition. In other words, the above definition demands value-trees to be plausible outcomes of the evaluation of e. (1) δ, δ; Θ; σ e fail cannot hold.
Proof. The proof follows from standard induction on rules (see Appendix B.1).
By this lemma, evaluation does not result on fail when it is performed relative to the current device (as it is the case for main expressions), and rules are deterministic. Furthermore, the evaluation rules respect the types given in Figure 4, provided that the built-in interpretations respect the given types. Formally, given b such that B; ∅ b : T → T and any B; ∅ v : T, Θ = δ → v with B; ∅ v : T, δ ∈ {δ, δ}, then we require b δ,δ Θ,σ to be a value of type T. Notice that, since the evaluation of e produces a value-tree which is coherent with e, the value-tree environment Θ can be proved to be coherent with the main expression by induction on the network evolution. Furthermore, observe that the typing rules (in Figure 4) and the evaluation rules (in Figure 6 and 7) are syntax directed. Then the proof can be carried out by induction on the derivation length for δ, δ ; Θ; σ e[x := v] ⇓ θ, while using the following standard lemmas.  The fact that ρ(θ) has type T can be verified by matching step-by-step every rule in Figure 6 with the corresponding rule in Figure 4, while using the inductive hypothesis and two further assumptions: for rule [E-B-APP], that built-in functions b respect the given types; for rules [E-NBR] and [E-REP], that Θ is coherent with e.
Finally, the fact that θ has the required sub-trees follows by inductive hypothesis since every rule in Figure 6 respects the corresponding row of Definition 3.14, together with the fact that Θ is coherent with e (for rules [E-NBR] and [E-REP] only).

NC vs HFC
In this section, we provide a formal account of the relationship between NC and the HFC minimal core calculus for Aggregate Computing [AVD + 19].
In Section 4.1, we give an informal overview of the relationship between the two calculi. In Section 4.2 we recollect HFC. In Section 4.3, we define a fragment of HFC, which we call HFC , aimed at ensuring that each HFC program is an NC program that behaves in the same way. In Section 4.4, we define the fragment of NC which corresponds to HFC , which we call call NC . Then, in Section 4.5 we prove the equivalence between NC and HFC . Finally, in Section 4.6, we point out that NC provides a different "flavour" of field computation with respect to HFC, though without losing practical expressiveness. As the syntax of NC can be mapped to/from that of HFC, a question naturally arises: what are the distributed computations that are expressed in both languages by the same program? The answer to this question, detailed in the remainder of this section, is summarised in Figure 9. Not all programs are interpreted to the same behaviour in NC and HFC, however, the (subset of) programs with identical behaviour in NC and HFC can be identified. Through Lemma 4.3 in Section 4.3, we define a restricted language HFC and show that it is indeed a subset of HFC. Similarly, in Theorem 4.5 we define NC while ensuring that it is a subset of NC. Finally, Theorem 4.7 shows that HFC coincides with NC , concluding the characterisation of this common fragment of programs with identical behaviour.
We remark that HFC /NC does not coincide with the "intersection" between HFC and NC: there are programs outside HFC /NC which nonetheless result in the same behaviour in both HFC and NC. Still, we will show in Section 4.6 that most interesting programs do fit inside HFC /NC , or can be refactored to fit into it through a small set of simple rewriting strategies. In fact, we are not aware of any practically relevant program that cannot be refactored into HFC /NC through the proposed strategies. We also remark that as HFC /NC is Turing-complete, some equivalent form in HFC /NC must exist for any given distributed program. Overall, these results show that NC provides a different "flavour" of field computation with respect to HFC, without losses in expressiveness. Figure 3), with a richer syntax of values and two other minor differences: the update function e 2 in a rep construct rep(e 1 ){e 2 } is required to be an anonymous function e 2 = (x)=> {e }, 10 and the language construct for foldhood is replaced by a built-in with the same meaning, so it does not appear in the syntax. In the richer syntax of HFC values (given in Figure 10), values are divided into local values, which are the values of NC, and neighbouring (field) values, which not allowed to appear in source code and arise at runtime. Neighbouring values are maps δ → from device identifiers to local values. In HFC, they are produced by evaluating the nbr construct and returned by some built-in functions. 10 We remark that this difference is historical in nature: HFC could be straightforwardly extended to allow for general update function expressions. The Hindley-Milner type system for HFC [AVD + 19] is given in Figure 11 (excluding the grey rule [T'-FOLD]), and distinguishes between types for local values from those that are not (namely, neighbouring types F for neighbouring values), as well as between types that are allowed to be returned by functions from those that are not. This induces four different type categories: types T, local types L, return types R, and local return types S-they are illustrated at the bottom of Figure 10. The main restrictions enforced by this type system in order to ensure the domain alignment property 11 are:

A Quick Recollection of HFC. The syntax of HFC programs (according to its original presentation [AVD + 19]) is the same of NC programs (in
• anonymous functions cannot capture variables of neighbouring type; • rep statements are demanded to have a local return type; • neighbouring types can only be built from local return types S (i.e., F = field(S)), since neighbouring values need to be aggregated and this is possible only for return types, and avoiding "neighbouring values of neighbouring values" which may lead to unintentionally heavy computations; • types of the form (T) → F (functions returning neighbouring values) are not return types.
Thus, functions of type (T) → F are used almost as in a first-order language. In particular, there is no way to write a non-constant expression e evaluating to such a function. The HFC operational semantics [AVD + 19] is given as a transition system analogous to that in Section 3.4, but based on a different judgement for the device operational semantics δ; Θ; σ e main ⇓ θ. For sake of completeness, we report the details of the HFC operational device semantics in Appendix A. In the remainder of this paper, we assume that the built-in operators of HFC always include: • consthood(v), which returns a neighbouring field value constantly equal to its (local) input v; • map(f, φ), which applies a function f with local inputs and outputs (of any ariety) pointwise to neighbouring field values φ; • foldhood( , f, φ), which collapses a field φ and starting value via an aggregator f (exactly as in NC). Furthermore, we use if(e 1 ){e 2 }{e 3 } as short for mux(e 1 , () =>e 2 , () =>e 3 )(), as in NC.
.n) D n ; ∅ e : T D 0 F 1 · · · F n e : T Figure 11. Hindley-Milner typing for HFC and NC expressions, function declarations, and programs -differences with HFC typing are highlighted in grey.
R2 : Expressions of neighbouring type can only be aggregated to local values with a foldhood operator if they do not capture variables of neighbouring types; so that, e.g., aggregating arguments of neighbouring type is never allowed. Example 4.1 (About restriction R1). In order to show the rationale behind Restriction R1, consider a built-in function sorthood rearranging values φ(δ) relative to neighbours in increasing order of neighbour identifier δ, thus effectively mixing up values relative to different neighbours. Formally, applying this function to a neighbouring value φ = δ → (assuming δ 1 ≤ . . . ≤ δ n ), we obtain the neighbouring value φ = δ 1 → π 1 , . . . , δ n → πn where the permutation π is such that π 1 ≤ . . . πn . This function is conceivable (although artificial) in HFC, but it is not implementable in NC, hence it is disallowed in HFC . We remark however that all practically used built-in functions in HFC are definable with respect to those allowed by restriction R1.
Example 4.2 (About restriction R2). In order to show the rationale behind Restriction R2, consider the following HFC program def hfc_avghood(x) { // : field(num) -> num foldhood(0, +, x) / foldhood(0, +, 1) } hfc_avghood(nbr{sns-temp()}) // : num which on each device calculates the average temperature of neighbours. We may suppose that the same code, interpreted as an NC program, would calculate the same quantity. Instead, it is equivalent to the simpler NC program sns-temp(), which yields the temperature of the device where it is evaluated. If we evaluate the expression nbr{sns-temp()} against a neighbour δ , we obtain the temperature t of that neighbour. Unfortunately, in the program the expression occurs outside of the scope of any foldhood construct and so it is evaluated against the device δ where it is evaluated. When function hfc avghood is applied to t (the temperature of δ), the neighbour device δ is ignored by both foldhood constructs, which fail to interpret the captured neighbouring value as such. The value of the program on device δ is then n · t/n = t, where n is the number of neighbours of δ (including δ itself) and t is the value of the temperature on device δ.
We remark that an HFC program computing the average temperature of neighbours also when interpreted as an NC program can be conveniently written, by resorting to suitable programming patterns (illustrated in Section 4.6.2). In particular, for the example above, it is sufficient to make x a "by-name" parameter, thus obtaining the following HFC program: We remark that all HFC programs considered in previous works [AVD + 19, ABDV18, VAB + 18] actually belong to HFC (or can straightforwardly be reformulated in order to do so). The following lemma provides a characterisation of HFC in terms of the type system in Figure 11. 4.4. The NC Fragment of NC. NC is the fragment of NC that can be typed by rules in Figure 11. These rules are significantly more restrictive then the rules in Figure 4, in particular: • They acknowledge the existence of the four type categories, in particular of field(S); • They demand that (anonymous and defined) functions have an allowed return type R; • They require the sub-expressions of rep, nbr and foldhood to have local return type S; • Finally, they require captured variables to have local type, in the body of both anonymous functions and folding expressions. For convenience in the comparison with HFC , we assume that NC comprises the consthood and map built-ins, even though in NC those functions could also be defined as follows.
The embedding of NC as a fragment of NC can be formally characterised by means of the following definition and theorem.  In all other cases, the thesis follows since the NC rules can be obtained by removing restrictions from their counterparts in NC : that types are restricted to peculiar kinds L, R or S (every rule except for [T'-VAR]); that captured variables have local type (in rules

4.5.
Equivalence between NC and HFC . In order to prove the equivalence between HFC and NC , we first need to define what it means for an expression to have an equivalent behaviour in the two languages. Let v be HFC values of type T, and let v(δ) be defined by cases as (δ) = , φ(δ) = where is such that δ → ∈ φ.
We say that e[x : = v] has the same behaviour in HFC and NC whenever:  Assuming that the above coherence condition holds for built-in functions, then it holds for every expression, as shown in the following theorem. 4.6. NC Expressiveness. In this section, we argue that NC is an expressive language for distributed computations. Section 4.6.1 shows that NC contains many relevant aggregate programs, including those providing universality for distributed computations. In Section 4.6.2, we argue that most of the HFC programs which are not directly interpretable in NC can be automatically refactored (while preserving their behaviour) in order to fit within HFC (hence NC ), enlarging the class of aggregate programs expressible in NC to virtually all HFC programs. This process is exemplified in Section 4.6.3, where the self-stabilising fragment of HFC [VAB + 18] is ported to NC through the refactoring just introduced. Finally, Section 4.6.4 argues that the NC programs that are not in NC can fruitfully extend the expressive power of HFC. 4.6.1. NC examples: G and T blocks and universality. The correspondence between NC and HFC given by Theorem 4.7, although restricted to a fragment of the two languages, is applicable to many relevant aggregate programs, allowing for the automatic transfer of algorithms and properties proven in HFC to NC. As a first paradigmatic example, consider the G and T blocks, proposed as part of a combinator basis able to express most aggregate systems [BV14]. The HFC formalisation of their code [VAB + 18] is reported in Figure 12, and has the same behaviour in NC since it actually belongs to HFC /NC .
As a further example, consider the code in Figure 13, which is also in HFC /NC and encodes the behaviour of any distributed Turing Machine (expressed as a function f taking as input in every firing the whole collection of causally available data) as an HFC function. This code was used to prove Turing Universality for HFC, 12 but in fact it also proves that HFC /NC hence NC are Turing-universal as well (assuming a sufficient collection of built-ins). 4.6.2. Refactoring of HFC programs into HFC . Despite many common aggregate functions being in HFC /NC , not every relevant such function belongs to this fragment. In particular, the restrictions imposed by the type system in Figure 11 prohibit the common pattern of functions with field arguments, folding those arguments in their body. However, refactoring strategies exist that we can use in order to turn most HFC programs into HFC while preserving their behaviour, by converting a field argument of a function into a local argument, thus allowing its capture within foldhood statements.
(1) Abstracting: a field argument of a function may be passed "by name" through This refactoring preserves the behaviour of a program, provided that either (i) the abstracted parameter does not depend on the current domain (e.g., relational sensors such as nbrRange), or (ii) the parameter does not occur within branches in the body of the function (so that the evaluation is performed in the function with respect to the same domain as in the argument). If instead it does occur within a branch and depends on the current domain, its deferred evaluation within the function body will generally produce a different result. For example, consider function counthood counting the number of neighbours in the current domain: This functions returns 0 on "cold" devices; otherwise, it returns the maximum number of total neighbours that any of the hot neighbours has (a metric that may be used, e.g., 12 A programming model for distributed systems is Turing-universal if and only if it is able to replicate the behaviour of any distributed Turing machine. to trigger an alarm). After the abstracting refactoring, this result changes slightly to the maximum number of hot neighbours that any of the hot neighbours has. This reduces the computed value, possibly reducing the effectiveness of the metric and delaying the triggering of the alarm. However, most programs with this characteristic can still be refactored, by resorting to the following extended refactoring whenever the domain dependency is due to sub-expressions of local type.
(2) Abstracting with parameters: a field argument e 2 [e ] where e have local type can be passed as a function with parameters through This refactoring can address the previous example, by leaving the counthood() sub-expression (which is the one depending on the current domain and has local type) as a parameter: If the evaluation of e 2 [e ] does not depend on the domain of evaluation, then this computation shift has no effect, concluding the proof for that case. If x does not occur within branches, the set of aligned neighbours for each of its occurrences is the same as the set of aligned neighbours for the function call itself. It follows that the evaluation of e 2 [e ] in place of every occurrence of x has to produce the same result as in the argument, concluding the proof.
The following simpler refactoring can address cases when the argument is a simple nbr-expression, regardless of branches in e 1 .
(3) Deferring: an nbr in the argument may be transferred into the body as This refactoring is a simplification of refactoring (2), abstracting nbr{e 2 } with respect to the parameter e 2 of local type. In fact, refactoring (2) in this case would produce:  Notice that the general refactoring (2) and its common special cases (1) and (3) cover practically any realistic situation. In order for neither of those to be applicable, we would need the argument e 2 to be a field expression which depends on the current domain (and not only because of sub-expressions of local type), and which is passed into a function e 1 folding it inside a branching statement: we are not aware of any meaningful or realistic situation meeting these requirements. Thus, through these refactorings we can convert practically any HFC program into HFC , hence into NC, although the resulting refactoring may be extensive: since the refactoring of a function depends on its call patterns, more than one version of a function may be needed to cover all of them. However, this is not an issue for a "native" NC programmer, which would just interpret the above refactorings as programming patterns, following them from the start of application development. Interpreted as programming patterns, the above rules suggest deferring nbr applications when possible, or using call-by-name instead of call-by-value on field arguments otherwise. 4.6.3. An example: refactoring the self-stabilising fragment of HFC. A paradigmatic example of HFC programs that can be converted into HFC through the refactorings in Section 4.6.2 is the self-stabilising fragment [VAB + 18]. We say that a time-and space-distributed data is stabilising iff it remains constant in every point after a certain time t 0 , and its limit is the value assumed after t 0 . We say that a distributed program is self-stabilising iff given stabilising inputs and topology, it produces a stabilising output which depends only on the limits of the inputs and topology (and not on the concrete scheduling of events, nor on the input values before stabilisation).
A subset of HFC, called self-stabilising fragment, has been proved in the literature [VAB + 18] to consist of self-stabilising programs only. This fragment makes use of functions folding field arguments, and thus is not part of HFC and cannot be directly translated into NC preserving its behaviour (or self-stabilisation). However, the refactorings in Section 4.6.2 can be applied to obtain the equivalent fragment in Figure 14 that does belong to HFC /NC . The self-stabilising fragment combines syntactic requirements with mathematical requirements on functions, annotated in the figure through superscripts on function names.
is said converging iff, in every firing, its return value is closer to v 2 than the maximal distance that the two arguments v 1 and v 2 have in any neighbour firing (according to any metric measuring that distance).

M (Monotonic non-decreasing).
A stateless 13 function f(x, x) with arguments of local type is monotonic non-decreasing in its first argument iff whenever 1 ≤ 2 , also f( 1 , ) ≤ f( 2 , ). P (Progressive). A stateless function f(x, x) with local arguments is progressive in its first argument iff f( , ) > or f( , ) = (where is the maximal element of the relevant type). A function f( 1 , 2 , v) is raising with respect to partial orders <, iff: Theorem 4.9 (Self-stabilising fragment of NC). Every self-stabilising expression according to the fragment in Figure 14 is a self-stabilising NC expression.

R (Raising).
Proof. Notice that the fragment in Figure 14 can be obtained from that in [VAB + 18, Figure  2] by means of the following two refactorings. abstraction is applied to the first argument with respect to parameters y, z (whose values are added into the additional parameters s passed to f). By Theorem 4.8, these expressions have the same behaviour as those in [VAB + 18, Figure 2], which are self-stabilising by [VAB + 18, Theorem 1]. The rest of the fragment is identical modulo inessential modifications (expansion of the nbrlt and minHoodLoc functions into their definition), concluding the proof. 4.6.4. NC programs that cannot be straightforwardly expressed as HFC programs. As argued in [ADVC16], programs such as updatable metrics and combined Boolean restriction are not conveniently expressed in HFC. In the former case, we can use the following general scheme for updatable functions, first proposed in [AVD + 19]: where injecter is a function returning a pair version number, function code , and the built-in operator max selects the pair with the highest version number among its arguments. This procedure defines a perfectly reasonable "upgradeable function" by spreading functions with higher version number throughout devices. However, it is not allowed by the type system of HFC for functions returning fields, such as metrics (which usually have type () → field(num)). This scheme can instead be used in NC (as shown in Section 6), and works properly provided that new versions are injected at a slow rate, and an occasionally empty domain of a field-like expression does not produce critical effects.
Another situation where the permissive behaviour of NC is crucial is that of combined Boolean restriction. In this setting, a field-like expression e needs to be restricted to those devices agreeing on the value of n Boolean parameters b 1 , . . . , b n , before being folded with foldhood(v, f, e). This rather abstract example might be concretely instantiated, e.g., in case an aggregation needs to be executed separately on devices with different configurations. In HFC, this effect can be achieved only by restricting on each of the 2 n possibilities for the parameters, as in the following. However, such a program might be infeasibly large even for small values of n. On the other hand, in NC the above program can be concisely rewritten as: whose size is linear in n. 14 The domain of the i-th 0-valued field-like subexpression above is equal to the set of devices agreeing on b i , hence by intersecting all of them the resulting domain corresponds to the set of devices agreeing on each of the n given parameters.

ScaFi: an Implementation of the neighbours calculus
In this section, we present ScaFi, an implementation of NC embedded in the Scala programming language. While external (or standalone) domain-specific languages (DSLs) have their own syntax and semantics, internal DSLs are embedded into some other language [Voe13]. Developing an internal DSL brings major benefits in terms of reuse of features, toolchain support, and familiarity with the host language-at the expense of the syntactical and semantical constraints imposed by the host. Scala has been chosen as the host language of ScaFi for its modernity, its feature set (it provides mechanisms supporting the creation of expressive APIs and DSLs [AHKY15]) and its ability to target and reuse libraries from various execution platforms (such as the JVM, but also Javascript through the ScalaJS project [Doe18]). With respect to other field calculi, NC lends itself to smooth implementation in Scala, as e.g. there are no neighbouring fields to be dealt with at the typing, syntactical, and semantical level. A Scala expression of type Int is automatically interpreted in ScaFi as an aggregate program producing a field of Int, completely fulfilling the "everything is a field" view. In this code we assumed that x has numerical type, but similar code can be obtained for any type by defining a binary operator which is the identity on its first argument. In Scala, methods are introduced with the def keyword, can be generic (with type parameters specified in square brackets), may accept multiple parameter lists, and specify a return type at the end of the signature (when this is not given the compiler attempts inference). Function types may take the form (I 1 ,. . .,I N )=>O, which is actually syntactic sugar over FunctionN[I 1 ,. . .,I N ,O]; curried function types can be written as I 1 =>· · · =>I N =>O (=> is right associative). Tuple types may take the form (T 1 ,. . .,T N ), which is actually syntactic sugar over TupleN[T 1 ,. . .,T N ]; similarly, a literal tuple value can be denoted as (v 1 ,. . .,v N ). By-name parameters, denoted with type =>T , capture expressions or blocks of code that are passed unevaluated to the method and are actually evaluated every time the parameter is used-they are basically syntactic sugar over 0-ary function types. As a relevant note on syntax, especially useful in DSLs to render constructs with code blocks, unary parameter lists in a method can be called also with curly brackets instead of parentheses. E.g., all the following are valid invocations for rep method above: rep(·)(·), rep(·){·}, rep{·}(·), rep{·}{·}. Finally, nullary methods can be invoked without parentheses; e.g., mid is a valid method call just like mid().
First of all, notice that the input and output types of the constructs (especially nbr) are proper, i.e., no field-like datatype appears in the signatures. Then, compare trait Construct with the NC syntax from Section 3.1. Beside the curried form of rep and foldhood, the only significant difference is an additional construct aggregate which is used to turn standard Scala functions/methods into field calculus functions, i.e., as units of alignment (refer to Section 3.3.1 for details). We report here the encoding of if, which is called branch in ScaFi due to the fact that if is a reserved keyword in Scala: Namely, the then and else expressions are passed unevaluated (as by-named arguments) to branch, and there used in the bodies of two corresponding lambdas, wrapped with aggregate (which could be seen as a form of tagging). The mux on cond is used to select one of those functions, which is then invoked straight on via operator (). Moreover, it is crucial to note that the programming patterns discussed in Section 4.6.2 (abstraction and deferring) are supported in a syntactically transparent way thanks to the Scala support of by-name parameters. Indeed, the refactoring shown in Section 4.6.2 are automatically applied.
Finally, in practice, an aggregate program can be defined in ScaFi by extending class AggregateProgram and implementing a method called main which defines the main program expression. This class provides an implementation of the Constructs trait: by subclassing it, the syntax and semantics of the ScaFi DSL is made available within the subclass definition, such that its objects could be used both for simulation purposes or to encapsulate the aggregate logic in actual distributed implementations. A full description of ScaFi is beyond the scope of this paper: the interested reader can refer to [CV18] for more details.

5.2.
On pragmatics: neighbouring fields vs computation against a neighbour. In this section, we provide a hint to the practical issues that NC/ScaFi solve, enabling easy implementations and a clean support for aggregate programming.
Consider the ScaFi expression of the classic gradient algorithm (cf. Example 3.10): and focus on expression nbr(d)+metric(). While in NC/ScaFi the latter is a sum of two local values, computed against aligned neighbours (by the *hood operator), in the field calculus it would be a sum of two neighbouring fields. While in a standalone DSL, implementations may transparently lift local operators to work with fields (i.e., collection-like objects), this would not typically be easy or even possible in internal DSLs. Suppose a neighbouring field of T-typed objects is represented as an instance of type Field[T]. In a purely functional world, two fields could be combined with a "classical" map2 function: map2(nbr(d), metric(), _+_) However, this would introduce some clutter in aggregate programs and require a versatile set of functional combinators. In a powerful object-oriented language with mechanisms like extension methods (such as Scala or C#), it would be possibly to manually add specific operators to work with fields. This example leverages Scala's pimp-my-library idiom [OMO10], which uses implicits to enable automatic, compile-time conversion of a Field of Numerics to a NumericField that could accept a method +. However, the lifting of local operators to fields would be manual, and not automatic-introducing a overhead on library development. Some language could provide advanced features to mitigate this issue (e.g., through powerful compile-time macro mechanisms), but in this case we would pose severe constraints on the host language for the internal DSL, hence limiting the development environment where aggregate programming could be supported.

Case Study
The goal of this section is to show how NC/ScaFi can be used to eloquently express collective behaviour. More specifically, the goal is not to show that NC/ScaFi necessarily provide a better programming support with respect to e.g. HFC/Protelis-though the Scala embedding does provide various practical benefits, through reuse of Scala's powerful type system and features. Indeed, the benefit of NC lies in a conceptual frugality (by avoiding the "neighbouring field" notion) that leads to a different computational model (cf., Section 4) with easier DSL embeddability in mainstream programming languages (cf. Section 5). Therefore, this section shows that such a model and implementation can be used to smoothly program increasingly complex field coordination-based systems. In particular, we apply the programming model to the context of edge computing [SCZ + 16] and ad-hoc cloudlets [CHL + 15] (introduced in Section 6.1). Evaluation of correctness of the proposed solution is performed by simulation. The source code, configuration files, and instructions for launching the experiments and reproducing the presented results is publicly available at an online and permanent repository 15 [Cas22b].
6.1. Ad-hoc Edge Computing Support. Edge computing [SCZ + 16] is a recent paradigm that aims to bring cloud-like functionality (e.g., virtualisation, elastic provisioning of resources, and "anything-as-a-service") closer to end devices at the edge of the network. Its goal is to ultimately improve the Quality of Service (QoS) of situated systems as in the IoT, e.g., through reduction of communication latencies, bandwidth and energy consumption, essentially achieved by shortening the round-trip path of data to/from elastic resource pools. In this case study for ScaFi, we focus on ad-hoc edge computing-i.e., a decentralised form of edge computing that does not rely on pre-existing infrastructure but is rather supported by a large-scale network of devices logically interacting in a peer-to-peer fashion. This may also be a first step for merging volunteer computing [DS14] and edge computing into a form of volunteer edge computing where devices with spare resources make them available (possibly by incentives) to nearby users.
We assume to operate in a region of a smart city where hundreds of devices willing to offer resources run the Aggregate Computing middleware and may hence participate in the Aggregate Edge Computing (AEC) application. In a vision of smart city operating systems, it is indeed sensible to assume that devices willing to fully benefit from smart city services are asked to give their "social contribution" by participating in the system as well, where, of course, we also assume proper security and privacy systems are in place. The system is open, in the sense that devices may enter or leave the system as they like. For this paper we abstract from the way in which tasks are assigned to devices.
The goal of AEC is to support a self-organisation process to monitor resource availability and usage (i.e., load) in the system, and spread information as well as directives which can be exploited by devices for decentralised control activities and by users for determining advantageous deployment options. The idea is to provide support for edge computing by leveraging the space-time distribution of resources. Managing edge resources in a collective, space-time-oriented fashion is meant to provide the following benefits: (i) it makes straightforward to exploit the locality principles that often sustain IoT, CPS, and smart city applications, (ii) it allows us to assume that devices are only able to communicate with other nearby devices through short-range wireless technologies (e.g., 10 to 100 meters range); (iii) it allows us to relate the spatio-temporal distribution of resources with human social activity; and (iv) it provides a natural way to handle collective metrics and take into account contextual data and constraints at multiple scales.
6.2. Model and Design. We model devices of the AEC system as self-aware, situated entities that can sense the resources they own (e.g., number of CPUs, memory, and storage) and their current levels of utilisation. We also assume the devices can communicate with other devices in their vicinity-i.e., the neighbouring relationship is basically spatial.
To divide the complexity of the resource monitoring task, we split the whole environment into manageable areas (which may also be called cloudlets) that can be monitored and controlled more easily. In order to solve issues related to distributed consensus, we want the system to resiliently self-organise to elect a leader in each area, so as to provide consistent views of the state of areas as well as enacting global decisions for an area. That is, the leaders effectively act as decentralised management points which are responsible for orchestrating a subset of devices in the system-so, in a sense, we apply a hybrid approach that combines orchestration and choreography as suggested in [VAA + 18]. In practice, leaders act as sinks of contributions from the devices of the corresponding area, and as propagators of area-wide context information back to its feeders; but, in order to implement such a process, we need mechanisms for resiliently streaming information across dynamic space-time regions. The following section shows library functions that can be used to streamline the implementation of aforementioned mechanisms.
6.3. Building Blocks. Implementing the above ideas requires a careful crafting of the aggregate specification. There is need of identifying some recurrent patterns (e.g., peer-topeer propagation of information in-and out-ward an area) that may come handy in many situations. They can then be used to raise the abstraction level through a reusable API that hides low-level detail (e.g. the interplay of rep and nbr) only exposing the functional contract expressing the relationship between input fields and output fields. In doing so, we progressively move from a device-centric view (i.e., local sensing and neighbour-oriented communication) to an aggregate view (i.e., collective behaviour and data) of the system, regaining declarativity and intent.
In this case study, we leverage the following building blocks, which are available in the ScaFi standard library. Recall the notion of gradient from Example 3.10: a field computation that, from a Boolean field denoting a region, returns the field of minimum distances from that region. The gradient field is key in spatial computations because it provides a direction from any point to a target: by descending (or ascending, resp.) the gradient field, i.e., by following the minimum (or maximum, resp.) values locally observed, one moves close (or away, resp.) to a certain target. With case class Gradient, we package together the algorithm and input fields-e.g., it may be useful to ensure that both a particular gradient and another function using it adopt the same metric. The gradient-cast operator, also known as G, is a generalised form of the gradient computation that accumulates values through a function acc along a gradient field, starting with the given field at sources and new devices. So, functionally G maps a source field and a field of values to a field whose values are progressively transformed, distance-wise, from source values. For instance, a gradient field can be constructed via G as follows. With G, it is also trivial to define a broadcast function for propagating information outward from a source, by ascending the potential field. A change of field where source is true basically generates a new wave in this continuous streaming of information. Notice that field has a value everywhere but only the value of the source is selected and then identically preserved in the propagation. Also, notice that both G and broadcast may admit a different signature where the "potential field" is given as input and not calculated internally.
Dual to the gradient-cast operator is the converge-cast operator, also known as C [ACD + 21]: indeed, the conceptually inverse notion of a propagation from a point outward is a propagation inward, from the edge towards a point. Functionally, C accumulates a local field along some potential field (yielding Null if no direction to descend can be found), where the final results of the accumulation end up collected at points of zero potential. In the Vol. 19:1 signature above, we show that we may abstract from the concrete type P of the potential field (which is usually a Double), provided that there exists an ordering over P values-as enforced by the context bound constraint P:Bounded (which is the mechanism for the typeclass idiom in Scala [OMO10], where the method may be called provided an implicit instance of type Bounded[P] can be statically resolved). It is also worth pointing out that both G and C are self-stabilising operators, whose compositions are also self-stabilising [VAB + 18] (see the discussion in Section 4.6.1); hence, there are formal guarantees that programs built using only self-stabilising constructs like G and C will eventually reach a fixpoint once inputs stop changing.
The last fundamental building block that we cover is the sparse-choice operator, also known as S, which yields a Boolean field holding true in correspondence of elected nodes (also called leaders) which are selected in a manner such that adjacent leaders are at about distance grain. In practice, it performs a leader election process which also results in a split of the space into areas of a diameter which is approximately grain, since any connected device will be at a distance at most grain from a leader (think of a gradient from the leader field). Internally, an implementation of S may work by performing a distance competition among devices: initially, every device will propose itself as a leader, but initially assigned random values (ultimately discriminated by device IDs in case of breakeven) will be used to break symmetry (e.g., by selecting the minimum); if the gradient from the currently selected leader is larger than grain at a device, then it will start another competition for leadership of another area. 6.3.1. Upgradeable functions. The case study also uses the notion of upgradeable function explained in Section 4.6.4. The idea is to support hot upgrades of application algorithms by allowing injection of versioned functions at some device of the system and then spreading such novel versions through a gossiping process that eventually settles everywhere the function of the higher version. Concretely, we model versioned functions and injectors as types of the form The flow is simple and reasoning is simplified by the collective stance and compositionality of Aggregate Computing: we use S to split the system into cloudlets monitored by corresponding leaders (where the metric used to create the areas can be dynamically updated); we build a gradient from leaders to set the pathways for collecting data into leaders and propagating the determinations of leaders to their workers; and finally, we use those pathways to stream the cloudlet-wise estimation of resource availability and usage to all the members. By the way, notice that this resource monitoring example represents the application of a more general pattern [CPVN19] that finds application in other large-scale, distributed coordination scenarios such as for situated problem solving [CTVD19] and client/server task allocation [CV19]. 6.5. Evaluation: Correctness. The system has been simulated in the Alchemist simulator [PMV13, VCP16b]-a graphical snapshot is given by Figure 15 16 , whereas Figure 16 provides empirical evidence of its correctness. The simulated system consists of 400 devices, each with a random amount of resources, dispersed unevenly around the downtown of Cesena, Italy. Mobility is not considered. A certain level of load is simulated in the system, and we perturb it with spikes of high load. The goal of the system is to reactively adjust the estimate of the load. As for additional perturbations, we also introduce a probability for temporary failure of leaders. Finally, in order to show the positive impact of updatable metrics, we inject a new metric to fine tune the dimension of areas at runtime-other aspects of configuration can be examined in the repository.

Related Work
Scenarios like the IoT, CPS, smart cities, and the like, foster a vision of rich computational ecosystems providing services by leveraging strict cooperation of large collectives of smart devices, which mostly operate in a contextual way. Engineering complex behaviour in these Figure 15. A visual representation of the scenario, with two snapshots taken before and after a hot update of the metric. The big red squares represent leader nodes. Superimposed with any leader is a semi-transparent blue ball whose intensity is proportional to the amount of resources available in the corresponding area. Other nodes have a colour representing the gradient field towards leaders (warmer, red-like colours for nodes close to the leader, and colder, green-like colours for peripheral nodes). The updated metric in (b) provides an overestimation of distances which results in smaller areas or, equivalently, in more cloudlets (and leaders). Figure 16. These graphs show the load estimation capability at leader nodes, for different round-wise probabilities of leader failure (leading to a disproportionate number of failures (150 to 250) in the considered timespan). The plots are obtained by taking the mean of 30 runs with different random seeds. The scenario is modelled as follows: the spikes of high load start at time t = 200 and t = 500, the new metric is injected at time t = 450, and we can observe that, after such upgrade, the system is able to perform a much more precise estimation of CPU usage. , and so on. By factoring out common ideas from these works, and neglecting the diversity in lower-level concepts, one can identify families of approaches which have relations with the programming framework developed in this paper. So, whereas the main related work is deeply covered in Section 4, in the following we comprehensively describe the research area in which our contribution can be positioned.
Device abstraction. A first family is that of languages providing some form of abstraction over device behaviour and interaction. TOTA [MZ09] and SAPERE [ZOA + 15] define platforms for pervasive computing focussing on agent coordination, where agents indirectly interact by injecting/perceiving "tuples" equipped with diffusion and aggregation behaviour (Java-defined in TOTA, declaratively specified by rules in SAPERE), and resulting in "fields of tuples" spread over the network; such works provide archetypal approaches to create computational fields that were an inspiration for Aggregate Computing and NC/ScaFithough, differently from them, we provide an expressive language to better control the shape and dynamics of fields over time. Hood [WSCB04] defines data types to model an agent's neighbourhood and attributes, with operations to read/modify such attributes across neighbours, and a platform optimising execution of such operations by proper caching techniques; ScaFi provides a comparable neighbourhood abstraction, in that operator nbr is essentially used to declaratively access the set of neighbours as well as to combine observation of neighbours' attributes (indicated by the expression passed as argument) with modification of the same attribute locally. Finally, works such as ActorSpace [CA94], SCEL [NFLP11,ANL20], and AbC [ANL20] rely on so-called attribute-based communication, where each actor/agent exposes a list of attributes, and communication can be directed to the group of actors whose attribute match a given pattern; ScaFi can achieve a similar expressiveness with construct branch, by which one can define subcomputations carried on by a subset of nodes, which are those that execute the same branch and hence remain actually "observable" by operator nbr. Generally speaking, it is worth noting that Aggregate Computing and NC/ScaFi address the key feature of fitting useful device abstractions (such as neighbourhood, message exchange, attribute-based filtering) into a purely functional approach, which can then smoothly interoperate with more traditional programming frameworks and languages.
Geometric/topological abstractions. Another class of related approaches falls under the umbrella of languages to express geometric constructs and topological patterns. In fact, in several application contexts concerning environment sensing and controlling, what is key is the physical (geometric, topological) shape that coalitions of mobile agents take, or that certain data items create while diffusing in the environment. In the Growing Point Language [Coo99], an amorphous medium [BMG08] (essentially defined by an ad-hoc network) can be programmed by a nature-inspired approach of "botanical computing", where computational processes are seen as "growing points" increasingly expanding across neighbours until reaching a fixpoint shape defined by declarative constraints; NC/ScaFi and Aggregate Programming work on similar hypothesis on structure and behaviour of the underlying network, though adopting a different functional paradigm that is more expressive as it can address dynamical aspects as well, and which could be used as a lower-level language to reproduce the expressiveness of the growing point abstraction. The Origami Shape Language defined in [Nag08] is used to achieve similar goals of the Growing Point Language though focussing on programming a "computational surface", intended as a set of small devices working independently of their density in the surface: this language defines geometrical constructs to create basic regions and compose them, which could be turned into an API of NC/ScaFi blocks to be functionally composed to achieve similar complex geometrical structures. A more recent work is PLEIADES [BBLT18], which aim at expressing self-stabilising overlay structures in large-scale distributed systems; however, these goals are not achieved through programs as in NC/ScaFi but rather through shape formation protocols and equation-based specifications. In general, due to the universal character of field computations [ABDV18], one could consider NC/ScaFi as a viable implementation framework for a number of approaches to organise the shape of computational entities in a physical environment, with the additional byproduct of leveraging the theory of field computations to assess formal validity of certain properties, such as density independence as developed in [BVPD17] or self-stabilisation in [VAB + 18].
WSN-based discovery and streaming. A number of works originating in the context of information systems for sensor networks, such as TinyDB [MSFC02], Regiment [NW04], and Cougar [YG02], address the problem of gathering information extracted from sensors in a given region of space, aggregating them somehow, and redirecting results over the devices in another region. They either focus on spatial query languages (Cougar), diffusion/aggregation policies (TinyDB) or functional models to express and manipulate streams of events (Regiment). A more recent approach is makeSense [MPO + 19], with leverages graphical models of business processes compiling to an imperative object-oriented macro-programming language (mPL) which supports many-to-one report actions and many-to-many collective actions, i.e., actions possibly spanning multiple nodes. Similar approaches for mobile ad-hoc networks (MANET) have been considered that do not use data-oriented techniques but rather focus on services: SpatialViews [NKSI05] works by abstracting a MANET into spatial views that can be iterated on to visit nodes and request services; AmbientTalk [CBS + 14] is another language for MANETs that provides resilience against transient network partitions by automatically buffering sent messages. A clear advantage of functional-based field computations as supported in NC/ScaFi is that the various bricks of information collection, aggregation, diffusion, can be defined with the same language, wrapped in homogeneous components represented by NC/ScaFi functions, and composed to create more complex applications as exemplified in Section 6, while leveraging the discovery of nearby services through an actor-based runtime.
Distributed/parallel computing. Aggregate Computing and its incarnation in ScaFi can evidently be considered as a declarative model for distributed programming, relying on abstractions and assumptions to make it easy to express certain kinds of programs by delegating important features (e.g., synthesising the concrete execution plan) to an underlying platform. As a notable example, we can draw a bridge with big data processing frameworks like MapReduce [DG08] and its derivation Apache Spark [ZXW + 16]: they essentially provide a highly declarative language of stream processing (based on simple map, reduce, filter and fold operations) and delegate to the underlying platform the duty of breaking tasks into smaller chunks to be allocated by the available computational/storage resources. An approach which is execution-wise technically more similar to the one developed in this paper is given by Bulk-Synchronous Parallel (BSP)-inspired frameworks, such as the large-scale graph processing framework Apache Giraph [SOAK16], which defines computations in terms of transformation of large graphs of data, typically stored in a distributed database. In this respect, NC/ScaFi can be seen as relying on a similar approach to achieve a more complex goal, namely, that of declaratively specifying a dynamic collective behaviour (based on distributed field computation), ultimately digesting information coming from distributed sensors and producing instructions for actuators, in such a way so as to make it breakable into smaller pieces (single rounds of computations) allocated to each device in the network.
Service choreographies. Choreographies [Pel03] are an approach to service composition where the interaction protocol of collaborative workflows is specified by a global viewpoint. So, they define the cooperative contract of multiple parties playing certain roles and collaborating to achieve a global goal. There are strong similarities with Aggregate Computing; indeed, aggregate programs (i) globally define the interactions supporting a collective computation, cf. the nbr construct; (ii) define services which are fundamentally collaborative in nature; and (iii) define roles for devices implicitly by the set of (sub-)computations executed by them or, equivalently, by the set of domain branches that they select in the collective workflow. By contrast, however, while classical choreographies usually express goal-oriented workflows where the roles are few and statically assigned to parties, aggregate computations typically carry on continuous processes where devices repeatedly participate in the collective service and can also play different roles across time depending on the context. Additionally, choreographies abstract from the particular services carried on by the involved parties, focussing instead only on when and what messages are exchanged, whereas aggregate programs specify computations, which are collective and yield global results represented as fields. Moreover, interaction in field computations is only possible (by alignment) between entities playing the same (sub-)computation, and is neighbour-driven (rather than peer-to-peer and role-driven). Finally, choreographies and choreographic programming [CM20] mainly focus on checking conformance or building correct-by-construction concurrent programs (e.g., deadlock-free), but do little for functional composability of adaptive behaviours, which is instead the core of Aggregate Computing.
Spatial and field-based computing languages. Aggregate Computing directly descends from the class of so-called general-purpose spatial computing languages, all addressing the problem of engineering distributed (or parallel) computing by providing mechanisms to manipulate data structures diffused in space and evolving in time. Notable examples include the StarLisp approach for parallel computing [LMMD88], the SDEF programming system inspired by systolic computing [EC89], and topological computing with MGS [GMCS05]. They typically provide specific abstractions that significantly differ from that of computational fields: for instance, MGS defines computations over manifolds, the goal of which is to alter the manifold itself as a way to represent input-output transformation. A particular class of spatial computing languages is given by field-based coordination languages [VBD + 19]. Beside the field calculus, covered in this paper, other works like TOTA [MZ09] and calculi like SMuC [LLM17] also come under this umbrella. SMuC, however, does not express fields as functional compositions, but as formulas on fixpoint steps whose result may serve as continuations of the program. Specific programming languages to work with computational fields have been introduced as well, with the Proto [BB06] programming language as common ancestor, Protelis [PVB15] as its Java-oriented DSL version, and the more recent FCPP [Aud20]. The field calculus, deeply described and compared to in Section 4, has been designed as common core calculus for such languages, and for studying behavioural properties and semantic aspects [BVPD17, VAB + 18, AVD + 19]. Though rather similar to those languages, NC evolved in a different way: its design is profoundly influenced by the need of smoothly integrating field computations in the syntactic, semantic, and typing structures of modern, conventional languages (like Scala-see Section 5), and this required key semantic changes that motivated a more general and expressive calculus, as presented in this paper.

Conclusions
Aggregate Computing is a recent paradigm for "holistically" engineering CASs and smart situated ecosystems, that aims to exploit, both functionally and operationally, the increasing computational capabilities of our environments-as fostered by driver scenarios like IoT, CPS, and smart cities. It formally builds on computational fields and corresponding calculi to functionally compose macro behavioural specifications that capture, in a declarative way, the adaptive logic for turning local activity into global, resilient behaviour. In order to promote conceptual frugality and foster smooth embedding of this programming model in mainstream languages, we propose a field calculus variant, called NC, which substitutes the notion of a "neighbouring field" with a novel notion of a "computation against a neighbour". We formalise NC and thoroughly compare to the higher-order field calculus (HFC), stressing differences in expressiveness and identifying a common fragment allowing straightforward transfer of interesting properties such as self-stabilisation. To witness the benefits of the novel calculus, we cover the ScaFi aggregate programming language, which implements NC as a DSL embedded in Scala. Finally, we use a simulated case study in edge computing to show that ScaFi/NC are effectively expressive in practice.
how much such edge computing systems need orchestration, what benefits and challenges can emerge from a hybrid approach that brings self-organising processes in, and how aggregate techniques allows us to tackle such integration of paradigms.