Goal-Driven Code. Not all coding is sequential. For… | by Alain Marchand


Photo by Emile Perron on Unsplash

Summary: Not all coding is sequential. For non-specialists, the programming language is often a barrier. Whand is a simplified declarative language intended to control external devices as a function of events and the passage of time.

A Whand script essentially describes the desired result, unlike sequential languages focusing more on the means than the goal. In Whand, many operations leading to the goal are performed implicitly. Its syntax evokes a natural language and is relatively easy to understand and customize.

Whand is also parallel, making objects independent of one another and facilitating debugging. Developers are welcome to participate in the open-source Whand community.

There is much talk about the “Internet of Objects,” but it could mean that the general public will need to code. This is a real challenge. For instance, how do you set up the fridge to automatically re-supply?

By definition, computer code does not lend itself to easy reading. Even programmers find it difficult to decipher someone else’s code or sometimes to revisit their own code after a few months. The syntax and boilerplate of a language may baffle a neophyte. Variable names are not always clear, and the structure of a program may be hidden, even when comments (#) are present.

To bypass some of these complications, I designed an original language called Whand. Whand is aimed at non-programmers who want to control simple devices. Whand is not procedural but declarative. It is goal-driven, meaning that it emphasizes the result rather than the means. Its simplified syntax evokes a natural language, so it reads rather easily while being quite versatile (box 1). It also makes it simple to control various appliances without interference.

max_session_time: 30 min            # define named parameter

reward: when count press is in cumul random_list # when total is 5, 8, 15, 23…
until reward+500ms # automatic delay

exit when count reward = count random_list # conditions to terminate session
or when max_session_time since start # whichever happens first

light: # house light and lever are present (on)
lever: # for whole session

random_list: 5,3,7,8,2,6,4, 8,5,2,4,7,6,3, 6,5,7,3,4,2,8, \
3,4,5,8,6,2,7, 4,3,8,7,2,5,6, 7,5,4,6,3 # pseudo-random sequence

include “connections.txt” # saved script associating input to press
# and outputs to reward, light, lever

Box 1. A Whand script to control a Skinner box. Rewards are delivered after a variable number of presses on a lever. Definitions may be given in any order.

To compare Whand to a procedural language, let us consider a light with two states (on/off) and the actions needed to switch states. Here is our task in natural language:

Switch on light when daylight is less than threshold (e.g. 5 arbitrary units)
Switch off light when daylight is more than threshold

The following Whand script does exactly that:

light_on: when daylight < daylight_threshold   # define state 'light_is_on'
until daylight >= daylight_threshold

daylight_threshold: 5 # a named parameter

daylight: measure 2 # an input (keyword: 'measure')
output 1: light_on # an output (keyword: 'output')

The script closely resembles the initial specifications, and the boilerplate is kept to a strict minimum. The goal is stated up front — switch the light on or off. It depends on two causes: the current daylight level and the threshold value (Figure 1, left).

The when condition results in light_on, and the until condition results in light_off (i.e., not light_on). Each condition is true or false at any given instant, depending on daylight_threshold and measured daylight. There is no need to further specify how the goal should be achieved.

The measure 2 instruction assigns name daylight to a particular input (assuming a measure instrument is connected to line 2). The output 1 instruction directly associates the named variable light_on to output line 1 (hardware controlling the light).

Note: The order of the various instructions (declarations) is completely indifferent.

Compare this to a standard procedural language, here in pseudocode:

Procedure control_light:
set daylight_threshold to 5
set light_state to off
measure daylight
if daylight < daylight_threshold and light_state is off then:
switch_on light
set light_state to active
if daylight >= daylight_threshold and light_state is active then:
switch_off light
set light_state to off

The procedural code appears more complex, even though procedures to measure daylight and to switch lights on or off are not detailed. The order and structure of instructions are critical. We must explicitly set up a loop to test the daylight level and compare it to the threshold (Figure 1, right).

We need indentation and/or curly brackets to delimit the loop and conditional statements. The loop indefinitely repeats. We must also keep track of the state of the light.

Figure 1. Goal-driven vs. procedural approaches. Left panel: In a goal-driven language such as Whand, the goal to switch the light on or off immediately depends on two conditions relating the two causes: The measure of daylight is permanent. Daylight threshold is constant. All instructions are processed in parallel. Right panel: In a procedural language, processing is sequential and follows a loop structure. All processing takes place within a general loop that repeats indefinitely. One must test the daylight level and the light’s state before switching the light on or off.

There is a profound difference in philosophy between goal-driven and procedural languages.

The procedural code focuses on actions and relies on a specific sequence of instructions. The loop structure must be explicit. The calls to procedures light_on and light_off constitute the actual goal but are somewhat buried inside the code. The state of each appliance must be tested before deciding to switch it on or off.

The procedural code must continuously test the daylight level and cannot leave the loop. The new code must be incorporated within the general loop to control another appliance. The loop must avoid noticeable delays, and it cannot stop to wait for an event because this would suspend all other processing.

The goal-driven code focuses on a goal (the output) and declares it upfront. It then follows a top-down approach to specify the causes of this output. Each cause is defined as a function of lower causes — all the way down to inputs. Details of implementation are left to the system, with minimal specifications. No particular sequence or structure of instructions is required.

The goal is specified as a state, so the system internally knows whether the light is on or off and whether any action is needed to change the state. The loop that tests the state of every object is implicit.

Because processing is parallel, the code for another appliance can be added before or after the current code without any risk of interference.

In the procedural approach, we need to keep track of the state of our appliance (the light) before taking any action. Suppose our goal is to order food to replenish the fridge. We must then send the order only once and not repeat it until delivery. But because the state of the fridge is tested on each iteration of the loop, our trigger cannot be the depleted state per se. It has to be the transition from full to depleted.

With the procedural approach, we must compare the current state to the preceding state and/or keep track of the execution of the action, as below:

Procedure control_milk:
set milk_threshold to 2
set milk_order to none
measure milk_level
if milk_level <= milk_threshold and milk_order is none then:
set milk_order to pending
if milk_delivery and milk_order is pending then:
set milk_order to none

How milk level is evaluated is not detailed here. Note that the state of milk_order needs to be explicitly adjusted: it is initialized to none, set to pending as soon as the order is passed, and reset to none upon delivery.

In Whand, milk is ordered only once because the when and until conditions only respond to changes, here the transition from false to true, or in electronics terms, a rising edge.

milk_threshold: 2

milk_ordered when milk_level <= milk_threshold
until milk_delivery

Here the state milk_ordered will only pass an actual order on the rising edge of milk_level < milk_threshold, i.e., when it becomes true. While the state remains true afterward, there is no risk of passing another order until milk_ordered returns to false upon delivery (milk_delivery must be defined elsewhere). The transition to false does not trigger any action by itself. It only enables the occurrence of a new rising edge.

Of course, assuming that delivery always increases the milk level above the threshold, the expression milk_level < milk_threshold will also return to false. Thus, we could use the shorter formulation:

milk_threshold: 2

milk_ordered: milk_level < milk_threshold

Here, there is not even a when condition. The left term milk_ordered continuously tracks the value of the expression on the right milk_level < milk_threshold. Its rising edge will trigger the order and return to false as soon as milk_level rises above milk_threshold.

Both forms let the user focus on the state of the goal and its transitions. They do not require the precise combination of instructions characteristic of procedural language.

In the preceding example, note that there was a waiting period of undefined duration between the order and the milk delivery. Surely, after a certain delay, further action is required if milk is not delivered. In the context of automation, it is often necessary to set up delays.

A particular event triggers a delay and is expected to trigger some action after the specified time has elapsed automatically. For instance, a microwave oven needs to stop heating, or a light in the basement needs to be switched off.

These delays may be specified in the hardware, but sometimes, they require flexibility, for instance, if an action needs to be repeated at regular intervals.

Delays come in two flavors: non-resetting delays and resetting delays. A non-resetting delay (e.g., microwave cooking) ignores triggering events during the delay. Cooking duration is fixed once started (except when prematurely stopped).

A resetting delay computes time concerning the last occurrence of the triggering event. A basement light controlled by a movement detector will need a resetting delay because the light should persist for some duration. Still, it should also remain on as long as movement occurs.

Whand handles delays effortlessly:

cook_duration: 2 min

microwave_on: when button_pressed # off at start by default
until microwave_on + cook_duration # non-resetting delay

# microwave and light are independely controlled
basement_light_duration: 30 s

basement_light: when movement_detected # off at start by default
until basement_light_duration since movement_detected # resetting delay

Note that microwave_on + cooking_duration adds a delay to a logical event. This creates an internal event that is automatically delayed by the specified duration and then acts as a condition for the when or until clause.

The since operator is similar except for the resetting mechanism.

Procedural programming needs a clock that provides the current time. The instruction flow must be carefully designed:

Procedure control_timers:
set cook_duration to 2 min
set light_duration to 30 s
set microwave_state to off
set basement_light_state to off
read clock
if button_pressed and microwave_state is off then: # ignore button while on
set microwave_state to active
store clock as microwave_t0
if clock-microwave_t0 > cook_duration and microwave_state is active then:
set microwave_state to off

# microwave and light must be in the same loop
if movement detected then: # do not ignore movement
store clock as basement_t0
if basement_light_state is off then:
switch_on basement light
set basement_light_state to active
if clock-basement_t0 > light_duration and basement_light_state is active then:
switch_off basement light
set basement_light_state to off

Here, each timer needs its own start time memory t0. This value is then compared to the clock time on each loop iteration. The code also needs to keep track of the state of the microwave and the basement light.

Another use of delays is to set up a repetitive event. Here is a basic Whand structure applicable to events with periods ranging from tens of milliseconds to hours or days:

output 1: sound
sound_duration: 1 s
sound_period: 3.5 s

sound: when enable_signal
or when (sound + sound_period) and enable_signal
until sound_duration since begin sound
or until not enable_signal

The delayed expression sound + sound_period will automatically restart the sound after each period. It is, however, gated by the enable signal (defined elsewhere).

The expression until sound_duration since begin sound is preferable to until sound + sound_duration to avoid a possible delayed cutoff after a new enable signal.

Being declarative and not procedural, Whand does not provide any simple way to create loops. One must learn to think differently.

Firstly, the general loop that monitors all objects is implicit in Whand.

Secondly, Whand understands lists and provides a general method to repeat the same tests or processes on multiple items from a list or dictionary (in a list, elements are addressed by the position. In a dictionary, each element has a name and a value).

Finally, it is possible to use repetitive events to apply multiple steps on the same item or group of items. However, this problem is where Whand may be less convenient than a procedural language.

Whand generally allows one to work on a list like a single object. The difference, of course, is that the result is a list instead of a single result. Consider, for instance, the problem of keeping track of depleted items in stock, represented as a dictionary with names and quantity (percentage) remaining. We want to extract the items with less than 40% remaining.

To perform this function in a procedural language is not too complicated. We need something like:

depleted = pick_if_less(stock, 40)

with the following function definition:

Function pick_if_less (stock as list, n as number):
set selection to empty list
for each element in stock do:
if value(element) < n then:
append element to selection
return selection

This function uses a for loop to cycle through all elements in the list.

Whand instead allows the following instruction, using the internally defined function pick:

depleted: stock pick (stock<40)

The formula returns a list of items with a value lower than 40. There is no when condition, so the result is automatically updated in real time.

The power of list processing in Whand is not just predefined list functions such as pick, count, find or ramp. It is also the fact that a list is considered as a single object when submitted to elementary operations. This property, which may be called distributivity, is illustrated in the expression stock<40:

Operator < is expected to only compare numbers (or delays) to yield true or false. But because of distributivity, the comparison is actually performed between each value in stock and the number 40, so stock<40 returns a list of logical values. Note that the quantity and not the name of each item is compared to 40.

Such a logical list is exactly what the pick operator expects as a second argument. Thus, stock pick (stock<40) will use the list of logical values to extract the list of elements in stock that fulfill the condition element<40. A true value will include the element (with name and quantity); a false value will exclude it.

Distributivity is a general property of operators in Whand, and it greatly simplifies the processing of a list without requiring any loop structure. Moreover, all this is done while preserving the intuitive feel of the Whand code.

There is much more to Whand (operators, predefined functions, user-defined functions, etc.), but a full description would exceed the scope of this article.

Whand is unique because it completely forgoes sequential programming and is entirely built on the when syntax. Transitions of an event from false to true trigger the attribution of a value to the object. If no value is specified, true/false is assumed.

Five types of values are allowed: a logical event (true/false), a number, a delay, a state (a name), and a list (or dictionary). Operations between objects of different types are allowed if they make sense, e.g., delay1/delay2 is a number, but delay1*delay2 is not allowed.

The definition of an object is concentrated in one place. It completely specifies its behavior, which facilitates debugging. However, dictionary elements may be defined separately, although they belong to the same collection. Below is an example with a three-way light switch. The light is on when 1 or 3 buttons are on:

output(9): light_on                       # link variable to hardware

button(front): pin 1 # define one element (input)
button(middle): pin 2 # define another element
button(back): pin 3 # define yet another element

light_on: count pick(button) is in (1,3) # button contains three elements

pick(button) extracts buttons that are true (on). front, middle, and back are just labels (state values) and need not be explicitly defined.

Devising a procedural version is left to the reader.

A disadvantage of parallelism is that objects and expressions tend to be updated and evaluated in any order. A brief delay (epsilon) may be required to avoid testing a value or expression just when it transitions.

There are also instances where distributivity is not enough. For instance, assume we want to count the occurrences of items in a list, i.e., from the list (A,B,C,A,D,D,B,D,A,D) extract item (A,B,C,D) and counts (3,2,1,4).

It is not impossible to do this in Whand, but the code becomes less explicit:

L: A,B,C,A,D,D,B,D,A,D

names: L pick ((L find L)=(ramp(count L))) # extract unique items A,B,D,C

range: ramp(count names) # 1,2,3,4
clock: any(start+(range*2epsilon)) # four events spaced by 2*epsilon
index: count clock # index increments on each event

counts: when start: empty # initialize counts list as empty
when clock+epsilon: old add count(L pick (L=names(index)))# use clock as a loop

Deciphering the code is left as an exercise for the bold reader (hints: L find L lists the first position of each item in list L, and ramp count L is list 1,2,3,…,10).

Whand is open source and available for download or inspection at this link.

Full documentation about the language is provided on the site. Whand runs under Python on either Windows® or Linux platforms. Drivers for Raspberry Pi and ASinterface are provided. It is also possible to run Whand in simulation mode without external hardware. A control panel displays the state of selected variables throughout the test.

The software is currently used in the lab to control rodent conditioning cages from Imetronic® (Pessac, France). Several independent programs can be run in parallel, with all events recorded as text files.

Whand currently focuses on controlling on and off events on external hardware. It is comfortable with timing and can perform simple computations, but it is inappropriate for complex algorithms. Also, it does not allow the manipulation of documents except for reading or writing text files. However, an interface with Python procedures is provided so that its functions may be extended to cover further needs.

Whand simplifies coding by making implicit and automatic several functions that need to be explicit in procedural language. Variables/objects have values or states and transitions that can be directly exploited in conditions. The permanent loop that monitors inputs in procedural languages is entirely implicit.

Delays are seamlessly integrated with events and do not require reference to a clock. Lists are not browsed using loops but are treated globally using distributivity and list functions. The result is a code that is compact yet readable and intuitive.

I hope that a growing community of users will develop around the language and that developers may become interested in improving and extending it.

Any feedback, comments, or suggestions about Whand are welcome.


Source link

Related Articles

Back to top button