Defensive programming and validations

· Allanderek's blog

#elm #programming

Defensive programming is generally defined using terms such as 'impossible', one definition on the c2 wiki has Defend against the impossible because the impossible will happen. In addition there is also the idea to do the right thing. Such a definition is obviously (and intentionally) vague. The question is, what is the right thing? That's always situation specific and it can be a difficult part in programming. That's what I find interesting here, the programmer can easily make happen whatever we want, but it's difficult to decide what we should want. That's difficult to follow so let's try an example.

We have an application with string inputs. Those string inputs can be validated against patterns. Because the validation happens both on the front-end (web) client (written in Elm) and the backend server code, we need a way to pass the validation patterns between the two. Now ideally we would do some code generation here so that at compile time we can check that we definitely have the validation patterns for all the string inputs that we use on the front-end. But let's assume that we cannot do that for some reason. Instead, the backend passes (in program flags) a dictionary mapping a string-input-key to a validation pattern. So when you output a string input, you might do something like:

 1let
 2    matchesWholeString s regex =
 3        ... 
 4
 5    mPattern =
 6        Dict.get "email" model.inputValidationPatterns
 7            |> Maybe.andThen Regex.fromString
 8
 9
10    isValidInput =
11        case mPattern of
12            Nothing ->
13                ... WHAT NOW ..
14            Just pattern ->
15                matchesWholeString model.emailInput pattern
16
17    patternError =
18        case isValidInput of
19            True ->
20                Html.text ""
21            False ->
22                Html.span
23                    [ Attributes.class "error" ]
24                    [ Html.text "Your input is invalid" ]
25
26    submitButton =
27        case isValidInput of
28            False ->
29                Html.text ""
30            True ->
31                Html.button
32                    [ Msg.SubmitEmail |> Events.onClick ]
33                    [ Html.text "Submit" ]
34
35in
36Html.div
37    []
38    [ Html.input
39        [ Attributes.value model.emailInput ]
40        []
41    , patternError
42    , submitButton
43    ]

So great Elm is forcing us to defensively program and deal with the 'impossible' situation in which the key for the input type is not in the set of validation patterns provided by the backend. The question is, what should we do? What should go in the WHAT NOW part?

We have basically two options, we can either throw up our hands and say, okay in that case no input is valid. Or we could throw up our hands and say okay all inputs are valid (both options involve throwing up our hands).

# Option One

1
2    isValidInput =
3        case mPattern of
4            Nothing ->
5                False
6

# Option Two

1
2    isValidInput =
3        case mPattern of
4            Nothing ->
5                True
6

# Conflicting desires

It is useful to take a step back and decide what is desirable behaviour. In this case we would want:

  1. The user to be able to submit valid input
  2. The user not be able to submit invalid input
  3. To be notified of bugs quickly so that we can fix them.

Using option one, the user will not be able to subit valid input, but we will surely be notified of this bug pretty quickly. Using option two, the user will be able to submit invalid input, that might be okay if we have some server side validation anyway, but it means we will not be notified about this bug soon.

# Half-way house

We can combine these two strategies. In the case that there is no validation pattern available, we can output a strange error message, but still allow for the user to submit input. This will satisfy both desires 1 and 3, but not 2:

 1let
 2    matchesWholeString s regex =
 3        ... 
 4
 5    mPattern =
 6        Dict.get "email" model.inputValidationPatterns
 7            |> Maybe.andThen Regex.fromString
 8
 9
10    isValidInput =
11        case mPattern of
12            Nothing ->
13                True -- So the input is seen as valid
14            Just pattern ->
15                matchesWholeString model.emailInput pattern
16
17    patternError =
18        case isValidInput of
19            True ->
20                Html.text ""
21            False ->
22                Html.span
23                    [ Attributes.class "error" ]
24                    [ Html.text "Your input is invalid" ]
25
26    systemError =
27        case mPattern of
28            Nothing ->
29                Html.span
30                    [ Attributes.class "system-error" ]
31                    [ Html.text "There is an internal error relating ..." ]
32
33    submitButton =
34        case isValidInput of
35            False ->
36                Html.text ""
37            True ->
38                Html.button
39                    [ Msg.SubmitEmail |> Events.onClick ]
40                    [ Html.text "Submit" ]
41
42in
43Html.div
44    []
45    [ Html.input
46        [ Attributes.value model.emailInput ]
47        []
48    , patternError
49    , submitButton
50    , systemError
51    ]

Obviously it depends on the particular situation, but by writing down the desires we can arrive at the least bad solution. In this case we were more interested in allowing valid input, rather than in disallowing invalid input, other situations may of course be different. The point here is that when accounting for impossible situations the 'correct' thing to do is not always obvious. Often being alerted to the problem is a strongly desired attribute.