📜 ⬆️ ⬇️

How to design and write a full program

"Instructions for creating a functional application", part 1.

“It seems to me that I understand functional programming at a basic level, and I even wrote simple programs, but how can I create a full-fledged application, with real data, error handling, and so on?”

This is a very common question, so I decided that in this series of articles I will describe the instruction covering design, validation, error handling, persistence, dependency management, code organization, and so on.

First, a few comments and cautions:

Overview


An overview of what I plan to describe in this series of articles:

Let's get started


Let's take a very simple example, namely, updating some customer information through a web service.
')
And so, our basic requirements are:

This is a common data processing script. Here there is a specific request that runs the script, after which the data from the request "flow" through the system, being processed at each step. I use this script as an example because it is distributed in corporate software.

Here is a diagram of the process components:


But this description is only a successful version of events. Reality is never so simple! What happens if the user ID is not found in the database, or the mailing address is incorrect, or is there an error in the database?

Let's change the diagram and mark everything that could go wrong.


As you can see, at each step of the script errors may occur for various reasons. One of the goals of the series of these articles is to explain how to manage errors elegantly.

Functional thinking


Now that we have understood the stages of our scenario, how to implement it with the help of the functional approach?

First we turn to the differences between the original scenario and functional thinking.

In a script, we usually mean a request-response model. The request is sent, the answer comes back. If something went wrong, then the flow of actions is completed and the answer comes "ahead of time" (note of the translator: It is solely about the process, not about the time spent.).

What I mean, you can see in the diagram of a simplified version of the script.


But in the functional model, the function is a black box with input and output, like this:


How can we adapt our script to such a model?

Unidirectional flow


First, you need to realize that the functional data flow is only forward. You cannot return "ahead of time".

In our case, this means that all errors must be transmitted before the end of the script along an alternative path.


As soon as we do this, we will have the opportunity to turn the entire stream into a single function — the black box:


Of course, if you look inside this large function, you will find that it is made from (“is composition” in terms of functional methodology) smaller functions, one for each stage of the scenario, connected in series.


Error management


The last diagram shows one successful exit and three exits for errors. This is a problem, since functions can only have one output, not four!

What can we do about it?

The answer is to use the Merge type, where each option represents one of the possible exits. Then the function will actually have only one way out.

Here is an example of a possible type definition for outputting a result:
type UseCaseResult =    | Success    | ValidationError    | UpdateError    | SmtpError 

And here is a reworked chart showing a single exit with four different options included in it:


Simplify Error Management


This solves the problem, but the presence of an error for each step is a fragile and poorly reusable design. Can we do better?

Yes! We really only need two methods. One for a successful case and another for all erroneous:
 type UseCaseResult =   | Success   | Failure 



This type is very versatile and will work with any process! Actually, you will soon see that to work with this type we can make a good library of useful functions, which is suitable for any scripts.

One more thing - as a result, which the function returns, there is no data at all, only the status of success / failure. We need to correct something so that the result of the function contains the actual successful or failed object. We will declare successful and failed types as generic (using type parameters).

Finally, our final, universal version:
 type Result<'TSuccess,'TFailure> =   | Success of 'TSuccess   | Failure of 'TFailure 

In fact, in the F # library there is already a similar type. It is called Choice . For clarity, I will continue to use the Result type created earlier in this and subsequent articles. We will return to this issue when we come to more serious problems.

Now, once again looking at the scenario with separate steps, we will see that we must combine the errors of each step into a single “failed” path.


How to do this is the topic of the next article.

Summary and guidelines


So, we have the following provisions to the instructions:

Methodical instructions

Source: https://habr.com/ru/post/263035/


All Articles