stateful

This hidden maze is stateful. With the correct sequence of actions, the marble will find the exit, but skip one action, and all is lost. This is because the position of the marble -the state of the maze- influences the result of any action.

Introduction

The term StateFul/StateLess is often used in communication protocols and comes from functionnal programming. We will focus here on a tiny aspect of this term, limited to the code we create in our humble scientific software craftsmanship.

A Stateful code implies that the developer using this code must pay a significant attention to some implicit information, which we will call a “state”. Oppositely, the code is stateless if it is presented in a way the developer can forget about a state. Fighting for a stateless programming interface is worth your attention, and here is why…

Making a Turtle state-less

In the famous coding game turtle, kids learn programming by giving a list of instructions to a “turtle” moving on a canvas. There is an online turtle you can use to try these examples. For example, the following code here draws a square

# Draw a square 
# numbers are the successive positions of the turtle
#        3┌──────┐2
#         │      │
#         │      │
#         │      │
#      0,4x──────┘1
t = turtle.Turtle('turtle')
t.forward(100)
t.left(90)
t.forward(100)
t.left(90)
t.forward(100)
t.left(90)
t.forward(100)
t.left(90)
turtle.done()

This coding game requires the Theory of mind, in other words the capacity to understand other entities by ascribing mental states to them. The mental states of the turtle are both its position (points 0,1,2,3 &4) and direction (resp. east, north, west, south).

Switch two instructions and the output of the game is totally wrong:

# Draw a square - mistaken
# numbers are the successive positions of the turtle
#  3       0,2      1
#  ┌────────────────
#  │
#  │
#  │
#  x 4
t = turtle.Turtle('turtle')
t.forward(100)     #
t.left(90)         #
t.left(90)         #
t.forward(100)     # swapping these calls changes output
t.forward(100)     #
t.left(90)         #
t.forward(100)     #
t.left(90)         #
turtle.done()

This is a typical drawback of a stateful programing.

However, you can draw the square segments in an almost stateless fashion like this:

# Draw a square, stateless
# numbers are the successive positions of the turtle
#      2,4┌──────┐1,7
#         │      │
#         │      │
#         │      │
#    0,3,6└──────┘5,8

t = turtle.Turtle('turtle')
t.penup()

def draw_segment(t,x0,y0,x1,y1):
    t.goto(x0,y0)
    t.pendown()
    t.goto(x1,y1)
    t.penup()

draw_segment(t,0,100, 100,100)#
draw_segment(t,0,0, 0,100)    # swapping these calls are allowed
draw_segment(t,100,0, 0,0)    #
draw_segment(t,100,100, 100,0)# 

turtle.done()

With this version, one can draw segments with the turtle without anticipating the state, be it position or orientation. It is therefore easier to extend. For example imagine how you would add two diagonals to the square with the initial, stateful version? And with the stateless one?

Stateless code is easier to read : the Pressure computation

We move to a more applied case to show the effect of a stateless code on readability.

Assume someone want compute Pressure (P) from temperature (T) and Density (Rho) with the ideal gas law.

state = [T, Rho]
print(state)
>> 300, 1.26
ideal_gas_law(state)
print(state)
>>  300, 1.26, 101325
print("pressure :", state[2])
>> pressure :101325

The function ideal_gas_law() is changing the state data, by adding a pressure to the list of state variables. The variable state must be kept in mind, since even its shape is changing in this dataflow. In particular : 1. it is not obvious to guess where pressure was computed. 1. ideal_gas_law() would probably fail if it was called a second time, or worse, work with wrong inputs. 1. the last print, done before the call of ideal_gas_law(), would raise a cryptic “IndexError”.

To make it stateless, we keep the shape of the variable state unchanged. pressure is a additional function ideal_gas_law2() which ignores the variable state and ask separately temperature and density.

state = [T, Rho]
print(state)
>> 300, 1.26
press = ideal_gas_law2(temp=state[0], rho=state[1])
print(state)
>>  300, 1.26
print("pressure :", press)
>> pressure :101325

This fixes our issues:

  1. The computation of the press variable is easy to spot.
  2. ideal_gas_law2() can be called multiple times.
  3. the last print, done before the call of ideal_gas_law(), would raise a clear variable press is not defined .

In other words, the data flow - the way the data is edited in the code - is easier to understand.

Spot the smell of a stateful development : A FORTRAN example

Imagine we are creating a FORTRAN feature able to concatenate strings, for example pif added to paf yields pifpaf.

program StringConcatenation
    implicit none

    character(len=10) :: string1 = "pif", string2 = "paf"

    ! Call the subroutine to concatenate the strings
    call concatenateStrings(string1, string2)

    ! Print the concatenated result
    write(*,*) "Concatenated String: ", trim(string1)

contains

    subroutine concatenateStrings(str1, str2)
        character(len=*), intent(inout) :: str1
        character(len=*), intent(in) :: str2

        ! Concatenate the strings
        str1 = trim(str1) // trim(str2)
    end subroutine concatenateStrings

end program StringConcatenation

This works, but is quite fragile. Indeed, the subroutine will work as expected only if there are less non-blanks characters of str2 than blanks characters at the end of str1:

str1 (len) str2 (len) result (len)
"paf "(10) "pif "(10) "pafpif "(10)
"paf "(10) "pif_000000"(10) "pafpif_000"(10)
"paf_000000"(10) "pif_000000"(10) "paf_000000"(10)

As the size of str1 is set at the input and not changed, it will impact the output. This is because str1 is both the input and the output of concatenateStrings() (intent INOUT). It is a stateful data.

At this point one could start a clever development with a str1 reallocated in the routine to change its lenght, plus maybe a clever way to tell the main StringConcatenation program that str1 changed… or switch to a stateless version:

program StringConcatenation
    implicit none

    character(len=10) :: resultString
    character(len=3) :: string1 = "pif", string2 = "paf"

    ! Call the subroutine to concatenate the strings
    call concatenateStrings(string1, string2, resultString)

    ! Print the concatenated result
    write(*,*) "Concatenated String: ", trim(resultString)

contains

    subroutine concatenateStrings(str1, str2, result)
        character(len=*), intent(in) :: str1, str2
        character(len=*), intent(out) :: result

        ! Concatenate the strings
        result = trim(str1) // trim(str2)
    end subroutine concatenateStrings

end program StringConcatenation

With this stateless version, both input and output are separatelty set in the main program, and the surbroutine just takes in whatever size is set.

The new code is not able to auto-adapt to all situations, but surprises are easier to spot and correct from the main program:

str1 (len) str2 (len) result (len)
"paf"(3) "pif"(3) "pafpif "(10)
"paf"(3) "pif_000000"(3) "pafpif "(10)
"paf"(3) "pif_000000"(10) "pafpif_000"(10)
"paf"(3) "pif_000000"(10) "pafpif_000000"(13)

Next time you are using an INOUT parameter in Fortran, do consider spliting it into separate inputs and outputs: it could be closer to your expectation than you think at first!

How far a code should be stateless?

Stateless is not a silver bullet to remove dirty code, so do not overdo it : it could backfire.

To our experience up to now, it is good practice to push the low-level code of a project towards stateless-ness. The low-level features tend to be moved around a lot. More over, their inputs/outputs tend to be smaller, more atomic , in short good candidates for functionnal programming.

On the other hand, high level code -the central components that makes the main calls- shows little advantage to be moved to a stateless version it self. For example a CFD solver is all about updating the state of the flow along the time. Making it stateless is counter intuitive.

In a nutshell, try to add some stateless-ness in the lower parts of the code, and watch for the gains in readability. If it prove to be too hard, focus more on a potential design problem, than blindly force statefulness.

Can everything be stateless?

No, some situations are stateful by nature. Here are the most common we deal with:

  • writing an input file on a disk. However you can generate the content in memory in a stateless manner, only the final I/O is stateful.
  • interacting with a stateful service, for example a job scheduler on an HPC machine.
  • event driven situations like a Graphical User Interface.

However, remember that most of the littles actions around these situations can be separated in neat little stateless bricks.

Like this post? Share on: TwitterFacebookEmail


Keep Reading


Published

Category

Pitch

Tags

Stay in Touch