Stream Types

We propose a rich foundational theory of typed data streams and stream transformers, motivated by two high-level goals: (1) The type of a stream should be able to express complex sequential patterns of events over time. And (2) it should describe the internal parallel structure of the stream to support deterministic stream processing on parallel and distributed systems. To these ends, we introduce stream types, with operators capturing sequential composition, parallel composition, and iteration, plus a core calculus lambda-ST of transformers over typed streams which naturally supports a number of common streaming idioms, including punctuation, windowing, and parallel partitioning, as first-class constructions. lambda-ST exploits a Curry-Howard-like correspondence with an ordered variant of the logic of Bunched Implication to program with streams compositionally and uses Brzozowski-style derivatives to enable an incremental, prefix-based operational semantics. To illustrate the programming style supported by the rich types of lambda-ST, we present a number of examples written in delta, a prototype high-level language design based on lambda-ST.


INTRODUCTION
What is the type of a stream?A straightforward answer, dating back to the early days of functional programming [17], is that a stream is an unbounded sequence of items drawn from a single fixed type, produced by one part of a system (or the external world) and consumed by another.This simple perspective has been immensely successful and can be found in the programming models exposed by today's most popular distributed stream processing eDSLs (e.g., Flink [18,30], Beam [35], Storm [34], and Heron [31]).
Common programming patterns from stream processing practice that are elegantly supported by this richer model include MapReduce-like pipelines, temporal integrity constraints, windowing, punctuation, parallelism control, and routing.Section 2 below explores some concrete cases where  ST 's structured types can prevent common stream processing bugs and enable cleaner programming patterns.Section 3 presents Kernel  ST , a minimal subset with just the features needed to state and understand the main technical results.Section 4 extends this presentation to Full  ST .Section 5 develops several further examples.Sections 6 and 7 discuss related and future work.An overview of our prototype implementation of Delta, technical details omitted from the main paper, and code for the examples can all be found in the extended version [22].

MOTIVATING EXAMPLES
Types for temporal invariants.Consider a stream of brightness data coming from a motion sensor, where each event in the stream is a number between 0 and 100.Suppose we want a stream transformer that acts as a thresholded filter, sending out a "Start" event when the brightness level goes above 50, forwarding along brightness values until the level dips below the threshold, and sending a final "Stop" event.For example: 11, 30, 52, 56, 53, 30, 10, 60, 10, . . .=⇒ Start, 52, 56, 53, Stop, Start, 60, Stop, . . .The output of the transformer should satisfy the following temporal invariant: each start event must be followed by one or more data events and then one end event.Conventional stream processing systems would give this transformation a type like Stream Int → Stream (Start + Int + Stop), which expresses only the possible kinds of events in the output, not the temporal invariant that the Start must come before the numbers and the Stop after.
These simple types are even more problematic when consuming streams.Suppose another transformer wants to consume the output stream of type Stream (Start + Int + Stop) and compute the average brightness between each start/end pair.We know a priori that the stream is well bracketed, but the type does not say so.Thus, the second transformer must re-parse the stream to compute the averages, requiring additional logic for various special cases (Stop before Start, empty Start/Stop pairs) that cannot actually occur in the stream it will see.
In  ST , we can express the required invariant with the type Start • Int • Int ★ • End ★ , specifying that the stream consists of a start message, at least one Int, and an end message, repeatedly.A well-typed transformer with this output type is guaranteed to enforce this invariant, and, conversely, a downstream transformer can assume that its input will adhere to it.
Enforcing deterministic parallelism.A second limitation of homogeneous streams is that they impose a total ordering on their component events.In other words, for each pair of events in the stream, a transformer can tell which came first.This is problematic in a world where stream transformers work over data that is logically only partially ordered-e.g., because it comes from separate sources. 1  For example, consider a system with two sensors, each producing one reading per second and sending these readings via different network connections to a single transformer that averages them pairwise, producing a composite reading each second.A natural way to do this is to merge the two streams into a single one, group adjacent pairs of elements (i.e., impose a size-two tumbling window), and average the pairs.But this is subtly wrong: a network delay could cause a pair of consecutive elements in the merged stream to come from the same sensor, after which the averages will all be bogus.
The problem with this transformer is that it is not deterministic: its result can depend on external factors like network latency.Bugs of this type can easily occur in practice [59,67] and can be very difficult to track down, since they may only manifest under rare conditions [50].
Once again, this is a failure of type structure.In  ST , we can prevent it by giving the merged stream the type (Sensor1∥Sensor2) ★ , capturing the fact that it is a stream of parallel pairs of readings from the two sensors.We can write a strongly typed merge operator that produces this type, given parallel streams of type Sensor1 ★ and Sensor2 ★ .This merge operator is deterministic-indeed, all well-typed  ST programs are, as we show in Section 3.3).Operationally, it waits for events to arrive on both of its input streams before sending them along as a pair.

KERNEL 𝜆 ST
In this section, we define the most important constructors of stream types and the corresponding features of the term language; these form the "kernel" of the  ST calculus.The rest of the types and terms of Full  ST will layered on bit by bit in Section 4.
The concatenation constructor • describes streams that vary over time: if  and  are stream types, then  •  describes a stream on which all the elements of  arrive first, followed by the elements of .A producer of a stream of type  •  must first produce a stream of type  and then a stream of type , while a consumer can assume that the incoming data will first consist of data of type  and then of type .The transition point between the  and  parts is handled automatically by  ST 's semantics: the underlying data of a stream of type  •  includes a punctuation marker [75] indicating the cross-over.One consequence of this is that, unlike Kleene Star for regular languages, streams of type  ★ are distinguishable from streams of type  ★ •  ★ because a transformer accepting the latter can see when its input crosses from the first  ★ to the second.
On the other hand, the parallel stream type  ∥ describes a stream with two parallel substreams of types  and .Semantically, the  and  components are produced and consumed independently: a transformer that produces  ∥ may send out an entire  first and then a , or an entire  and then the , or any interleaving of the two.Conversely, a transformer that accepts  ∥ must handle all these possibilities uniformly, by processing the  and  parts independently.To enable this, each element in the parallel stream is tagged to indicate which substream it belongs to.This means that streams of type  ∥ are isomorphic, but not identical, to streams of type  ∥, and similarly Int ★ ∥Int ★ is not the same as Int ★ .
Parallel types can be combined with concatenation types in interesting ways.For example, a stream of type ( ∥) •  consists of a stream of interleaved items from  and , followed (once all the  data and  data has arrived) by a stream of type  .By contrast, a stream of type ( • ) ∥ ( ′ •  ′ ) has two interleaved components, one a stream described by  followed by a stream described by  and the other an  ′ followed by a  ′ .The fact that the parallel type is on the outside means that the change-over points from  to  and  ′ to  ′ are completely independent.
The base type 1 describes a stream containing just one data item, itself a unit value.The other base type is , the type of the empty stream containing no data; it is the unit for both the • and ∥ constructors-i.e.,  • ,  • ,  ∥ and  ∥ are all isomorphic to , in the sense that there are  ST transformers that convert between them.
In summary, the Kernel  ST stream types are given by the grammar on the top left in Figure 1.(So far, these types can only describe streams of fixed, finite size.In Section 4.2 we will enrich the kernel type system with unbounded streams via the Kleene star type  ★ .) What about terms?Recall that our goal is to develop a language of core terms , typed by stream types, where well-typed terms  :  ⊢  :  are interpreted as stream transformers accepting a stream described by  and producing one described by .The term  runs by accepting some inputs as described by , producing some outputs as described by , and then stepping to a new term  ′ , with an updated type  ′ , that is ready to accept the rest of the input and produce the rest of the output.This process happens reactively: output is only produced when an input arrives.The formal semantics of  ST is described in Section 3.2.
To represent stream transformers with multiple parallel and sequential inputs, we draw upon insights from proof theory.Both the types  •  and  ∥ are product types, in the sense that a stream of either of these types contains both the data of a stream of type  and a stream of type -although the temporal structure differs between the two.A standard observation from proof theory is that, in situations where a logic or type theory includes two products with different structural properties, the corresponding typing judgment requires a context with two different context formers. 2  The first context former, written with a comma (Γ , Δ), describes inputs to a transformer arriving in parallel, one component structured according to Γ and the other according to Δ.The second context former, written with a semicolon (Γ ; Δ) describes inputs that will first arrive from the environment according to Γ, then according to Δ.
These interpretations are enforced by restricting the ways that these contexts can be manipulated using structural rules.Comma contexts can be manipulated in all the ways standard contexts can: their bindings can be reordered (from Γ , Δ to Δ , Γ) duplicated, and dropped.Semicolon contexts, on the other hand, are ordered and affine: a context Γ ; Δ cannot be freely rewritten to a context Δ ; Γ, and a context Γ cannot be duplicated into Γ ; Γ.These restrictions enforce the interpretation of Γ ; Δ as data arriving according to Γ and then Δ: to exchange them would be to allow a consumer to assume that the data is sent in the opposite order, and to duplicate would be to assume that the data input will be replayed.
Thus, part of our type system is substructural: the semicolon context former is ordered (no exchange) and affine (no contraction), while the comma context former is fully structural.Both context formers are associative, with the empty context serving as a unit for each.(The full list of structural rules can be found in the extended version [22].)Formally, stream contexts are drawn from the grammar at the top right of Figure 1.

Kernel Typing Rules
The typing rules for Kernel  ST are collected in Figure 1.The typing judgment, written Γ ⊢  : , says that  is a stream transformer from a collection of streams structured like Γ to a single stream structured like .
The most straightforward typing rule is the right rule for parallel (T-Par-R).It says that, from a context Γ, we can produce a stream of type  ∥ by producing  and  independently from Γ, using transformers  1 and  2 .We write the combined transformer as a "parallel pair" ( 1 ,  2 ).Semantically, it operates by copying the inputs arriving on Γ, passing the copies to  1 and  2 , and merging the tagged outputs into a parallel stream.Similarly, the T-Cat-R rule is used to produce a stream of type  • .It uses a similar pairing syntax-if term  1 has type  and  2 has type , then the "sequential pair" ( 1 ;  2 ) has type  • -but the context in the conclusion differs.Since  1 needs to run before  2 , the part of the input stream that  1 depends on must arrive before the part that  2 depends on.Semantically, this term will operate by accepting data from the Γ part of the context and running  1 ; once  1 has produced its output it will switch to running  2 , consuming data from Δ.
2 Such bunched contexts were first introduced in the Logic of Bunched Implication [63], the basis of modern separation logic [66].Our bunched contexts differ from those of BI by the choice of structural rules: our substructural type former is affine ordered, while the BI one is linear.These right rules describe how to produce a stream of parallel or concatenation type.The corresponding left rules describe how to use a variable of one of these types appearing somewhere in the context.Syntactically, the terms take the form of let-bindings that deconstruct variables of type  •  (or  ∥) as pairs of variables of type  and , connected by ; (or ,).We use the standard BI notation Γ(−) for a context with a hole and write Γ(Δ) when this hole has been filled with the context Δ.In particular, Γ( : ) is a context with a distinguished variable .

𝑠, 𝑡, 𝑟
The T-Par-L rule says that if  is a variable of type  ∥ somewhere in the context, we can replace its binding with with a pair of bindings for variables  and  of types  and  and use these in a continuation term  of final type  .When typing , the variables  and  appear in the same position as the original variable , separated by a comma-i.e.,  and  are assumed to arrive in parallel.Similarly, the rule T-Cat-L says that if a variable  of type  •  appears somewhere in the context, it can be let-bound to a pair of variables  and  of types  and  that are again used in the continuation .This time, though,  and  are separated by a semicolon-i.e., the substream bound to  will arrive and be processed first, followed by the substream bound to .
T-Eps-R and T-One-R are the right rules for the two base types, witnessed by the terms sink and ().Semantically, sink does nothing: it accepts inputs on Γ and produces no output.On the other hand, () emits a unit value as soon as it receives its first input and never emits anything else.
The variable rule (T-Var) says that if  :  is a variable somewhere in the context, then we can simply send it along the output stream.Semantically, it works by dropping everything in the context except for the -typed data for , which it forwards along.
The rule T-SubCtx bundles together all of the structural rules as a subtyping relation on contexts.For example, the weakening rule for semicolon contexts is written Γ ; Δ ≤ Γ and the comma exchange rule is Γ , Δ ≤ Δ , Γ.
Examples and Non-Examples.To show the typing rules in action, here are two small examples of transformers written in Kernel  ST , as well as three examples of programs that are rejected by the type system.The first example is a simple "parallel-swap" transformer, which accepts a stream  of type  ∥ and outputs a stream of type  ∥, swapping the parallel substreams: The most important non-example is the lack of a corresponding "cat-swap" term, which would accept a stream  of type  • , and produce a stream of type  • .This program is undesirable because it is not implementable without a space leak.Implementing it requires the entire stream of type  to be saved in memory to emit it after the stream of type . 3 The natural term for this program would be let  (; ) =  in (; ), but this does not typecheck.Applying the syntax-directed rules gets us to a point where we must show that  has type  in a context with only  and that  has type  in a context with only .This is because the T-Cat-R splits the context, but the variables are listed in the opposite order from what we'd need.The lack of a structural rule to let us permute the  and  in the context means that there is nothing we can do here, and a typechecker will reject this program.
The second example is a "broadcast" transformer, which takes a variable  :  and outputs a stream of type  ∥, duplicating the variable and sending it out to two parallel outputs:  :  ⊢ (, ) :  ∥.
The second non-example is the "replay" transformer, which would (if it existed) take a variable  :  and produce a stream  •  that repeats the input stream twice.This is the concat-equivalent of the broadcast transformer, and it is undesirable for the same reason as the cat-swap program: it would require saving the entire incoming stream of type  in order to replay it.This time, the failure of the natural term (; ) to typecheck comes down to the lack of a contraction rule for semicolon contexts: we are not permitted to turn a context  :  into a context  :  ;  : .The last non-example is a "tie-breaking" transformer, which would take a stream  : Int∥Int of two ints in parallel and produce a stream of type Int by forwarding along the Int that arrives first.This program, like others that require inspecting the interleaving of substreams in a stream of type  ∥, is not expressible.In Section 3.3, we'll prove that a well-typed program cannot implement this behavior.

Prefixes and Semantics
We next define the semantics of Kernel  ST .The natural notion of "values" in this semantics is finite prefixes of streams, and the meaning of a well-typed term Γ ⊢  :  is a function that accepts an environment mapping variables in Γ to prefixes of streams and produces a prefix of a stream of type .Because the streams that  ST programs operate over are more structured than traditional homogeneous streams-including cross-over punctuation in streams of type  • and disambiguating tags in streams of type  ∥-the prefixes are also more structured.That is, a prefix in  ST is not a simple sequence of data items, but a structured value whose possible shapes are determined by its type [58].

. Environments for Contexts
There are two prefixes of a stream of type 1: the empty prefix, written oneEmp, and the prefix containing the single element (), written oneFull.Similarly, the unique stream of type  has a single prefix, the empty prefix, which we write epsEmp.
What about  ∥?A parallel stream of type  ∥ is conceptually a pair of independent streams of type  and , so a prefix of a parallel stream should be a pair parPair( 1 ,  2 ), where  1 is a prefix of a stream of type , and  2 is a prefix of a stream of type .Crucially, this definition encodes no information about any interleaving of  1 and  2 : the prefix parPair( 1 ,  2 ) equally represents a situation where all of  1 arrived first and then all of  2 , one where  2 arrived before  1 , and many others where the elements of  1 and  2 arrived in some interleaved order.In a nutshell, this definition is what guarantees deterministic processing.By representing all possible interleavings using the same prefix value, we ensure that a transformer that operates on these values cannot possibly depend on ordering information that isn't present in the type.
Finally, let's consider the prefixes of streams of type  • .One case is a prefix that only includes data from  because it cuts off before reaching the point where the  •  stream stops carrying elements of  and starts on .We write such a prefix as catFst(), with  a prefix of type .The other case is where the prefix does include the crossover point-i.e., it consists of a "completed" prefix of  plus a prefix of .We write this as catBoth(,  ′ ), with  a prefix of  and  ′ a prefix of .The requirement that  be completed is formalized by the judgment  maximal, which ensures that the prefix  describes an entire completed stream (see the extended version [22] for details).We formalize all these possibilities as a judgment  : prefix (), shown in Figure 2.
Every type  has a distinguished empty prefix, written emp  and defined by straightforward recursion on  (see the extended version [22]).We then lift the idea of prefixes from types to contexts, defining an environment  for a context Γ to be a mapping from the variables  :  in Γ to prefixes of the corresponding types ; we write this with a judgment  : env (Γ) (Figure 3).Besides ensuring that  has well-typed bindings for all variables, the judgment ensures that the prefixes respect the order structure of the context.In particular, an environment  for a semicolon context Γ ; Δ must assign prefixes in order: the prefixes for Γ, the earlier part of the context, must all be complete before any of the prefixes for Δ can begin.In other words, either  assigns maximal prefixes to every variable in Γ-which we write  maximalOn Γ-or  assigns empty prefixes to every variable in Δ-which we write  emptyOn Δ.
One might worry that these structured stream prefixes might be incompatible with a future distributed implementation atop an existing stream processing substrate.Fortunately, they are not: by viewing a  ST stream as a series of single-event prefixes, each consisting of a data item plus some extra tag bits, we can recover the traditional homogeneous view.Moreover, this wire representation incurs only a constant overhead: the maximum size of the tag bits on a stream element of type  is bounded by the syntactic depth of  (see the extended version [22] for details).
Semantics.We next describe how well-typed  ST terms behave with an operational semantics.Given a well-typed term Γ ⊢  :  and an input environment  : env (Γ), this semantics describes how to run  with  to produce an output prefix .It also describes how to produce a "resultant" term  ′ that is ready to continue the computation once further data arrives on the input stream data.Formally, the semantics is given by a judgment  ⇒  ↓  ′ ⇒ , pronounced "running the core term  on the input environment  yields the output prefix  and steps to  ′ ."The rules for this judgment are gathered in Figure 4 and described below; the full set of rules for all of  ST can be found in [22].
The following theorem establishes the soundness of the Kernel  ST semantics, formalizing the intuitive description given above: If we run a well-typed core term  on an environment  of the context type, it will return a prefix  with the result type  and step to a term  ′ that is well typed in context "the rest of Γ" after  and has type "the rest of " after .The "rest" of a type (or context) after a prefix (or environment) is, intuitively, its derivative with respect to the prefix (or environment), in the sense of standard Brzozowski derivatives of regular expressions [16]-we make this formal in Section 3.2.Most critically, the types of the variables in  and  ′ are different: if  has type  in , then  has type   ( )  in  ′ , having already consumed  ().Theorem 3.1 (Soundness of the Kernel  ST Semantics).Suppose: Γ ⊢  :  and  : env (Γ).Then there are  and  ′ such that  ⇒  ↓  ′ ⇒ , with  : prefix () and   (Γ) ⊢  ′ :   () See the appendix of the extended paper [22] for the proof of soundness for Full  ST .In light of the soundness theorem, the operational semantics can be thought of as defining a reactive state machine.Well-typed terms Γ ⊢  :  are the states, while the semantic judgment defines the transition function: when new inputs  arrive, the machine produces an output prefix  and steps to a new state   (Γ) ⊢  ′ :   ().This form of semantics-a state machine with terms themselves as states, typed by derivatives-was pioneered by the Esterel programming language [14].
Semantics of the Right Rules.The right rules for parallel and concatenation are the simplest to understand.For S-Par-R, we accept an environment  and use it to run the component terms  1 and  2 , independently producing outputs  1 and  2 and stepping to new terms  ′ 1 and  ′ 2 .The pair term ( 1 ,  2 ) then steps to  ′ 1 ,  ′ 2 and produces the output parPair( 1 ,  2 ).There are two rules, S-Cat-R-1 and S-Cat-R-2, for running the concatenation pair ( 1 ;  2 ) :  • .In either case, we begin by running  1 with the environment , producing a prefix  and term  ′ 1 .If  is not maximal, we stop there: more of the input is needed for the first component to produce the rest of , so it is not yet time to start running  2 to produce .This case is handled by S-Cat-R-1, where the resulting term is  ′ 1 ;  2 and the output prefix is catFst().On the other hand, if  is maximal, then we also run  2 , which steps to  ′ 2 and produces a prefix  ′ using rule S-Cat-R-2; the entire term then outputs catBoth(,  ′ ) and steps to  ′ 2 .Note that the pair is eliminated in the process: we step from ( 1 ;  2 ) to just  ′ 2 .This is because we are done producing the  part of the  • , and so a subsequent step of evaluation only has to run  ′ 2 to produce the rest of the .Semantics of Variables.The variable semantics S-Var is a simple lookup.We find the prefix bound to the variable  in the environment, return it, and then step to  itself.
Semantics of Left Rules.The left rules for concatenation and parallel are similar, both accepting an environment  with a binding for  :  ⊗  where ⊗ is one of the two products, binding variables  and  of types  and  to the two components of the product, and using the updated environment to run the continuation term.In the case of the left rule for parallel (S-Par-L), looking up  of type  ∥ will always yield a prefix parPair( 1 ,  2 ).The rule binds  1 to  and  2 to  and runs the continuation term, stepping to  ′ and producing the output prefix .Then the whole term steps to let (, ) =  in  ′ and produces .The left rule for concatenation has two cases, depending on what kind of prefix comes back from the lookup for .If the lookup yields is catFst(), then the rule S-Cat-L-1 applies.Since no data for  has arrived, we bind  to emp  , the empty prefix of type , and run the continuation. 4If the result comes back as catBoth(,  ′ ), then the rule S-Cat-L-2 applies, so we run the continuation with  and  bound to  and  ′ .
Both rules output the prefix obtained from running the continuation, but they step to different resulting terms.If  () = catFst(), then the resulting term must be another use of Cat-L: the variable  still expects to get some more of the first component of the concatenation, and then the second component.If  () = catBoth(,  ′ ) on the other hand, the  stream has crossed over to the second part.In this case, we close over the (now not-needed)  variable in  ′ and connect  to the  input of  ′ by substituting  for .
Derivatives.When  : prefix (), we write   () for the derivative [16] of  by -the type of streams that result after a prefix of type  has been "chopped off" the beginning of a stream of type .Because this operation is partial-  () is only defined when  : prefix ()-we formally define this as a a 3-place relation, written as   () ∼  ′ and pronounced as "the derivative of  with respect to  is  ′ " (see Figure 5).
The derivative of the type 1 with respect to the empty prefix oneEmp is 1 (the rest of the stream is the entire stream), and its derivative with respect to the full prefix oneFull is  (there is no more stream left after the unit element has arrived).For parallel, the derivative is taken component-wise.
The interesting cases are those for the concatenation type.If the prefix has the form catFst(), the derivative , some of the  has gone by but not all, and once it does we still expect  to come after it.On the other hand, if the prefix has the form catBoth(,  ′ ), the derivative  catBoth(, ′ ) ( • ) is just   ′ (), i.e., the  component is complete, and the rest of the stream is just the part of  after  ′ .This definition is lifted to contexts and environments pointwise: if  :  is a variable in Γ, the derivative of   (Γ) has  :   ( ) () in the same location.

The Homomorphism Property and Determinism
The semantics is designed to run a stream transformer on "input chunks" of any size, from individual input events one at a time all the way up to the entire stream at once.The cost of this flexibility is that it raises the question of coherence-i.e., whether we are guaranteed to arrive at the same final output depending on how we carve up a transformer's input into a series of prefixes.Fortunately, this is indeed guaranteed.Coherence is a corollary of our main technical result: a homomorphism theorem that says running a term  on an environment  and then running the resulting term  ′ on an environment  ′ of appropriate type produces the same end result as running  on the combined environment (cf.[58]).Theorem 3.2 (Homomorphism Theorem).Suppose (1) Γ ⊢  : , (2)  : env (Γ), (3)  ′ : env   (Γ) , (4)  : prefix (), (5)  ′ : prefix   () , (6)  ⇒  ↓  ′ ⇒ , and (7) The operation  •  ′ here is prefix concatenation, which takes a prefix  of type  and a prefix  ′ of type   () and produces the prefix of type  that is first  and then  ′ .Formally, this is defined as a 4-place partial inductive relation  •  ′ ∼  ′′ , which is defined when  and  ′ have types  and   (), respectively.The operation  •  ′ ∼  ′′ does the same for environments. .The homomorphism theorem not only justifies running the semantics on prefixes of any size; it also implies deterministic processing of parallel streams.Intuitively, determinism states that the results of a stream transformer do not depend on the particular order in which parallel data arrives.We formalize this through the following scenario.Suppose Γ , Γ ′ ⊢  :  is a term with two parallel contexts serving as its input, and suppose that  is an environment for Γ , Γ ′ .Write  1 = | Γ and  2 = | Γ ′ , for the restrictions of  to the variables in Γ and Γ ′ , respectively.There are two different ways of running  on this data.One is to first run  on  1 ∪ emp Γ ′ (which has  1 bindings for Γ and then the empty prefix for everything in Γ ′ ) and then run the resulting term on  2 ∪ emp Γ (with an empty prefixes for Γ).The other does the opposite, first running  on  2 ∪ emp Γ and then running the resulting term on  1 ∪ emp Γ ′ .Determinism says that these strategies produce equal results.
See the appendix of the extended version [22] for the proof.To intuitively see how this theorem follows from homomorphism, note that prefixes are canonical representatives of equivalence classes of sequences of stream elements, up to the possible reorderings defined by their type [70].The homomorphism theorem then guarantees that these normal forms are processed compositionally, and so are independent of the actual temporal ordering of parallel data-it suffices to compute on the combined normal forms from the two steps.

FULL 𝜆 ST
We now sketch the remaining types and terms of  ST that are not part of Kernel  ST .

Sums
Sum types in  ST , written  + , are tagged unions: a stream of type  +  is either a stream of type  or a stream of type , and a consumer can tell which.Streams of type  are not the same as streams of type  + , and streams of type  +  are isomorphic to, but not identical to, streams of type  + .Operationally, a producer of a sum stream sends a tag bit before sending the rest of the stream, to tell downstream consumers which side to expect.Conversely, a consumer of  +  first reads the bit to learn which it is getting next.
A prefix of  +  can be a prefix of one of  or one of , written sumInl() or sumInr(), or it can be sumEmp, the empty prefix of type  + , which does not even include the initial tag bit.The derivatives with respect to these prefixes are defined as follows: The typing rules for sums are the normal injections on the right (T-Sum-R-1 and a symmetric rule T-Sum-R-2) and a case analysis rule on the left (T-Sum-L-Surf).The right rules operate by prepending their respective tags and then running the embedded terms.The left rule does case analysis: if the incoming stream  comes from the left of the sum, it is processed with  1 ; if from the right,  2 .To run a sum case term, the semantics must dispatch on the tag that says if the stream  being destructed is a left or a right.But the prefix  might not include a tag, if only data from the surrounding context has arrived.In this case,  will map to sumEmp, and we have no way of determining which branch to run.The solution is to run neither!Instead, we hold on to the environment, saving all incoming data to the program until the tag arrives.Once we get a prefix that includes the tag, we continue by running the corresponding branch with the accumulated inputs.Note that this buffering is necessarily a blocking operation.All this requires a slightly generalized typing rule (T-Sum-L) that includes a buffer environment  : env (Γ( :  + )) of the context type in the term.This buffer holds all of the input data we've seen so far.As prefixes arrive, 5 Depending on the rest of the context, it could also require unbounded memory!Fortunately, we believe we can detect this, and flag it as a warning to the user: running a case on  :  +  in a context Γ ( :  +  ) could require buffering all variables to the left of  or in parallel with  in the context.Unbounded memory is required if and only if any of those variables have star type.We hope to demonstrate this formally in future work.we append to this buffer until we get the tag.Accordingly, the context in this rule is   (Γ( :  + )): the term is typed in the context consisting of everything after the part of the stream that has so far been buffered.
Fortunately, the only typing rule that a  ST programmer needs to concern themselves with is T-Sum-L-Surf.While writing the program, and before it runs, the buffer is empty ( = emp Γ (:+ ) ).In this case, the   (Γ( :  + )) = Γ( :  + ), and so the generalized rule T-Sum-L simplifies to the "surface" rule, T-Sum-L-Surf.Full details can be found in the extended version [22].

Star
Full  ST also includes a type constructor for unbounded streams, written  ★ because it is inspired by the Kleene star from the theory of regular languages.(We do not need to distinguish between unbounded finite streams and "truly infinite" ones, because our operational semantics is based on prefixes: we're always only operating on "the first part" of the input stream, and it doesn't matter whether the part we haven't seen yet is finite or infinite.)The type  ★ describes a stream that consists of zero or more sub-streams of type , in sequence.
In ordinary regular languages,  ★ is equal to  +  •  ★ .In the language of stream types, this equation says that a stream of type  ★ is either empty () or a stream of type  followed by another stream of type  ★ -i.e.,  ★ can be understood as the least fixpoint of the stream type operator  ↦ →  +  • .The definitions of prefixes and typing rules for star all follow from this perspective.
In particular, prefix( ★ ) = prefix( +  •  ★ ).The empty prefix of type  ★ , written starEmp, is effectively the empty prefix of the sum that makes up  ★ .The second form of prefix-the "done" prefix of type  ★ -is written starDone.It corresponds to the left injection of the sum, and receiving it means that the stream has ended.Note that, despite containing no  data, this prefix is not empty: it conveys the information that the stream is complete.The final two cases correspond to the right injection of the sum, i.e., a prefix of type  •  ★ .This is either starFirst(), with  a prefix of , or starRest(,  ′ ), with  a maximal prefix of type  and  ′ another prefix of  ★ .
For derivatives, the empty prefix leaves the type as-is ( starEmp  ★ =  ★ ).Because no data will arrive after the done prefix, the derivative of  ★ with respect to starDone is .In the case for starFirst(), after some of an  has been received, the remainder of  ★ looks like the remainder of the first  followed by some more  ★ , so the derivative is defined as  starFirst( The typing rules for star are again motivated by the analogy with lists.There are right rules for nil and cons and a case analysis principle for the left rule.The "nil" rule T-Star-R-1 corresponds to the left injection into the sum  ★ =  +  •  ★ : from any context, we can produce  ★ by simply ending the stream.The "cons" rule T-Star-R-2 is the right injection: from a context Γ;Δ, we can produce an  ★ by producing one  from Γ and the remaining  ★ from Δ. Operationally, this should run the same way as the T-Cat-R rule: by first running  1 , and if an entire  is produced, continuing by running  2 to produce some prefix of the tail.The T-Star-L rule is a case analysis principle for streams of star type: either such a stream is empty, or else it comprises one  followed by an  ★ .The fact that the head  will come first and the tail  ★ later tells us that the variables  :  and  :  ★ should be separated by a semicolon in the context.Like T-Sum-L, this rule includes a buffer, collecting input environments until the prefix bound to  is enough to make the decision for which branch of the case to run. The Full  ST also allows for more general letbinding.Given a transformer  whose output is used in the input of another term  ′ , we can compose them to form a single term let  =  in  ′ that operates as the sequential composition of  followed by  ′ .The rules for this construct are in Figure 6.Note that this sequencing is not the same kind of sequencing as in a concat-pair (;  ′ ).The latter produces data that follows the sequential pattern  • , while the former is sequential composition of code.When a let binding is run, both terms are evaluated, and the output of the first is passed to the input of the second.An important point to note is that this semantics is non-blocking: even if  produces the empty prefix, we still run  ′ , potentially producing output.The semantic rule S-Let for letbinding (in Figure 6) is a straightforward encoding of this behavior.Given the input environment , we run the term , bind the resulting prefix  to , and run the continuation  ′ , returning its output.The resultant term is another let-binding between the resultant terms of  and  ′ .
The typing rule T-Let says that if  has type  in context Δ and  ′ has type  in a context Γ( : ) with a variable of type , we can form the let-binding term let  =  in  ′ , which has type  in context Γ(Δ).The soundness of the semantics rule S-Let depends on a subtle requirement:  must not produce nonempty output until  ′ is ready to accept it.This is enforced by the third premise of the T-Let rule, which states that  must be inert: it only produces nonempty output when given nonempty input.This restriction rules out let-bindings such as let  = () in  ′ , since the semantics of () always produces nonempty output (namely oneFull), even when given an environment mapping every variable to an empty prefix 6 .In actuality, inertness is not a purely syntactic condition on terms, but depends also on typing information.To this end, inertness is tracked like an effect through the type system: see the appendix [22] for details.

Recursion
To write interesting transformers over  ★ streams, we provide a way to define transformers recursively.Adding a traditional general recursion operator fix( .)does not work in our context, as arrow types are required to define functions this way.We instead add explicit term-level recursion and recursive call operators.The program fix  args .() defines a recursive transformer with body  and initial arguments  args .Recursive calls are made inside the body  with a term rec  args , which calls the function being defined with arguments  args .This back-reference works in the same way that uses of the variable  in the body of a traditional fix point fix( .)refer to the term fix( .)itself.This function-free approach is approach is inspired by the concept of cyclic proofs [15,24,29] from proof theory, where derivations may refer back to themselves.Alternatively, one can think of this construction as defining our terms and proof trees as infinite coinductive trees; then the term-level fix operator defines terms as cofixpoints.
In brief, to typecheck a fixpoint term, we simply type its body , assuming that all instances of the rec in  have the same type as the fixpoint itself.Then, to run a fixpoint term fix  args .(), the rule unfolds the recursion one step by substituting the body  for instances of rec in itself, then runs the resulting term, binding all of the arguments to their variables.Full details of the typing rules and semantics of fixpoints can be found in the extended version [22].
Naturally, this semantics can lead to non-termination, as fix(rec) unfolds to itself. 7To bound the depth of evaluation, we step index both semantic judgments by adding a fuel parameter that decreases when we unfold a fix.The semantic judgment then looks like  ⇒  ↓   ′ ⇒ : when we run  on , it steps to  ′ producing  and unfolding at most  uses of fix along the way.

Stateful Transformers
In the  ST typing judgment Γ ⊢  : , the variables in Γ range over future values that have yet to arrive at the transformer .The ordered nature of semicolon contexts means that variables further to the right in Γ correspond to data that will arrive further in the future.This imposes a strong restriction on programming: if earlier values in the stream are used at all, they must be used before later values; once a value in the stream has "gone by," there is no way to refer to it again.By using variables from the Γ context, a term  can refer to values that will arrive in the future; but it has no way of referring to values that have arrived in the past.This limitation is by design: from a programming perspective, referring to variables from the past requires memory, which is a resource to be carefully managed in streaming contexts.Of course, while some important streaming functions (e.g., map and filter) can get by without state, but many others (e.g., "running sums") require it.In this section, we add support for stateful stream transformers.
To maintain state from the past, we extend the typing judgment of  ST to include a second context, Ω, called the historical context, which gives types to variables bound to values stored in memory.We write Ω | Γ ⊢  :  to mean " has type  in context Γ and historical context Ω".
What types do variables in the historical context have?Once a complete stream of type Int ★ ∥Int ★ • Int ★ has been received and is stored in memory, we may as well regard the data as a value of the standard type (list(Int) × list(Int)) × list(Int) from the simply typed lambda-calculus (STLC).In other words, parts of streams that will arrive in the future have stream types, parts of streams that have arrived in the past can be given standard STLC types.The "flattening" operation ⟨⟩ transforms stream types into STLC types.The interesting cases of its definition are The historical context is a fully structural: Ω ::= • | Ω,  : , where the types  are drawn from some set of conventional lambda-calculus types including at least products, sums, a unit, and a list type.Operationally, the historical context behaves like a standard context in a functional programming language: at the top level, terms to be run must be typed in an empty historical context; at runtime, historical variables get their values by substitution.
Ω ⊢  : ⟨⟩ Rather than giving a specific set of ad-hoc rules for manipulating values from the historical context, we parameterize the  ST calculus over an arbitrary language with terms , typing judgment Ω ⊢  : , and big-step semantics  ↓ .We call any such fixed choice of language the history language.Programs from the history language can be embedded in  ST programs using the T-HistPgm rule, which says that a historical program  of type Ω ⊢  : ⟨⟩ with access the historical context can be used in place of a  ST term of type .Operationally, as soon as any prefix of the input arrives, we run the historical program to completion and yield the result as its stream output (after converting it into a value of type ).
How does information get added to the historical context?Intuitively, a variable in Γ (a stream that will arrive in the future) can be moved to Ω, where streams that have arrived in the past are saved, by waiting for the future to become the past!Formally, we define an operation called "wait, " which allows the programmer to specify part of the incoming context and block this subcomputation until that part of the input stream has arrived in full.Once it has, we can bind it to the variables in the historical context and continue by running .
Ω,  : The T-Wait-Surf rule encodes the typing content of this behavior.It allows us to specify a variable  of the input, flatten its type, and then move it to the historical context, so that the continuation  can refer to it in historical terms.Semantically, this works by buffering in environments until a maximal prefix for  has arrived.Once we have a full prefix for , we substitute it into  and continue running the resulting term. 8This buffering is implemented the same way as in the left rules for plus and star, by generalizing the typing rule T-Wait-Surf to a rule T-Wait which includes an explicit prefix buffer.As with plus and star, the generalized rule simplifies to the surface rule when the buffer is empty.The generalized rule and the semantics of both the wait and historical program constructs can be found in the extended version [22].The remaining typing rules in  ST change only by adding an Ω to the typing judgment everywhere.
Updated Soundness Theorems.Adding recursion and the historical context requires us to update to the soundness theorem from that of Kernel  ST to Full  ST .If a well-typed term has (a) closed historical context, and (b) no unbound recursive calls, takes a step on a well-typed input using some amount of gas, then the output and resulting term are also well typed.A similarly updated statement of the homomorphism theorem can be found in the full version [22].

DELTA
We next show how  ST addresses the problems that we identified in Section 2 of (a) type-safe programming with temporal patterns and (b) deterministic processing of parallel data.We also show how some other characteristic streaming idioms can be expressed elegantly in  ST .
The examples in this section are written in Delta, 9 an experimental language design based on  ST .Delta proposes a high-level functional syntax that, after typechecking, is desugars to  ST terms.It further supports some features that are not included in the  ST calculus, but that we expect will be required in full-blown language designs based on  ST .

Delta Syntax and Features
While the proof terms of  ST allow elimination forms (such as let (x,y) = z in e) to be applied only to variables (an artifact of the sequent calculus-style formalism), Delta's syntax is a standard ("natural deduction style") one where elimination forms can be applied to arbitrary expressions.Delta also includes more types than  ST , adding base types Int and Bool.If the macro g is recursive, its recursive calls do not receive a macro argument-all recursive usages of a macro get passed the initial macro parameter f.This discipline ensures that the macro usage does not depend on runtime data, and so higher-order functions can be fully resolved to  ST terms statically.
Neither of these features-standard top-level functions and higher-order macros-require the use of first-class function types, which  ST does not currently support.Defining true higher-order functions would allow for streams of functions, such as ( → ) ★ .We hope to investigate these in future work; see Section 7.
Functions in Delta can also be (prenex-) polymorphic [61].Polymorphic functions definitions are annotated with an list of their type arguments, like fun f[s,t](x : s*) : t* = e.When such a function is called, the type arguments must be passed explicitly like f[Int,Bool].
Historical Arguments and Generalized Wait.Functions in Delta also take arguments for their historical contexts: a function fun f{acc : Int}(xs : Bool*) : Int* = e takes an in-memory Int argument and elaborates to a core term that satisfies the typing judgment acc : Int | xs : Bool ★ ⊢  : Int ★ .When f is called, the acc argument must be passed a historical program.For example, if u : Int is in the current historical context (and ys : Bool* in the regular one), f{u + 1}(ys) is an acceptable call to f.
The wait construct is also slightly more general in Delta.Instead of just waiting on variables, programmers may wait on the result of some expression, and then save its result into memory.This is written wait e as x do e' end.
Delta Implementation.The implementation first lowers the surface syntax to an "elaborated syntax" via a transformation which eliminates shadowing, resolves function calls, and transforms the syntax into the sequent calculus representation by introducing intermediate variables for subexpressions.Elaborated terms are then typechecked.Typechecking expands macros and produces monomorphizers of  ST terms: functions from closed types (to plug in for type variables) to monomorphic  ST terms.Terms of base type can then be evaluated with a definitional interpreter that implements the  ST semantics.

Examples
Besides its type system, Delta's design differs from that of most stream processing languages in another important respect.In languages like Flink [30], Beam [35], and Spark [33], streaming programs must be written using a handful of provided combinators like map, filter, and fold (or possibly as SQL-style queries, in languages derived from CQL [6]).By contrast, Delta programs are written in the style of functional list processors.Instead of working to cram complex program behaviors into maps, filters, and folds, programmers can express their intent more directly in the form of general recursive functional programs.Of course, this does not preclude the use of the aforementioned combinators, which are directly implementable in Delta.For space, we omit the

RELATED WORK
Streams as a programming abstraction have their sources in early work in the programming languages [17,49,71,73] and database [1, 2, 5-7, 21, 57] communities.Though streams have mostly been viewed as homogeneous sequences, more interesting treatments have also been proposed.For example, streams in the database literature are sometimes viewed as time-varying relations, while the PL community has produced formalisms like process calculi and functional reactive programming.To our knowledge, ours is first type system for stream programming capturing both (1) heterogeneous patterns of events over time and (2) combinations of parallel and sequential data.
Sequential, homogeneous streams and dataflow programs.Traditionally, streams have been viewed in the PL community as coinductive sequences [17]: a stream of A has a single (co)constructor, cocons : Stream → ( × Stream ) and acts as a lazily evaluated infinite list.In particular, this is the setting of traditional dataflow programming [71].One major challenge in reasoning about dataflow over sequential streams is the nondeterminism arising from operators whose output may depend on the order in which events arrive on multiple input streams.Kahn's seminal "process networks" [49] (including their restriction to synchronous networks [12,56,73]) avoid this problem by allowing only blocking reads of messages on FIFO queues.In contrast, the semantics of  ST leverages its type structure to guarantee deterministic parallel processing without blocking in many cases.For example, in the context of a T-Let rule, if the type system can detect statically that a transformer is using two parallel streams safely, it can read from them simultaneously.
Partitioned streams.Building on streams as homogeneous sequences, modern stream processing systems such as Flink [18,30], Spark Streaming [33,78], Samza [32,62], Arc [55], and Storm [34] support dynamic partitioning: a stream type can define one stream with many parallel substreams, where the number of substreams and assignment of data to substreams is determined at runtime.The type Stream t in these systems is implicitly a parallel composition of homogeneous streams: t ★ ∥ • • • ∥t ★ .Unlike in  ST , these parallel substreams cannot have more general types.
Some which papers which attempt to build very general compile targets for stream processing support parallelism in only restricted ways.For example, Brooklet [69] and the DON Calculus [25] support data parallelism only as an optimization pass in limited cases.This is because stream partitioning does not in general preserve the semantics of the source program and can introduce undesirable nondeterminism [42,59,67].While  ST does not support dynamic partitioning, we hope to address it in future work; see Section 7.
Streams as time-varying relations.In the database literature, streams are often viewed as relations (sets of tuples) that vary over time.Stream management systems in the early 2000s pioneered this paradigm, including Aurora [2] and Borealis [1], TelegraphCQ [21] and CACQ [57], and STREAM [5].A time-varying relation can be viewed as either a function from timestamps to finite relations or an infinite set of timestamped values; this correspondence was elegantly exploited by early streaming query languages such as CQL [6,7] and remains popular today [11,46].Time-varying relations can be expressed in  ST using Kleene star and concatenation: a relation of tuples of type T timestamped by Time can be expressed as T ★ • Time ★ .We can also express the common pattern where parallel streams are synchronized by a single timestamp (again, modulo dynamic partitioning) with types like T ★ ∥T ★ • Time ★ .Each Time event is a punctuation mark containing the timestamp of the prior set of tuples [48,76].Traditional systems include separate APIs for operations that modify punctuation (e.g., a delay function that increments timestamps); whereas in our system they are ordinary stream operators and punctuation markers are ordinary events.
Streams as Pomsets and Monoids.A sweet spot between the homogeneous sequential and relational viewpoints is found in prior work treating streams as pomsets (partially ordered multisets) [3,[50][51][52]59], inspired by work in concurrency theory [26,60].In a pomset, data items may be completely ordered (a sequence), completely unordered (a bag), or somewhere in between.Some recent works have proposed pomset-based and structured monoid-based types for streams [3,58,59], but their types do not include concatenation and do not come with type systems-programs must be shown to be well typed semantically, rather than via syntactic typing rules.
Functional reactive programming (FRP) [27] treats programs as incremental, reactive state machines written using functional combinators.The fundamental abstraction is a "signal": a timevarying value Sig(A) = Time -> A. Work on type systems for FRP has used modal and substructural types [8,9,19,54] to guarantee properties like causality, productivity, and space leak freedom.While our type system is not designed to address these issues, it does incidentally have bearing on them.For one, our incremental semantics demonstrates that  ST 's type system enforces causality: since outputs that have been incrementally emitted cannot be retracted or changed, the type system must ensure that past outputs cannot depend on future inputs.Similarly, potential space leaks can be detected statically by checking that only bounded-sized types are buffered using wait or the buffering built into the left rules for sums and star.Our current calculus does not guarantee productivity (new inputs must eventually produce new outputs), but in Section 7 we discuss how to remedy this by imposing guardedness conditions on recursive calls.
Jeffrey [47] permits the type of a signal to vary over time, using dependent types inspired by Linear Temporal Logic [65].This system includes an until type that behaves like our concatenation type: a signal of type    is a signal of type , followed by a signal of type .However, unlike parallel streams in our setting, time updates in steps, discretely; i.e., parallel signals all present new values together, at the same time.Concurrently with our work, Bahr and Møgelberg [10] proposes a modal type system to weaken the synchronicity assumption; however, it still treats signals as homogeneous: the type of data cannot change over time.Lastly, Paykin et al. [64] develop a modal type system which expresses low-level event handlers.These are also purely synchronous, and the programs are written as event handlers as opposed to high-level "batch" processors.
Stream Runtime Verification (SRV) aims, broadly, to monitor streams at runtime and provide boolean or numerical "triggers" that fire when they satisfy some specification.Many RV projects like LOLA [23], HLola [20], RTLola [28], Striver [41], HStriver [40] also provide high-level, declarative specification languages for writing such monitors.Because these languages often use regular expressions or LTL as a formalism, they often bear a resemblance to our stream types.Despite this similarity, our goals and methods are quite different.Unlike the dynamically-checked specifications of SRV, the types in Delta are static guarantees: a stream program of type  necessarily produces a stream of type .
Streaming with Laziness.It is folklore in the Haskell community that a "sufficiently lazy" list program can be run as a streaming program using a clever trick with lazy IO [53,74].This "sufficient laziness" condition is syntactically brittle, and requires an expert Haskell programmer to carefully ensure that all functions involved are lazy in the just the right way.Indeed, many Haskell programmers instead reach for combinator libraries like Pipes [38], FoldL [39], Conduit [68], Streamly [72], and others to ensure their programs have a streaming semantics.In Delta, the type system takes care of this for you: all well-typed programs can be given a streaming semantics.Moreover, the  ST semantics gives a direct account of how pure functions execute incrementally as state machines, as opposed to the way that Haskell's non-strict semantics incidentally yields streaming behavior when combined with Lazy IO.
Session types and process calculi.Another large body of work with similar vision is session types for process calculi [44], where types describe complex sequential protocols between communicating processes as they evolve through time.A main difference from our work is that the session type of a process describes the protocol for its communications with other processes-i.e., the sequence of sends and receives on different channels-while the stream type of a  ST program describes only the data that it communicates.Indeed, a stream transformer might display many patterns of communication with downstream transformers: it can run in "batch mode"-sending exactly one output after accepting all available input-or in a sequence smaller steps, sending along partial outputs as it receives partial inputs.Also, a single channel in a process calculus cannot carry parallel substreams: all events in a channel are ordered relative to each other.Recently, Frumin et al. [37] proposed a session-types interpretation of BI that uses the bunched structure very differently from  ST .In particular, processes of type  *  and  ∧  both behave semantically like a process of type  in parallel with a process of type , while, in  ST ,  •  and  ∥ describe very different streams.
Concurrent Kleene Algebras and regular expression types.Stream types are partly inspired by Concurrent Kleene Algebras (CKAs) [43] and related syntaxes for pomset languages [52], but we are apparently the first to use these formalisms as types in a programming language rather than as a tool for reasoning about concurrency.In particular, traditional applications of Kleene algebra such as NetKAT [4] and Concurrent NetKAT [77] use KA to model programs, whereas in  ST we use the KA structure to describe the data that programs exchange, while the programs themselves are written in a separate language.We have also taken inspiration from languages for programming with XML data [13, 36, 45, etc.] using types based on regular expressions.

CONCLUSIONS AND FUTURE WORK
We have proposed a new static type system for stream programming, motivated by a novel variant of BI logic and able to capture both complex temporal patterns and deterministic parallel processing.
In the future, we hope to add more types to  ST .Adding a support for bags-unbounded parallelism, the parallel analog of Kleene star-would enable dynamic partitioning. ST also lacks function types.The proof theory of BI would imply that there should be two (one for each context former), but we have yet to investigate what these functions might mean in the streaming setting.
Further theoretical investigations include (1) alternate semantics for stream types, including a denotational semantics as pomset morphisms, Kahn Process Networks [49], or some category of state machines, (2) eliminating the inertness restriction on let-bindings, and (3) adding a guardedness condition on recursive calls to ensure termination and hence productivity.
On the applied side, we plan to build a distributed implementation of Delta by compiling  ST terms to programs for an existing stream processing system like Apache Storm [34], thus inheriting its desirable fault-tolerance and delivery guarantees.We hope to build such a compiler and use it as a platform for experimenting with type-enabled optimizations and resource usage analysis.

Functions
and Macros.Top-level functions in Delta are simply open terms: a function definition fun f(x : Int*) : Int* = e elaborates and typechecks to a core term  which satisfies the typing judgment  : Int ★ ⊢ e : Int ★ .Higher order functions in Delta are implemented as macros.A function written as fun g<f : Int -> Int>(x : Int*) : Int* = e is a macro which takes another function f : Int* -> Int* as a parameter.Calls to g in other functions then look like g<f'>, where f' is either (a) another function defined at top level, or (b) a call to yet another macro.
) :  ∥ It works by splitting the variable  :  ∥ into variables  :  and  :  and yielding a parallel pair with the order reversed.