Created
September 18, 2020 21:20
-
-
Save jackfoxy/10185bbc33e9b388d8bbb0d7ca73832a to your computer and use it in GitHub Desktop.
A demo of decision tables using F# types and pattern matching
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(* | |
Decision Table Demo | |
MIT License, do with this what you will | |
A demo of decision tables using F# types and pattern matching, inspired by https://www.hillelwayne.com/post/decision-table-patterns/ | |
The compiler service flags problematic (incomplete) decision tables. | |
Requirements: | |
Some F# development environment using F# Compiler Services, https://fsharp.github.io/FSharp.Compiler.Service/, in order to generate intellisense help regarding the completeness of pattern matches. | |
Visual Studio Community Edition, https://visualstudio.microsoft.com/vs/community/ | |
Rider, https://www.jetbrains.com/rider/ | |
VS Code with Ionide plugin, http://ionide.io/ | |
VIM, https://github.com/ionide/Ionide-vim | |
Developed prior to F# 5.0 ... shouldn't make much difference. | |
*) | |
module Domain = | |
type Action = | |
| Merge | |
| FixTests | |
| FixReviewComments | |
| FixTestsFirst | |
type PR = | |
{ | |
PassedReview : bool | |
PassedTests : bool | |
} | |
type InfiniteState = int | |
type DoSomething = | |
| DoThis | |
| DoThat | |
open Domain | |
let decisionTable1 (pr : PR) = | |
(* | |
A complete decision table, no warnings. | |
*) | |
match pr with | |
| { PassedReview = true; PassedTests = true } -> Action.Merge | |
| { PassedReview = true; PassedTests = false } -> Action.FixTests | |
| { PassedReview = false; PassedTests = true } -> Action.FixReviewComments | |
| { PassedReview = false; PassedTests = false } -> Action.FixTestsFirst | |
let decisionTable1Alternate1 passedReview passedTests = | |
(* | |
...or if you prefer your patterns less verbose | |
*) | |
match passedReview, passedTests with | |
| true, true -> Action.Merge | |
| true, false -> Action.FixTests | |
| false, true -> Action.FixReviewComments | |
| false, false -> Action.FixTestsFirst | |
let decisionTable1Alternate2 passedReview passedTests = | |
(* | |
Multiple patterns result in same decision. | |
*) | |
match passedReview, passedTests with | |
| true, true -> Action.Merge | |
| false, true -> Action.FixReviewComments | |
| true, false | |
| false, false -> Action.FixTests | |
let decisionTable1Warning1 (pr : PR) = | |
(* | |
Note green squiggly under the pr in match statement indicates a warning. | |
Mouse-over to read message | |
FS00025: Incomplete pattern matches on this expression. For example, the value | |
'{PassedReview=_;PassedTests=false}' may indicate a case not covered by the pattern(s). | |
Comment: notice the generated example is not quite correct in that 'PassedReview' is assigned the wild card. | |
*) | |
match pr with | |
| { PassedReview = true; PassedTests = true } -> Action.Merge | |
| { PassedReview = true; PassedTests = false } -> Action.FixTests | |
| { PassedReview = false; PassedTests = true } -> Action.FixReviewComments | |
let decisionTable1Warning2 (pr : PR) = | |
(* | |
This time the green squiggly is under our last pattern. | |
FS00026: This rule will never be matched. | |
We included the same pattern a second time and the compiler warns us. | |
*) | |
match pr with | |
| { PassedReview = true; PassedTests = true } -> Action.Merge | |
| { PassedReview = true; PassedTests = false } -> Action.FixTests | |
| { PassedReview = false; PassedTests = true } -> Action.FixReviewComments | |
| { PassedReview = true; PassedTests = false } -> Action.FixTestsFirst | |
let decisionTable2 (state : InfiniteState) = | |
(* | |
When using guards in the pattern, the compiler is not smart enough. | |
FS00025: Incomplete pattern matches on this expression. | |
*) | |
match state with | |
| s when s % 2 = 0 -> DoSomething.DoThis | |
| s when s % 2 <> 0 -> DoSomething.DoThat | |
(* | |
We get around this limitation using F# Active Patterns, https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/active-patterns | |
We could just as easily define up to 7 ranges. | |
*) | |
let (|Even|Odd|) n = | |
if n % 2 = 0 then | |
Even | |
else | |
Odd | |
let decisionTable2Better (state : InfiniteState) = | |
(* | |
No warnings. | |
*) | |
match state with | |
| Even -> DoSomething.DoThis | |
| Odd -> DoSomething.DoThat |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment