• No results found

Coding for stability

In document Interactive functional programming (Page 143-148)

Our visualisation code represents a different use case for interactive programming from those we have con- sidered so far. Until now we have focused on allowing the programmer to see the impact of code changes on the intensional structure of a computation. For application development, we are more concerned with

extensional behaviour, i.e. how an interactive program maps input changes to output changes. The behaviour we see is a consequence both of the strategy we use for assigning indices to values described in Chapter6 and of how we write the actual program. Studying this behaviour may help us design better differential execution schemes, or even different kinds of language better suited to differential execution. In this section we describe an idiom that has turned out to be essential for controlling the differential behaviour of the visualisation code, and illustrate with some examples from the code. The examples will also give the reader a feel for the complexity of the codebase, which is about 500 lines of LambdaCalc. Naturally, the figures are generated using the visualisation code itself.

For two LambdaCalc values to have the same identity in two different computations it is necessary, al- though not sufficient, that they be constructed by the same expression. Now suppose in our code we have a value which we want to construct with a particular constructor c, but for which where there are some con- ditional choices to be made to determine the arguments to be passed to c. Normally it would be common to duplicate the constructor code into each branch of the conditional logic. But the problem with this is that in different executions, different branches of the conditional may be taken and different expressions evaluated. So if we want it to be possible for this value to have the same identity in different executions, we have to swap things around so that there is a single constructor containing multiple copies of the conditional code.

fun executedBody showTrace tr3 →

let

bodyIntro:Glyph case browsingState tr3 of Expanded → fwBold otherwise → fwNorm textCol case browsingState tr3 of Expanded → liveArrow otherwise → ellipsis borderlessCell traceBackCol Horiz bodyIntro case browsingState tr3 of Expanded → Horiz Space onBackground traceBackCol showTrace tr3 otherwise → EmptyView

Figure B.2 Visualising executed function bodies

We see this pattern in the functionexecutedBody shown in FigureB.2, which visualises the execution of

a function body. We create a glyph, which we callbodyIntro, which will be in one of two states depending

on whether the function bodytr3is expanded. If the body is expanded, we want a control-flow arrow ։

with bold font-weight; otherwise we want an ellipsis with normal font-weight. In both cases, we also want to passtextColas the second component. We think of the three fields of the glyph as constituting its state

because we would like the ellipsis to be able to mutate into the arrow, and vice-versa, without the identity of the glyph changing. That can only be achieved if there is a centralised point of construction for the glyph. But that means duplicating the conditional code that switches on the browsing state oftr3.

which will appear in a value pane. Once again we centralise construction of a glyph, which we callglyph,

so that in different executions it has the potential to be assigned the same identity. Here, this necessitates duplicating the case analysis ofv. The idiom here is a little more problematic: we do not actually need the

glyph in every branch of the conditional which follows the construction ofglyph, but by centralising its

construction, we end up having to construct it even for cases ofvwhere we do not need it. This explains the otherwiseclause that returns the dummy value"Unused".

When the conditional logic is too complicated to duplicate, an alternative is to construct an interim tuple in each branch of the conditional, holding the arguments for c. The tuple can be unpacked after the conditional and its components used to construct c. We see this in thevisualise function given in FigureB.4, the

top-level function responsible for visualising a computation in a cell. The goal here is to centralise the construction of aRoundedCellso that it will be uniquely associated with the computationtr, the argument

tovisualise. The cell has two components: a trace viewwt’and an optional view panewv_optarranged in

an “overlay”, a kind of compact horizonal composition. To centralise the construction of the cell, we build an interim triple of the formPair(Pair(−,−),−)containing the raw materials, store it in the variableargs_,

and then unpack the components ofargs_at the bottom of the function when we are ready to build the cell.

Unfortunately there is no generalisation of this centralised construction idiom that permits the constructor itself to be conditionally selected. This is because a given constructor expression must always mention a specific constructor. It is not therefore possible to write a LambdaCalc program where, for example, a value which wasNilin one execution becomesConsin the next execution as the result of a different branch being

taken. This feels like a limitation: the user can freely edit aNilinto aCons, so it should equally be possible

to “compute” such a change.

We end by saying something about accumulators, which can be problematic. Changing the identity of an argument to a function changes the identity of any values constructed in the body of the function. If one of these values is an accumulator, these changes of identity will cascade to all recursive invocations. How- ever, this problem might not be as serious as it seems. Perhaps for historical reasons, functional programs tends to favour the use of lists when often more balanced data types would be more appropriate. Lists as normally defined have “right-bias”: a right-fold of a binary operation over lists can be expressed directly as a list catamorphism, but a left-fold cannot, requiring an accumulator. In practice, we have found that we can commonly avoid accumulators by switching to a data type which is more redundant but also more symmetric. For example, vertical and horizontal view composition are monoids, for which the bracketing structure is irrelevant. For list-like structures, we can use a data typeRListof reversible lists that combines

bothConscells andSnoccells:

data RList a = Nil | Cons a (RList a) | Snoc (RList a) a

with similar properties.

In conclusion, with some care LambdaCalc can be used quite effectively for differential execution. The evidence is the delta visualisations we have been able to produce for this thesis. If programs are to exhibit useful differential behaviour, the programmer must adopt some idioms for constructing values, be conscious of the identity of values passed as argument to functions, and sometimes rethink the data types their func- tions operate on. Beyond these considerations, the programmer can for the most part write standard, purely

functional code. We could have made these general points more clearly, perhaps, with simpler examples that did not involve the meta-circular use of LambdaCalc to implement some of its own behaviour. On the other hand, this route has allowed us to demonstrate some of the potential for using interactive programming techniques to build non-trivial applications.

fun showValue showTrace v → case v of NullValue → NullView otherwise → let showValue':showValue showTrace RoundedCell case v of

Constr(_,_) → Some borderCol

otherwise → None Background valueBackCol let glyph:Glyph fwNorm textCol case v of ConstInt(n) → intToString n Constr(c,vs) → string c Op(op_) → string op_

otherwise → "Unused." case v of ConstString(str) → showString str ConstInt(n) → glyph Constr(c,vs) → case isViewCtr c of True →

Horiz Separator borderCol reflectWidth

Horiz Background reflectBackCol reflect v Separator borderCol reflectWidth False → Horiz glyph case layoutVert c of False → argList showValue' vs True → Horiz Space constrArgs showValue' vs Op(op_) → glyph Closure(f,x,e,rho ) →

showTrace TraceResult Unknown rho FunT f x e None otherwise → error v "[showValue] Impossible."

fun visualise tr → case trace tr of NullTrace → NullView otherwise → let wt_opt:case collapsed tr of True → EmptyView otherwise → showTrace visualise tr let

traceOnly:Pair Pair None wt_opt EmptyView let args_:case trace tr of OpT(_) → traceOnly VarT(_) → traceOnly otherwise → case valueOpt tr of None → case trace tr of ConstIntT(_) → traceOnly ConstStringT(_) → traceOnly otherwise →

Pair Pair Some borderCol wt_opt EmptyView Some(v) →

let

const:isConstant tr Pair Pair Some borderCol

case const of

True → EmptyView False → wt_opt Horiz case const of

True → EmptyView

otherwise →

LeftConnector borderCol valueBackCol RoundedCell Some borderCol

onBackground valueBackCol

showValue showTrace visualise v let border:fst fst args_ let wt':onBackground traceBackCol snd fst args_ let wv_opt:snd args_ RoundedCell border Overlay wt' wv_opt

In document Interactive functional programming (Page 143-148)