Time flow and dynamic change are now clear concepts of FRP. In the previous chapter, we discussed over and over again what they are. They represent one of the main axes of FRP together with discrete and continuous semantics.
Concerning reactive systems (hybrid systems), we immediately understood how much important the concepts of execution time and used memory are. These two features may have some problems that could make the system nonreactive. The title of this section suggests a solution for them both. The following are the methods which can be used:
F# and any other language based on functional paradigm exposes and uses functions and architecture that already support these two features This is one of main reasons why languages that allow a functional approach are usually used for reactive scenarios
In FRP and also in real time scenarios, it is very likely that data flow and its manipulation happens in an asynchronous way.
Anyway, we know that in an asynchronous context it is not possible to establish with certainty the execution time of any operation. We can analyze the following code taken from MSDN ( https://msdn.microsoft.com/en-us/library/ee370262.aspx ):
let bufferData (number:int) = [| for count in 1 .. 10 -> byte (count % 256) |] |> Array.permute (fun index -> index) let writeFile fileName bufferData = async { use outputFile = System.IO.File.Create(fileName) do! outputFile.AsyncWrite(bufferData) } Seq.init 10 (fun num -> bufferData num) |> Seq.mapi (fun num value -> writeFile ("file" + num.ToString() + ".dat") value) |> Async.Parallel |> Async.RunSynchronously |> ignore
The function Array.permute
performs the mapping from the input index to the output index.
The keyword do!
and the asynchronous counterpart of the keyword do
is the equivalent of the instruction let () = expr.
, that is, the execution of an expression that returns a value unit type.
If we try to perform the function in Interactive Console
of F#, in the Windows Temp
folder, the following code will generate 10
files with 1000
chars in each one.
Anyway, if instead of 10
files, there are thousands or even worse infinite files, then it would not be possible to determine the time flow. In this case, it would be pointless to add a timeout to control the flow. Take a look at the following code:
let bufferData (number:int) = [| for i in 1 .. 1000 -> byte (i % 256) |] |> Array.permute (fun index -> index) let counter = ref 0 let writeFileInner (stream:System.IO.Stream) data = let result = stream.AsyncWrite(data) lock counter (fun () -> counter := !counter + 1) result let writeFile fileName bufferData = async { use outputFile = System.IO.File.Create(fileName) do! writeFileInner outputFile bufferData } let async1 = Seq.init 1000 (fun num -> bufferData num) |> Seq.mapi (fun num value -> writeFile ("file_timeout" + num.ToString() + ".dat") value) |> Async.Parallel try Async.RunSynchronously(async1, 200) |> ignore with | exc -> printfn "%s" exc.Message printfn "%d write operations completed successfully." !counter
Through the second parameter of the method Async.RunSynchronously(async1, 200)
, we can set up the maximum execution time value in order to be able to control the time flow.
In this specific case, it will stop the flow that generates files when the countdown hits zero. In fact, if we look into the Temp
folder or simply read the output message in Interactive Console
, we can find a number of generated N files that can change slightly at every execution, according to CPU work.
When we presented F# and FRP in particular, we introduced the following set of essential concepts:
Now, if we try to put together these features, we can obtain an evolution of FRP, or better, a chance to organize our code and logical architecture through a flow of choices.
To be able to handle a set of choices, such as a set of objects or functions, first of all we should have a common denominator which represents them, otherwise it won't be possible. In informatics term, we could say switch from a concrete implementation to an abstract one.
In particular, functional programming has a strict connection with mathematics, so we should create Computation Expressions. These expressions are inspired by the Monads of the functional language Haskell, which in turn are inspired by the Monads concept in mathematics.
Computation Expressions are merely expressions that execute a function given an input and return a result (output). Otherwise, in terms more similar to programming, they are interfaces with rules for the execution of their own methods. For example, this allows us to connect more Monads and handle a workflow.
In F#, one of the simplest representations of Computation Expression during the flow of choices is the following:
type Result<'TSuccess,'TFailure> = | Success of 'TSuccess | Failure of 'TFailure
This code represents a general discriminate union that has as a possible result Success
or Failure
of the type 'TSuccess
or 'Tfailure
, respectively.
The next code shows how we can take full advantage of Monads, connecting more functions that return always the same output:
type Result<'TSuccess,'TFailure> = | Success of 'TSuccess | Failure of 'TFailure let bind inputFunc = function | Success s -> inputFunc s | Failure f -> Failure f type Account = { UserName : string; IsLogged : bool; Email : string } let validateAccount account = match account with | account when account.UserName = "" -> Failure "UserName is not valid" | account when account.Email = "" -> Failure " Email is not empty" | _ -> Success account let checkLogin account = if(account.IsLogged) then Success account else Failure "User is not logged" let LogIn account = if(account.IsLogged) then Failure "User has already Logged" else Success {account with IsLogged = true} let LogOut account = if(account.IsLogged) then Success {account with IsLogged = false} else Failure "User has already Logged" let ProcessNewAccount = let checkLogin = bind checkLogin let login = bind LogIn validateAccount >> login >> checkLogin let NewFakeAccount = { UserName = ""; Email = ""; IsLogged = false } let AccountLogged = { UserName = "User"; Email = "[email protected]"; IsLogged = true } let NewAccount = { UserName = "User1"; Email = " [email protected] "; IsLogged = false } ProcessNewAccount NewFakeAccount |> printfn "Result = %A" ProcessNewAccount AccountLogged |> printfn "Result = %A" ProcessNewAccount NewAccount |> printfn "Result = %A"
As you can see, we created different Monads: validateAccount
, checkLogin
, and LogIn
, which return a type Result<Account,string>
as a result. Later, we created a function called ProcessNewAccount
that, through the use of the composition operator, (>>
) connects each Monads in a definite flow. It is important to note how, for every function, it is necessary to use the method bind
to avoid cast and anonymous type errors.
In the last row, three different accounts are defined, which are processed with the function ProcessNewAccount
.
The result obtained through Interactive Console
is as follows:
Result = Failure "UserName is not valid" Result = Failure "User has already Logged" Result = Success {UserName = "User1"; IsLogged = true; Email = " [email protected] ";}
18.118.24.106