8.4.2 F # Decision Tree
As you can see from the last sentence of the specification, a link can either point to a query or point to the final result. In F #, we can write directly using a differential union type with two options. The specification also covers the details of the query, which contains different fields, so it can be represented by the record type of F #.
We will define an F # record type (QueryInfo) that represents information about the query, and a differential union type (decision), which can be either another query or the final result. These data types are referenced to each other. In functional terms, that is, the types are recursively recursive (mutually recursive). Listing 8.14 shows what this piece of F # source code means.
Listing 8.14 describes the decision tree with reciprocal recursive types (F #)
Type queryinfo = [1]
{title:string
Check:client->bool [2]
Positive:D ecision | Referencing the second type
Negative:decision} |
and decision = [3]
| Result of String
| Query of QueryInfo <--references the first type
With F # write type declarations, we can only reference types that have been declared in the file (or the file that was specified in the compilation order, or that are located in the previous file in the Visual Studio solution). Obviously, in this case, it causes the problem, we want to define two references between the types. To solve this problem, F # provides the and keyword. The type declaration in front of the manifest, as usual, uses the type keyword [1], but then uses and [3], indicating that both types are declared at the same time, and each other can see each other.
QueryInfo declares that data and behavior are combined in a single record. The name of the check is simple data members, but the rest of the members are more meaningful. A Check member [2] is a function, that is, a behavior that can return a Boolean value that we will use to select one of the two subsequent branches. These branches are combined values that may either hold strings or contain other queryinfo values recursively, so they can hold data and behavior at the same time. In addition, the return result of the function can be a decision value, but it is not convenient to report whether the check passed, because we only know what the next run checks are. In Listing 8.15, we create a value that represents the decision tree in the 8.3 diagram.
Listing 8.15 checking the customer's decision tree (F #)
Let rec tree = [1]
Query ({Title = "more than$40k"
Check = (fun cl, CL.) Income > 40000)
Positive = moreThan40; negative = lessThan40})
and moreThan40 = [2]
Query ({Title = "has Criminalrecord"
Check = (fun cl, CL.) Criminalrecord)
Positive = Result ("NO"); Negative = Result ("YES")})
and lessThan40 = [3]
Query ({Title = "years Injob")
Check = (fun cl, CL.) Yearsinjob > 1)
Positive = Result ("YES"); negative = Usescredit})
and Usescredit = [4]
Query ({Title = "Uses creditcard")
Check = (fun cl, CL.) Usescreditcard)
Positive = Result ("YES"); Negative = Result ("NO")})
In Listing 8.15, there's a new thing we haven't seen before. When declaring a value, we use the REC keyword together with the new and keyword, which is completely different from the usage of declaring two types in the previous manifest, but with a similar purpose. The AND keyword can declare several values (or functions) that are referenced by each other. For example, in the declaration of tree [1], we can use the value moreThan40 [2], although it is declared after the code.
In this example, the declaration order is the main reason for using let Rec, because it is possible to start the root node of the tree [1], then, on the second level [2][3], create values for the two possible options, and finally, at the third level [4], declare additional issues for one situation. We used the Let rec to declare a recursive function so that the function itself could be called in the body of the function (before the declaration). In general, F # can also declare recursive values, simplifying many common tasks.
Class with a let binding that is recursive
We've seen a few examples of recursive functions, but what about recursive values? The code that uses Windows Forms to create the user interface might be an example; with a simplified API, it might look like this:
Let rec form = CreateForm "MainForm" [BTN]
and btn = Createbutton "Close" (Fun (), form. Close ())
The first line creates the form and puts the list of controls on the form as its last argument, and the list contains only one button, declared in the second row. The last parameter of the Createbutton function is a lambda function that is called when the user clicks the button, closes the application, and therefore needs to refer to the form value, which is declared in the first line.
What's so hard about that? In C #, we can easily write code to do the same thing, rather than think of it as a special recursion. In C #, we add an event handler for the button after the form is created, or you add a button after the form is created. Either way, we change the (mutating) object. By changing (mutate), it is easy to make mutual references between the two values, but when you want the values to be immutable, the problem comes.
With recursive let bindings, we can create values that reference other values, and the entire sequence is declared together. Of course, recursion also has its limitations, such as the following code snippet:
Let rec num1 = num2 + 1
and num2 = NUM1 + 1
Here, we must calculate the NUM1 in order to get the value num2, but to do so we need to num1 the value. The correct difference between the first example is that the form value is only used inside the lambda function, so it is not needed immediately. Fortunately, the F # compiler can detect code that cannot be run and generate compilation errors.
We've shown how to declare records that behave in conjunction with data, and how to use lambda functions to create values of this type of record. In Listing 8.16, we will complete this example to implement a function that examines the client using the decision tree.
Listing 8.16 Recursive processing decision tree (F # Interactive)
> Let rec testclienttree (client, tree) = [1]
Match Treewith
| Result (Message) –> [2]
PRINTFN "Offer A LOAN:%s" message
| Query (Qinfo) –> [3]
Letresult, Case =
if (qinfo. Check (client) then "Yes", qinfo. Positive <--According to the results of the examination
Else "No", qinfo. Negative
Printfn "-%s? %s "Qinfo. Title result
Testclienttree (client, case) <--recursive processing subtree
;;
Val testclienttree:client * decision–> Unit
> Testclienttree (John, tree);; <--Interactive Test Code
-More than $40k? No
-Years in job? No
-Uses credit card? Yes
Offer A Loan:yes
Val it:unit = ()
The program uses recursive functions to implement [1]. The decision tree may be either the final result [2] or another query [3]. In the first case, the result is output; In the second case, run the check first, and select one of the two possible subtrees to be processed later, depending on the result. Then, report the progress to the console and recursively call itself to process the subtree. In Listing 8.16, we also immediately test the code to see which of our sample clients conforms to the decision tree which path algorithm.
In this section, we developed a decision tree for pure functions with F #. As we've seen before, rewriting functional structures in C # (especially differential unions) can be difficult, so in the next section, we'll use C # 3.0, in conjunction with object-oriented techniques and function styles, to implement similar solutions.
8.4.2 F # Decision Tree