├── .gitattributes ├── .gitignore ├── README.md ├── prog-fsharp-ddd.sln ├── slides ├── 1-ProgFsharp-Introduction.pptx ├── 2-ProgFsharp-HelloWorld.pptx ├── 3-ProgFsharp-DDD.pptx └── fsharp-basic-syntax.pdf └── src └── DomainModelingInFSharp ├── Basic syntax you need to know.fsx ├── DDD Exercise 1 - CardGame (answers).fsx ├── DDD Exercise 1 - CardGame.fsx ├── DDD Exercise 2 - Contact (answers).fsx ├── DDD Exercise 2 - Contact.fsx ├── DDD Exercise 3 - Payments (answers).fsx ├── DDD Exercise 3 - Payments.fsx ├── DDD Exercise 4 - Refactoring flags (answers).fsx ├── DDD Exercise 4 - Refactoring flags.fsx ├── DDD_Projects.fsx ├── DSL Exercise 1 - Time (answers).fsx ├── DSL Exercise 1 - Time.fsx ├── DSL Exercise 2 - Turtle Graphics (answers).fsx ├── DSL Exercise 2 - Turtle Graphics.fsx ├── DSL Exercise 3 - Recipe (answers).fsx ├── DSL Exercise 3 - Recipe.fsx ├── DomainModelingInFSharp.fsproj ├── FSM Exercise 1 - Verified Email (answers).fsx ├── FSM Exercise 1 - Verified Email transition diagram.png ├── FSM Exercise 1 - Verified Email.fsx ├── FSM Exercise 2 - Shipments (answers).fsx ├── FSM Exercise 2 - Shipments transition diagram.png ├── FSM Exercise 2 - Shipments.fsx ├── FSM Exercise 3 - Shopping cart (answers).fsx ├── FSM Exercise 3 - Shopping cart transition diagram.png ├── FSM Exercise 3 - Shopping cart.fsx ├── FSM Exercises - State machine diagram.png ├── FSM Exercises - Template to work from.fsx ├── Final Exercise - Model your own domain.fsx ├── HelloWorld.fsx ├── OpaqueApiClient.fs ├── OpaqueApiExample.fs ├── OpaqueApiExample.fsi ├── Rop.fsx ├── Syntax-help-Basic.fsx ├── Syntax-help-Functions.fsx ├── Syntax-help-Generics.fsx ├── Syntax-help-Lists.fsx ├── Syntax-help-Options-and-Choices.fsx ├── Syntax-help-Records.fsx ├── Syntax-help-Tuples.fsx ├── Syntax-help-Unit-type.fsx ├── Validation - Domain input and output.png ├── Validation 1 - Roman Numerals.fsx └── Validation 2 - Contact.fsx /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | bld/ 16 | [Bb]in/ 17 | [Oo]bj/ 18 | 19 | # Roslyn cache directories 20 | *.ide/ 21 | 22 | # MSTest test Results 23 | [Tt]est[Rr]esult*/ 24 | [Bb]uild[Ll]og.* 25 | 26 | #NUNIT 27 | *.VisualState.xml 28 | TestResult.xml 29 | 30 | # Build Results of an ATL Project 31 | [Dd]ebugPS/ 32 | [Rr]eleasePS/ 33 | dlldata.c 34 | 35 | *_i.c 36 | *_p.c 37 | *_i.h 38 | *.ilk 39 | *.meta 40 | *.obj 41 | *.pch 42 | *.pdb 43 | *.pgc 44 | *.pgd 45 | *.rsp 46 | *.sbr 47 | *.tlb 48 | *.tli 49 | *.tlh 50 | *.tmp 51 | *.tmp_proj 52 | *.log 53 | *.vspscc 54 | *.vssscc 55 | .builds 56 | *.pidb 57 | *.svclog 58 | *.scc 59 | 60 | # Chutzpah Test files 61 | _Chutzpah* 62 | 63 | # Visual C++ cache files 64 | ipch/ 65 | *.aps 66 | *.ncb 67 | *.opensdf 68 | *.sdf 69 | *.cachefile 70 | 71 | # Visual Studio profiler 72 | *.psess 73 | *.vsp 74 | *.vspx 75 | 76 | # TFS 2012 Local Workspace 77 | $tf/ 78 | 79 | # Guidance Automation Toolkit 80 | *.gpState 81 | 82 | # ReSharper is a .NET coding add-in 83 | _ReSharper*/ 84 | *.[Rr]e[Ss]harper 85 | *.DotSettings.user 86 | 87 | # JustCode is a .NET coding addin-in 88 | .JustCode 89 | 90 | # TeamCity is a build add-in 91 | _TeamCity* 92 | 93 | # DotCover is a Code Coverage Tool 94 | *.dotCover 95 | 96 | # NCrunch 97 | _NCrunch_* 98 | .*crunch*.local.xml 99 | 100 | # MightyMoose 101 | *.mm.* 102 | AutoTest.Net/ 103 | 104 | # Web workbench (sass) 105 | .sass-cache/ 106 | 107 | # Installshield output folder 108 | [Ee]xpress/ 109 | 110 | # DocProject is a documentation generator add-in 111 | DocProject/buildhelp/ 112 | DocProject/Help/*.HxT 113 | DocProject/Help/*.HxC 114 | DocProject/Help/*.hhc 115 | DocProject/Help/*.hhk 116 | DocProject/Help/*.hhp 117 | DocProject/Help/Html2 118 | DocProject/Help/html 119 | 120 | # Click-Once directory 121 | publish/ 122 | 123 | # Publish Web Output 124 | *.[Pp]ublish.xml 125 | *.azurePubxml 126 | ## TODO: Comment the next line if you want to checkin your 127 | ## web deploy settings but do note that will include unencrypted 128 | ## passwords 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | packages/* 133 | ## TODO: If the tool you use requires repositories.config 134 | ## uncomment the next line 135 | #!packages/repositories.config 136 | 137 | # Enable "build/" folder in the NuGet Packages folder since 138 | # NuGet packages use it for MSBuild targets. 139 | # This line needs to be after the ignore of the build folder 140 | # (and the packages folder if the line above has been uncommented) 141 | !packages/build/ 142 | 143 | # Windows Azure Build Output 144 | csx/ 145 | *.build.csdef 146 | 147 | # Windows Store app package directory 148 | AppPackages/ 149 | 150 | # Others 151 | sql/ 152 | *.Cache 153 | ClientBin/ 154 | [Ss]tyle[Cc]op.* 155 | ~$* 156 | *~ 157 | *.dbmdl 158 | *.dbproj.schemaview 159 | *.pfx 160 | *.publishsettings 161 | node_modules/ 162 | 163 | # RIA/Silverlight projects 164 | Generated_Code/ 165 | 166 | # Backup & report files from converting an old project file 167 | # to a newer Visual Studio version. Backup files are not needed, 168 | # because we have git ;-) 169 | _UpgradeReport_Files/ 170 | Backup*/ 171 | UpgradeLog*.XML 172 | UpgradeLog*.htm 173 | 174 | # SQL Server files 175 | *.mdf 176 | *.ldf 177 | 178 | # Business Intelligence projects 179 | *.rdl.data 180 | *.bim.layout 181 | *.bim_*.settings 182 | 183 | # Microsoft Fakes 184 | FakesAssemblies/ 185 | /src/FsXsd/_junk 186 | Thumbs.db 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Domain Modelling in F# 2 | 3 | This is the training material for a hands-on 3 hour "Domain Modelling in F#" session. 4 | 5 | The topics covered are: 6 | * Why use F# for domain modelling? 7 | * Understanding the F# type system (algebraic types) 8 | * Using the F# type system : Options 9 | * Using the F# type system : Single case unions 10 | * Making illegal states unrepresentable 11 | * Validation 12 | * Domain specific languages 13 | * State machines 14 | -------------------------------------------------------------------------------- /prog-fsharp-ddd.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.30110.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4CFE0276-31B1-4D05-B8DA-ADAC6EC6ED6A}" 7 | ProjectSection(SolutionItems) = preProject 8 | README.md = README.md 9 | EndProjectSection 10 | EndProject 11 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "DomainModelingInFSharp", "src\DomainModelingInFSharp\DomainModelingInFSharp.fsproj", "{41A07C34-53D6-4B60-B65D-C0D11941EC02}" 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {41A07C34-53D6-4B60-B65D-C0D11941EC02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {41A07C34-53D6-4B60-B65D-C0D11941EC02}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {41A07C34-53D6-4B60-B65D-C0D11941EC02}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {41A07C34-53D6-4B60-B65D-C0D11941EC02}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | EndGlobal 28 | -------------------------------------------------------------------------------- /slides/1-ProgFsharp-Introduction.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swlaschin/DomainModellingInFsharp/ed21291dd1329972160dee814084a02c2f0f9e48/slides/1-ProgFsharp-Introduction.pptx -------------------------------------------------------------------------------- /slides/2-ProgFsharp-HelloWorld.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swlaschin/DomainModellingInFsharp/ed21291dd1329972160dee814084a02c2f0f9e48/slides/2-ProgFsharp-HelloWorld.pptx -------------------------------------------------------------------------------- /slides/3-ProgFsharp-DDD.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swlaschin/DomainModellingInFsharp/ed21291dd1329972160dee814084a02c2f0f9e48/slides/3-ProgFsharp-DDD.pptx -------------------------------------------------------------------------------- /slides/fsharp-basic-syntax.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swlaschin/DomainModellingInFsharp/ed21291dd1329972160dee814084a02c2f0f9e48/slides/fsharp-basic-syntax.pdf -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/Basic syntax you need to know.fsx: -------------------------------------------------------------------------------- 1 | // ============================== 2 | // General 3 | // ============================== 4 | 5 | (* 6 | 7 | 1) Curly braces are NOT used to delimit blocks of code. Instead, indentation is used (like Python). 8 | 2) Whitespace is used to separate parameters rather than commas. 9 | 10 | Other keywords: 11 | * "let" is used instead of "var" 12 | * "let" is also used for defining functions 13 | * "type" is used instead of "class", "enum", etc 14 | 15 | Symbols 16 | * "=" is used instead of "==" 17 | * "<>" is used instead of "!=" 18 | * "not" is used instead of "!" 19 | * In parameters, commas are replaced by whitespace 20 | * In non-parameter usage (eg lists), commas are replaced by semicolons in most places. 21 | 22 | *) 23 | 24 | // ============================== 25 | // Functions 26 | // ============================== 27 | 28 | // define a function 29 | let printName myName = 30 | printfn "my name is %s" myName 31 | 32 | // call the function 33 | printName "Scott" 34 | 35 | // define a function 36 | let add x y = 37 | x + y // no return needed 38 | 39 | 40 | // ============================== 41 | // Piping 42 | // ============================== 43 | 44 | // "|>" is the pipe symbol 45 | 46 | // piping passes the left side to the LAST parameter 47 | "Scott" |> printName 48 | 49 | // piping passes the left side to the LAST parameter 50 | 2 |> add 1 51 | 52 | // piping can be used to connect a sequence of actions 53 | add 1 2 54 | |> add 3 55 | |> printfn "1 + 2 + 3 = %i" 56 | 57 | 58 | // ============================== 59 | // Type annotations 60 | // ============================== 61 | 62 | // How to add type annotations to a function: 63 | // (Not normally needed but might be helpful when you are a beginner) 64 | let addInt (x:int) (y:int) :int = // last one is the type of the return value 65 | x + y 66 | 67 | let addString (x:string) (y:string) :string = 68 | x + y 69 | 70 | let intToString (x:int) :string = 71 | x.ToString() 72 | 73 | // printing 74 | printfn "an int: %i | a string: %s | a float: %g | a bool: %b | an F# type: %A" 1 "hello" 3.14 true [1;2;3] 75 | 76 | // ============================== 77 | // Pattern matching 78 | // ============================== 79 | 80 | // pattern matching 81 | let matchInt i = 82 | match i with 83 | | 1 -> printfn "One" 84 | | 2 -> printfn "Two" 85 | | _ -> printfn "other" // "_" is a wildcard 86 | 87 | // More on pattern matching when we talk about union types 88 | 89 | // ============================== 90 | // Making sense of the compiler output 91 | // ============================== 92 | 93 | // ------------- 94 | // Function signatures 95 | // ------------- 96 | 97 | // function signatures are like this 98 | // paramType -> returnType // one parameter function 99 | // paramType -> paramType -> returnType // two parameter function 100 | 101 | // try it 102 | let add1 x = x + 1 // val add1 : x:int -> int 103 | let plus x y = x + y // val plus : x:int -> y:int -> int 104 | 105 | // ------------- 106 | // The unit type 107 | // ------------- 108 | // The "unit" type is like void, sort of. It is written "()" and means no output or no input 109 | // E.g. print signatures are like this 110 | 111 | // paramType -> unit // a one parameter function returning nothing 112 | 113 | // try it 114 | printfn "hello %s" // string -> unit 115 | 116 | // ------------- 117 | // Generic types 118 | // ------------- 119 | // Generic types are written 'a, 'b etc. 120 | // Equivalent to in C# 121 | 122 | // try it 123 | let same x = x // val same : x:'a -> 'a 124 | 125 | 126 | // ============================== 127 | // Common types 128 | // ============================== 129 | 130 | // ------------- 131 | // Tuples (pairs,triples) 132 | // ------------- 133 | 134 | let myPair = 1,2 // pair 135 | let myTriple = 1,2,3 // triple 136 | 137 | // How many parameters does this function have? 138 | let tupleExample1 (x,y,z) = x + y + z 139 | // How many parameters does this function have? 140 | let tupleExample2 x y z = x + y + z 141 | 142 | // Can I call the functions like this? 143 | (* 144 | tupleExample1 1 2 3 145 | tupleExample2 (1,2,3) 146 | *) 147 | 148 | 149 | // ------------- 150 | // Records 151 | // ------------- 152 | 153 | // { is used for defining record types and constructing record values 154 | type MyRecordType = {a:int; b:string} 155 | let myRecordValue = {a=1; b="hello"} 156 | 157 | // you can copy all the fields but some like this 158 | let cloneMyRecordValue = {myRecordValue with b="goodbye"} 159 | 160 | 161 | // ------------- 162 | // Choices 163 | // ------------- 164 | 165 | type MyChoices = Choice1 | Choice2 166 | let myChoice1 = Choice1 167 | let myChoice2 = Choice2 168 | 169 | 170 | type MyChoiceWithData = 171 | | Choice0WithNoData 172 | | Choice1WithIntData of int 173 | | Choice2WithStringData of string 174 | 175 | // To create one of the choices, use the case pattern as a constructor 176 | let myChoice0WithNonData = Choice0WithNoData 177 | let myChoice1WithData = Choice1WithIntData 42 178 | let myChoice2WithData = Choice2WithStringData "hello" 179 | 180 | // Pattern matching for choices -- 181 | // to extract one of the choices, use the case pattern as a "deconstructor" 182 | match myChoice1WithData with 183 | | Choice0WithNoData -> 184 | printfn "no extra data" 185 | | Choice1WithIntData anInt -> 186 | printfn "an int %i" anInt 187 | | Choice2WithStringData aString -> 188 | printfn "a string %s" aString 189 | 190 | // ------------- 191 | // Lists 192 | // ------------- 193 | 194 | let myList = [1;2;3] // square brackets 195 | let myList2 = 0 :: myList // prepend with "::" 196 | 197 | // NOTE: needs "rec" keyword for recursion 198 | let rec loopThroughList aList = 199 | 200 | match aList with 201 | | [] -> // match empty list 202 | printfn "List is empty. Stopping." 203 | 204 | | first::rest -> // match first element and rest of list 205 | printfn "processing element %i" first 206 | loopThroughList rest 207 | 208 | loopThroughList myList2 209 | 210 | // helpful methods in the "List" module. 211 | myList |> List.rev 212 | 213 | // "map" loop with one parameter lambda that returns a new value 214 | myList |> List.map (fun x -> x + 1) 215 | 216 | // e.g. collect uppercase versions 217 | ["Alice"; "Bob"; "Carol"] |> List.map (fun s -> s.ToUpper()) 218 | 219 | // "iter" loop with one parameter lambda that returns unit (such as printfn) 220 | myList |> List.iter (fun x -> printfn "x=%i" x) 221 | 222 | // e.g. given a print function with ONE parameter that returns unit 223 | let printHello = printfn "Hello %s" // string -> unit 224 | 225 | // you can use List.iter like this: 226 | ["Alice"; "Bob"; "Carol"] |> List.iter printHello 227 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DDD Exercise 1 - CardGame (answers).fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // DDD Exercise: Model a card game 3 | // 4 | // ================================================ 5 | 6 | (* 7 | A card is a combination of a Suit (Heart, Spade) and a Rank (Two, Three, ... King, Ace) 8 | 9 | A hand is a list of cards 10 | 11 | A deck is a list of cards 12 | 13 | A player has a name and a hand 14 | 15 | A game consists of a deck and list of players 16 | 17 | *) 18 | 19 | module CardGame = 20 | 21 | type Suit = Club | Diamond | Spade | Heart 22 | 23 | type Rank = Two | Three | Four | Five | Six | Seven | Eight 24 | | Nine | Ten | Jack | Queen | King | Ace 25 | 26 | type Card = Suit * Rank 27 | 28 | type Hand = Card list 29 | type Deck = Card list 30 | 31 | type Player = {Name : string; Hand : Hand} 32 | type Game = {Deck : Deck; Players : Player list} 33 | 34 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DDD Exercise 1 - CardGame.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // DDD Exercise: Model a card game 3 | // 4 | // ================================================ 5 | 6 | (* 7 | A card is a combination of a Suit (Heart, Spade) and a Rank (Two, Three, ... King, Ace) 8 | 9 | A hand is a list of cards 10 | 11 | A deck is a list of cards 12 | 13 | A player has a name and a hand 14 | 15 | A game consists of a deck and list of players 16 | 17 | *) 18 | 19 | module CardGame = 20 | 21 | type Suit = ?? 22 | type Rank = ?? 23 | type Card = ?? 24 | 25 | type Hand = ?? 26 | type Deck = ?? 27 | 28 | type Player = ?? 29 | type Game = ?? 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DDD Exercise 2 - Contact (answers).fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // DDD Exercise: Model a Contact management system 3 | // 4 | // ================================================ 5 | 6 | (* 7 | REQUIREMENTS 8 | 9 | The Contact management system stores Contacts 10 | 11 | A Contact has 12 | * a personal name 13 | * an optional email address 14 | * an optional postal address 15 | * Rule: a contact must have an email or a postal address 16 | 17 | A Personal Name consists of a first name, middle initial, last name 18 | * Rule: the first name and last name are required 19 | * Rule: the middle initial is optional 20 | * Rule: the first name and last name must not be more than 50 chars 21 | * Rule: the middle initial is exactly 1 char, if present 22 | 23 | A postal address consists of a four address fields plus a country 24 | 25 | Rule: An Email Address can be verified or unverified 26 | 27 | *) 28 | 29 | 30 | 31 | // ---------------------------------------- 32 | // Helper module 33 | // ---------------------------------------- 34 | module StringTypes = 35 | 36 | type String1 = String1 of string 37 | type String50 = String50 of string 38 | 39 | let createString1 (s:string) = 40 | if (s.Length <= 1) 41 | then Some (String50 s) 42 | else None 43 | 44 | let createString50 (s:string) = 45 | if s.Length <= 50 46 | then Some (String50 s) 47 | else None 48 | 49 | 50 | // ---------------------------------------- 51 | // Main domain code 52 | // ---------------------------------------- 53 | 54 | open StringTypes 55 | 56 | type EmailAddress = 57 | EmailAddress of string 58 | 59 | type VerifiedEmail = 60 | VerifiedEmail of EmailAddress 61 | 62 | type EmailContactInfo = 63 | | Unverified of EmailAddress 64 | | Verified of VerifiedEmail 65 | 66 | type PostalContactInfo = { 67 | address1: string 68 | address2: string 69 | address3: string 70 | address4: string 71 | country: string 72 | } 73 | 74 | type ContactInfo = 75 | | EmailOnly of EmailContactInfo 76 | | AddrOnly of PostalContactInfo 77 | | EmailAndAddr of EmailContactInfo * PostalContactInfo 78 | 79 | type PersonalName = { 80 | FirstName: String50 81 | MiddleInitial: String1 option 82 | LastName: String50 } 83 | 84 | type Contact = { 85 | Name: PersonalName 86 | ContactInfo : ContactInfo } 87 | 88 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DDD Exercise 2 - Contact.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // DDD Exercise: Model a Contact management system 3 | // 4 | // ================================================ 5 | 6 | (* 7 | REQUIREMENTS 8 | 9 | The Contact management system stores Contacts 10 | 11 | A Contact has 12 | * a personal name 13 | * an optional email address 14 | * an optional postal address 15 | * Rule: a contact must have an email or a postal address 16 | 17 | A Personal Name consists of a first name, middle initial, last name 18 | * Rule: the first name and last name are required 19 | * Rule: the middle initial is optional 20 | * Rule: the first name and last name must not be more than 50 chars 21 | * Rule: the middle initial is exactly 1 char, if present 22 | 23 | A postal address consists of a four address fields plus a country 24 | 25 | Rule: An Email Address can be verified or unverified 26 | 27 | *) 28 | 29 | 30 | 31 | // ---------------------------------------- 32 | // Helper module 33 | // ---------------------------------------- 34 | module StringTypes = 35 | 36 | type String1 = String1 of string 37 | type String50 = String50 of string 38 | 39 | let createString1 (s:string) = 40 | if (s.Length <= 1) 41 | then Some (String50 s) 42 | else None 43 | 44 | let createString50 (s:string) = 45 | if s.Length <= 50 46 | then Some (String50 s) 47 | else None 48 | 49 | 50 | // ---------------------------------------- 51 | // Main domain code 52 | // ---------------------------------------- 53 | 54 | open StringTypes 55 | 56 | type Contact = ?? // you take it from here! 57 | 58 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DDD Exercise 3 - Payments (answers).fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // DDD Exercise: Model a payment taking system 3 | // 4 | // ================================================ 5 | 6 | (* 7 | The payment taking system should accept 8 | * Cash 9 | * Credit cards 10 | * Cheques 11 | * Paypal 12 | * Bitcoin 13 | 14 | A payment consists of a: 15 | * payment 16 | * non-negative amount 17 | 18 | After designing the types, create functions that will: 19 | 20 | * print a payment method 21 | * print a payment, including the amount 22 | * create a new payment from an amount and method 23 | 24 | *) 25 | 26 | type CardType = Visa | Mastercard 27 | type CardNumber = CardNumber of string 28 | type ChequeNumber = ChequeNumber of int 29 | type EmailAddress = EmailAddress of string 30 | type BitcoinAddress = BitcoinAddress of string 31 | 32 | type PaymentMethod = 33 | | Cash 34 | | Cheque of ChequeNumber 35 | | Card of CardType * CardNumber 36 | | PayPal of EmailAddress 37 | | Bitcoin of BitcoinAddress 38 | 39 | type PaymentAmount = 40 | PaymentAmount of float 41 | 42 | type Payment = { 43 | paymentMethod: PaymentMethod 44 | paymentAmount: PaymentAmount 45 | } 46 | 47 | let printPaymentMethod (paymentMethod:PaymentMethod) = 48 | match paymentMethod with 49 | | Cash -> printfn "Paid in cash" 50 | | Cheque checkNo -> printfn "Paid by cheque: %A" checkNo 51 | | Card (cardType,cardNo) -> printfn "Paid with %A %A" cardType cardNo 52 | | PayPal emailAddress -> printfn "Paid with PayPal %A" emailAddress 53 | | Bitcoin bitcoinAddress -> printfn "Paid with BitCoin %A" bitcoinAddress 54 | 55 | let printPayment (payment:Payment) = 56 | match payment.paymentAmount 57 | with PaymentAmount amount -> printf "Amount: %g. " amount 58 | printPaymentMethod payment.paymentMethod 59 | 60 | let makePayment (amount:float) (paymentMethod:PaymentMethod) :Payment = 61 | {paymentMethod=paymentMethod; paymentAmount=PaymentAmount amount} 62 | 63 | // examples 64 | let paymentMethod1 = Cash 65 | let paymentMethod2 = Cheque (ChequeNumber 42) 66 | let paymentMethod3 = Card (Visa, CardNumber "1234") 67 | let paymentMethod4 = PayPal (EmailAddress "me@example.com") 68 | let paymentMethod5 = Bitcoin (BitcoinAddress "1234") 69 | 70 | let payment1 = makePayment 42.0 paymentMethod1 71 | let payment2 = makePayment 123.0 paymentMethod2 72 | let payment3 = makePayment 123.0 paymentMethod3 73 | 74 | // highlight and run 75 | printPaymentMethod paymentMethod1 76 | printPaymentMethod paymentMethod2 77 | printPaymentMethod paymentMethod3 78 | printPaymentMethod paymentMethod4 79 | printPaymentMethod paymentMethod5 80 | 81 | printPayment payment1 82 | printPayment payment2 83 | printPayment payment3 84 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DDD Exercise 3 - Payments.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // DDD Exercise: Model a payment taking system 3 | // 4 | // ================================================ 5 | 6 | (* 7 | The payment taking system should accept 8 | * Cash 9 | * Credit cards 10 | * Cheques 11 | * Paypal 12 | * Bitcoin 13 | 14 | A payment consists of a: 15 | * payment 16 | * non-negative amount 17 | 18 | After designing the types, create functions that will: 19 | 20 | * print a payment method 21 | * print a payment, including the amount 22 | * create a new payment from an amount and method 23 | 24 | 25 | *) 26 | 27 | type CardType = Visa | Mastercard | ?? 28 | type CardNumber = CardNumber of string 29 | type ChequeNumber = ?? 30 | type EmailAddress = ?? 31 | type BitcoinAddress = ?? 32 | 33 | type PaymentMethod = 34 | | Cash 35 | | Cheque of ChequeNumber 36 | | Card of ?? 37 | | PayPal of ?? 38 | | Bitcoin of ?? 39 | 40 | type PaymentAmount = ?? 41 | 42 | type Payment = ?? 43 | 44 | let printPaymentMethod (paymentMethod:PaymentMethod) = 45 | match paymentMethod with 46 | | Cash -> ?? 47 | | Cheque checkNo -> ?? 48 | | Card (cardType,cardNo) -> ?? 49 | 50 | let printPayment (payment:Payment) = 51 | ?? 52 | 53 | let makePayment (amount:float) (paymentMethod:PaymentMethod) :Payment = 54 | ?? 55 | 56 | // examples 57 | let paymentMethod1 = Cash 58 | let paymentMethod2 = Cheque (ChequeNumber 42) 59 | let paymentMethod3 = Card (Visa, CardNumber "1234") 60 | let paymentMethod4 = PayPal (EmailAddress "me@example.com") 61 | let paymentMethod5 = Bitcoin (BitcoinAddress "1234") 62 | 63 | let payment1 = makePayment 42.0 paymentMethod1 64 | let payment2 = makePayment 123.0 paymentMethod2 65 | let payment3 = makePayment 123.0 paymentMethod3 66 | 67 | // highlight and run 68 | printPaymentMethod paymentMethod1 69 | printPaymentMethod paymentMethod2 70 | printPaymentMethod paymentMethod3 71 | printPaymentMethod paymentMethod4 72 | printPaymentMethod paymentMethod5 73 | 74 | printPayment payment1 75 | printPayment payment2 76 | printPayment payment3 77 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DDD Exercise 4 - Refactoring flags (answers).fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // DDD Exercise: Refactoring designs to use states 3 | // ================================================ 4 | 5 | (* 6 | Much C# code has implicit states that you can recognize by fields called "IsSomething", or nullable date 7 | 8 | This is a sign that states transitions are present but not being modelled properly. 9 | *) 10 | 11 | // Exercise 3a - redesign this type into two states: RegisteredCustomer (with an id) and GuestCustomer (without an id) 12 | type Customer_Before = 13 | { 14 | CustomerName: string 15 | IsGuest: bool 16 | RegistrationId: int option 17 | } 18 | 19 | type CustomerName = CustomerName of string 20 | type RegistrationId = RegistrationId of int 21 | 22 | type Customer_After = 23 | | Guest of CustomerName 24 | | RegisteredCustomer of CustomerName * RegistrationId 25 | 26 | 27 | // Exercise 3b - redesign this type into two states: Connected and Disconnected 28 | type Connection_Before = 29 | { 30 | IsConnected: bool 31 | ConnectionStartedUtc: System.DateTime option 32 | ConnectionHandle: int 33 | ReasonForDisconnection: string 34 | } 35 | 36 | type ConnectionHandle = ConnectionHandle of int 37 | type ConnectionStartedUtc = System.DateTime 38 | type ReasonForDisconnection = string 39 | 40 | type Connection__After = 41 | | Connected of ConnectionHandle * ConnectionStartedUtc 42 | | Disconnected of ReasonForDisconnection 43 | 44 | 45 | // Exercise 3c - redesign this type into two states -- can you guess what the states 46 | // are from the flags -- how does the refactored version help improve the documentation? 47 | type Order_Before = 48 | { 49 | OrderId: int 50 | IsPaid: bool 51 | PaidAmount: float option 52 | PaidDate: System.DateTime option 53 | } 54 | 55 | type OrderId = OrderId of int 56 | type PaidAmount = float 57 | type PaidDate = System.DateTime 58 | type Order__After = 59 | | Unpaid of OrderId 60 | | Paid of OrderId * PaidAmount * PaidDate 61 | 62 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DDD Exercise 4 - Refactoring flags.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // DDD Exercise: Refactoring designs to use states 3 | // ================================================ 4 | 5 | (* 6 | Much C# code has implicit states that you can recognize by fields called "IsSomething", or nullable date 7 | 8 | This is a sign that states transitions are present but not being modelled properly. 9 | *) 10 | 11 | // Exercise 3a - redesign this type into two states: RegisteredCustomer (with an id) and GuestCustomer (without an id) 12 | type Customer_Before = 13 | { 14 | CustomerName: string 15 | IsGuest: bool 16 | RegistrationId: int option 17 | } 18 | 19 | type Customer_After = ?? 20 | 21 | 22 | // Exercise 3b - redesign this type into two states: Connected and Disconnected 23 | type Connection_Before = 24 | { 25 | IsConnected: bool 26 | ConnectionStartedUtc: System.DateTime option 27 | ConnectionHandle: int 28 | ReasonForDisconnection: string 29 | } 30 | 31 | type Connection__After = ?? 32 | 33 | 34 | // Exercise 3c - redesign this type into two states -- can you guess what the states 35 | // are from the flags -- how does the refactored version help improve the documentation? 36 | type Order_Before = 37 | { 38 | OrderId: int 39 | IsPaid: bool 40 | PaidAmount: float option 41 | PaidDate: System.DateTime option 42 | } 43 | 44 | type Order__After = ?? 45 | 46 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DDD_Projects.fsx: -------------------------------------------------------------------------------- 1 |  2 | // ================================================ 3 | // Exercise 1: Modeling some domains 4 | // 5 | // 6 | // ================================================ 7 | 8 | (* 9 | Exercise 1a: create types that model a Tic-Tac-Toe game 10 | 11 | type BoardLocation = ??? 12 | 13 | type BoardState = ??? 14 | 15 | type Player = ??? 16 | 17 | type Move = ??? 18 | 19 | 20 | *) 21 | 22 | 23 | (* 24 | Exercise 1b: create types that model a domain that you are expert in. 25 | 26 | Are there any business/domain rules that you can encode in types? 27 | 28 | When you're done, we can do a show and tell! 29 | 30 | 31 | *) 32 | 33 | 34 | // ================================================ 35 | // Exercise 2: Refactoring designs to use states 36 | // ================================================ 37 | 38 | (* 39 | Many existing designs have implicit states that you can recognize by fields called "IsSomething", or nullable date 40 | 41 | This is a sign that state transitions are present but not being modelled properly. 42 | *) 43 | 44 | // Exercise 2a - redesign this type into two states: RegisteredCustomer and GuestCustomer 45 | type Customer = 46 | { 47 | CustomerName: string 48 | IsGuest: bool 49 | RegistrationId: int option 50 | } 51 | 52 | 53 | // Exercise 2b - redesign this type into two states: Connected and Disconnected 54 | type Connection = 55 | { 56 | IsConnected: bool 57 | ConnectionStartedUtc: Nullable 58 | ConnectionHandle: int 59 | ReasonForDisconnection: string 60 | } 61 | 62 | 63 | 64 | // ================================================ 65 | // Exercise 3: Modeling e-commerce shopping cart transitions 66 | // 67 | // See shopping_cart_transition_diagram.png 68 | // 69 | // ================================================ 70 | 71 | (* 72 | Exercise: create types that model an e-commerce shopping cart 73 | 74 | Rule: "You can't remove an item from an empty cart" 75 | Rule: "You can't change a paid cart" 76 | Rule: "You can't pay for a cart twice" 77 | 78 | States are: 79 | * Empty 80 | * ActiveCartData 81 | * PaidCartData 82 | 83 | // 1) Start with the domain types that are independent of state 84 | 85 | type Product = string // placeholder for now 86 | type Cart = Product list // placeholder for now 87 | 88 | // 2) Create a type to represent the data stored for each type 89 | 90 | type Empty = no data to store, so not needed? 91 | type Active = what data to store?? 92 | type Paid = what data to store?? 93 | 94 | // 3) Create a type that represent the choice of all the states 95 | 96 | type ShoppingCart = 97 | | what? 98 | | what? 99 | | what? 100 | 101 | // 4) Create transition functions that transition from one state to another 102 | 103 | // "initCart" creates a new cart when adding the first item 104 | // The function signature should be 105 | // string -> ShoppingCart 106 | 107 | let initCart itemToAdd = what?? 108 | 109 | // "addToActive" creates a new state from active data and a new item 110 | // function signature should be 111 | // string -> ActiveCartData -> ShoppingCart 112 | 113 | let addToActive itemToAdd activeCartData = 114 | 115 | // "pay" creates a new state from active data and a payment amount 116 | // function signature should be 117 | // float -> ActiveCartData -> ShoppingCart 118 | 119 | let pay paymentAmount activeCartData = 120 | 121 | 122 | // "removeFromActive" creates a new state from active data after removing an item 123 | // function signature should be 124 | // string -> ActiveCartData -> ShoppingCart 125 | 126 | // removeItem is tricky -- you need to test the card contents after removal to find out what the new state is! 127 | 128 | // you'll need this helper for removeItem transition 129 | let removeItemFromCartHelper (productToRemove:Product) (cart:Cart) :Cart = 130 | cart |> List.filter (fun prod -> prod <> productToRemove) 131 | 132 | let removeFromActive itemToRemove activeCartData = 133 | 134 | 135 | // 5) Clients write functions using the state union type 136 | 137 | // "clientAddItem" changes the cart state after adding an item 138 | // function signature should be 139 | // string -> ShoppingCart-> ShoppingCart 140 | 141 | let clientAddItem newItem cart = 142 | match state with 143 | // | empty -> 144 | let new cart contents = what?? 145 | return what new state 146 | 147 | // | active -> 148 | let new cart contents = what?? 149 | return what new state 150 | 151 | // | paid -> ignore? 152 | 153 | // "clientPayForCart " changes the cart state after paying 154 | // function signature should be 155 | // float -> ShoppingCart-> ShoppingCart 156 | 157 | let clientPayForCart payment cart = 158 | match cart with 159 | // | empty -> ignore? 160 | // | active -> return new state 161 | // | paid -> ignore? 162 | 163 | *) 164 | 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DSL Exercise 1 - Time (answers).fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // DSL Exercise: Create a DSL to report the relative time 3 | // 4 | // ================================================ 5 | 6 | (* 7 | // Design the types to work with these syntax examples 8 | let example1 = getDate 5 Days Ago 9 | let example2 = getDate 1 Hour Hence 10 | 11 | // the C# equivalent would probably be more like this: 12 | // getDate().Interval(5).Days().Ago() 13 | // getDate().Interval(1).Hour().Hence() 14 | 15 | *) 16 | 17 | // set up the vocabulary 18 | type DateScale = Hour | Hours | Day | Days | Week | Weeks 19 | type DateDirection = Ago | Hence 20 | 21 | // define a function that matches on the vocabulary 22 | let getDate (interval:int) (scale:DateScale) (direction:DateDirection) = 23 | let absHours = match scale with 24 | | Hour | Hours -> 1 * interval 25 | | Day | Days -> 24 * interval 26 | | Week | Weeks -> 24 * 7 * interval 27 | let signedHours = match direction with 28 | | Ago -> -1 * absHours 29 | | Hence -> absHours 30 | System.DateTime.Now.AddHours(float signedHours) 31 | 32 | // test some examples 33 | let example1 = getDate 5 Days Ago 34 | let example2 = getDate 1 Hour Hence 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DSL Exercise 1 - Time.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // DSL Exercise: Create a DSL to report the relative time 3 | // 4 | // ================================================ 5 | 6 | (* 7 | // Design the types to work with these syntax examples 8 | let example1 = getDate 5 Days Ago 9 | let example2 = getDate 1 Hour Hence 10 | 11 | // the C# equivalent would probably be more like this: 12 | // getDate().Interval(5).Days().Ago() 13 | // getDate().Interval(1).Hour().Hence() 14 | 15 | *) 16 | 17 | // set up the vocabulary 18 | type DateScale = what?? 19 | type DateDirection = what?? 20 | 21 | // define a function that matches on the vocabulary 22 | let getDate (interval:int) (scale:DateScale) (direction:DateDirection) = 23 | what?? 24 | 25 | 26 | // test some examples 27 | let example1 = getDate 5 Days Ago 28 | let example2 = getDate 1 Hour Hence 29 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DSL Exercise 2 - Turtle Graphics (answers).fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // DSL Exercise: Create a DSL to move a turtle around 3 | // 4 | // ================================================ 5 | 6 | 7 | (* 8 | 9 | A turtle has a Position (in a x,y coordinate grid), a Direction and a Color. 10 | 11 | You can instruct the turtle to do something using the following instructions: 12 | 13 | * Move N, where n is an int 14 | * Turn left, which rotates the direction anti-clockwise 15 | * Turn right, which rotates the direction clockwise 16 | * Change color to black 17 | * Change color to red 18 | 19 | 1) Create a vocabulary for a turtle 20 | 21 | 2) Write code that will make the above instructions work. 22 | 23 | 3) Then write code that will take a list of instructions and apply them all, as follows: 24 | 25 | let instructions = [ 26 | Turn Left 27 | Move 100 28 | SetColor Red 29 | Turn Right 30 | Move 10 31 | ] 32 | 33 | instructions |> applyListOfInstructions 34 | 35 | *) 36 | 37 | // set up the vocabulary 38 | type Position = int * int 39 | type Color = Black | Red 40 | type Direction = North | South | East | West 41 | type Turtle = { 42 | pos: Position 43 | direction: Direction 44 | color: Color 45 | } 46 | 47 | type TurnInstruction = Left | Right 48 | 49 | type TurtleInstruction = 50 | | Move of int 51 | | Turn of TurnInstruction 52 | | SetColor of Color 53 | 54 | 55 | // define a function that changes the position given a distance and direction and returns a new Position 56 | let changePosition (distance:int) (direction:Direction) (pos:Position) :Position = 57 | let x,y = pos 58 | match direction with 59 | | North -> 60 | x, y + distance 61 | | South -> 62 | x, y - distance 63 | | East -> 64 | x + distance, y 65 | | West -> 66 | x - distance, y 67 | 68 | // define a function that changes the direction given a turn instruction and returns a new Direction 69 | let turnDirection (turnInstruction:TurnInstruction) (direction:Direction) :Direction = 70 | match direction with 71 | | North -> 72 | match turnInstruction with Left -> West | Right -> East 73 | | South -> 74 | match turnInstruction with Left -> East | Right -> West 75 | | East -> 76 | match turnInstruction with Left -> North | Right -> South 77 | | West -> 78 | match turnInstruction with Left -> South | Right -> North 79 | 80 | // define a function that moves the turtle given a TurtleInstruction and returns a new Turtle 81 | let moveTurtle (instruction:TurtleInstruction) (turtle:Turtle) :Turtle = 82 | match instruction with 83 | | Move distance -> 84 | let newPos = turtle.pos |> changePosition distance turtle.direction 85 | {turtle with pos = newPos} 86 | | Turn turnInstruction -> 87 | let newDirection = turtle.direction |> turnDirection turnInstruction 88 | {turtle with direction = newDirection} 89 | | SetColor newColor -> 90 | {turtle with color = newColor} 91 | 92 | // define function that applies a list of instructions 93 | let applyListOfInstructions (instructions:TurtleInstruction list) = 94 | // List.fold has parameters [action] [initialValue] [list] 95 | // - action has two params - the state and the new instruction 96 | let foldAction turtle instruction = 97 | moveTurtle instruction turtle 98 | let initialState = { pos=0,0; direction=North; color=Black} 99 | List.fold foldAction initialState instructions 100 | 101 | // --------------------------------------------- 102 | // test some examples 103 | // --------------------------------------------- 104 | let turtle0 = { pos=0,0; direction=North; color=Black} 105 | 106 | let instruction1 = Turn Left 107 | let turtle1 = turtle0 |> moveTurtle instruction1 108 | 109 | let instruction2 = Move 100 110 | let turtle2 = turtle1 |> moveTurtle instruction2 111 | 112 | // --------------------------------------------- 113 | // test a whole set of instructions 114 | // --------------------------------------------- 115 | let instructions = [ 116 | Turn Left 117 | Move 100 118 | SetColor Red 119 | Turn Right 120 | Move 10 121 | ] 122 | 123 | instructions |> applyListOfInstructions -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DSL Exercise 2 - Turtle Graphics.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // DSL Exercise: Create a DSL to move a turtle around 3 | // 4 | // ================================================ 5 | 6 | 7 | (* 8 | A turtle has a Position (in a x,y coordinate grid), a Direction and a Color. 9 | 10 | You can instruct the turtle to do something using the following instructions: 11 | 12 | * Move N, where n is an int 13 | * Turn left, which rotates the direction anti-clockwise 14 | * Turn right, which rotates the direction clockwise 15 | * Change color to black 16 | * Change color to red 17 | 18 | 1) Create a vocabulary for a turtle 19 | 20 | 2) Write code that will make the above instructions work. 21 | 22 | 3) Then write code that will take a list of instructions and apply them all, as follows: 23 | 24 | let instructions = [ 25 | Turn Left 26 | Move 100 27 | SetColor Red 28 | Turn Right 29 | Move 10 30 | ] 31 | 32 | instructions |> applyListOfInstructions 33 | 34 | *) 35 | 36 | // set up the vocabulary 37 | type Position = what?? 38 | type Color = what?? 39 | type Direction = what?? 40 | type Turtle = what?? // hint: a record type 41 | 42 | type TurnInstruction = what?? 43 | 44 | type TurtleInstruction = what?? 45 | 46 | // define a function that changes the position given a distance and direction and returns a new Position 47 | let changePosition (distance:int) (direction:Direction) (pos:Position) :Position = 48 | match direction with 49 | | what -> 50 | what?? 51 | 52 | // define a function that changes the direction given a turn instruction and returns a new Direction 53 | let turnDirection (turnInstruction:TurnInstruction) (direction:Direction) :Direction = 54 | match direction with 55 | | what -> 56 | what?? 57 | 58 | // define a function that moves the turtle given a TurtleInstruction and returns a new Turtle 59 | let moveTurtle (instruction:TurtleInstruction) (turtle:Turtle) :Turtle = 60 | match instruction with 61 | | what -> 62 | what?? 63 | 64 | // define function that applies a list of instructions 65 | let applyListOfInstructions (instructions:TurtleInstruction list) = 66 | // List.fold has parameters [action] [initialValue] [list] 67 | // - action has two params - the state and the new instruction 68 | let foldAction turtle instruction = 69 | moveTurtle instruction turtle 70 | let initialState = ?? 71 | List.fold foldAction initialState instructions 72 | 73 | 74 | // --------------------------------------------- 75 | // test some examples 76 | // --------------------------------------------- 77 | let turtle0 = ?? 78 | 79 | let instruction1 = Turn Left 80 | let turtle1 = turtle0 |> moveTurtle instruction1 81 | 82 | let instruction2 = Move 100 83 | let turtle2 = turtle1 |> moveTurtle instruction2 84 | 85 | // --------------------------------------------- 86 | // test a whole set of instructions 87 | // --------------------------------------------- 88 | let instructions = [ 89 | Turn Left 90 | Move 100 91 | SetColor Red 92 | Turn Right 93 | Move 10 94 | ] 95 | 96 | instructions |> applyListOfInstructions -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DSL Exercise 3 - Recipe (answers).fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // DSL Exercise: Create a DSL to represent a recipe 3 | // 4 | // 5 | // ================================================ 6 | 7 | (* 8 | A Recipe consists of a list of steps. 9 | 10 | Each step is one of the following: 11 | * add some ingredients (e.g "take 2 eggs") 12 | * follow an instruction (e.g. "beat for 1 minute") 13 | * oven (e.g. "put in 200C oven") 14 | * timed (e.g. "bake for 20 minutes") 15 | 16 | An ingredient is either a Thing (e.g. an egg) or Stuff (e.g. milk) 17 | 18 | 1) Create a vocabulary for a creating Recipes 19 | 20 | 2) Write code that will make the example recipes (shown below) work 21 | 22 | 23 | *) 24 | 25 | 26 | // ----------------------------------------- 27 | // The core domain types 28 | // ----------------------------------------- 29 | 30 | 31 | // define the types 32 | type IngredientName = string 33 | type IngredientAmount = int 34 | type UnitOfMeasure = Grams | Mils | Tsp | Tsps | Tbsp | Tbsps 35 | 36 | type Ingredient = 37 | // Ingredient is a Thing (which is counted) 38 | | IngredientThing of IngredientAmount * IngredientName 39 | // OR it is Stuff (which is measured) 40 | | IngredientStuff of IngredientAmount * UnitOfMeasure * IngredientName 41 | 42 | type Comment = string 43 | type TemperatureUnit = C | F 44 | type TemperatureLevel = int 45 | type TimeDuration = int 46 | type TimeUnit = Mins | Hours 47 | 48 | type RecipeStep = 49 | | IngredientStep of Comment * Ingredient list 50 | | InstructionStep of Comment 51 | | OvenStep of TemperatureLevel * TemperatureUnit 52 | | TimedStep of Comment * TimeDuration * TimeUnit 53 | 54 | type Recipe = RecipeStep list 55 | 56 | // ----------------------------------------- 57 | // Helper functions form the DSL vocabulary 58 | // ----------------------------------------- 59 | 60 | // create an IngredientThing with eggs 61 | let takeEggs amount = IngredientThing (amount, "eggs") 62 | 63 | // create an IngredientStuff 64 | let take amount unit ingredientName = IngredientStuff(amount, unit, ingredientName) 65 | 66 | // create an IngredientStep 67 | let combine str ingredients = IngredientStep ("Combine "+str,ingredients) 68 | let toStart ingredient = IngredientStep ("Start with",[ingredient]) 69 | let thenAdd ingredients = IngredientStep ("Add",ingredients) 70 | 71 | // create an InstructionStep 72 | let thenDo action = InstructionStep ("Then " + action) 73 | 74 | // create a OvenStep 75 | let bakeAt temp unit = OvenStep(temp,unit) 76 | 77 | // create a TimedStep 78 | let beatFor time unit = TimedStep ("Beat for",time, unit) 79 | let cookFor time unit = TimedStep ("Cook for",time, unit) 80 | 81 | 82 | // ----------------------------------------- 83 | // Examples of recipes using the DSL 84 | // ----------------------------------------- 85 | 86 | // create a recipe for chocolate cake 87 | let chocolateCake = [ 88 | combine "in a large bowl: " [ 89 | take 225 Grams "flour" 90 | take 350 Grams "sugar" 91 | take 85 Grams "cocoa" 92 | take 2 Tsps "baking soda" 93 | take 1 Tsp "baking powder" 94 | take 1 Tsp "salt" ] 95 | thenDo "make a well in the centre" 96 | thenAdd [ 97 | takeEggs 2 98 | take 125 Mils "oil" 99 | take 250 Mils "milk"] 100 | beatFor 2 Mins 101 | bakeAt 175 C 102 | thenDo "add icing" 103 | ] 104 | 105 | 106 | // create a recipe for pasta with sauce 107 | let pastaWithSauce = [ 108 | take 400 Grams "pasta" |> toStart 109 | cookFor 8 Mins 110 | thenDo "drain." 111 | thenAdd [ 112 | take 100 Grams "tomato sauce" 113 | take 1 Tsp "pepper"] 114 | thenDo "serve hot" 115 | ] 116 | 117 | // Add your own examples from a recipe site, such as 118 | // http://www.bbc.co.uk/food/recipes/easy_chocolate_cake_31070 119 | 120 | // ----------------------------------------- 121 | // Create utility functions that work with recipes to 122 | // * print the ingredient list 123 | // * print the preperation (e.g. preheat oven) 124 | // * print the steps 125 | // 126 | // See below for examples of the output 127 | // ----------------------------------------- 128 | 129 | // create a function that takes a single ingredient and prints it in the form 130 | // "400 Grams pasta" 131 | // "2 Eggs" 132 | let printSingleIngredient (ingredient:Ingredient) = 133 | match ingredient with 134 | | IngredientThing (amount,ingr) -> 135 | printfn "%i %s" amount ingr 136 | | IngredientStuff (amount,unit,ingr) -> 137 | printfn "%i %A %s" amount unit ingr 138 | 139 | // create a function that prints the ingredients, if it is an ingredient step, otherwise do nothing 140 | let printIngredientsForStep (step:RecipeStep) = 141 | match step with 142 | | IngredientStep (_, ingredients) -> 143 | ingredients |> List.iter printSingleIngredient 144 | | InstructionStep _ -> // use underscore to ignore data 145 | () // return unit for ignored branches 146 | | OvenStep _ -> 147 | () 148 | | TimedStep _ -> 149 | () 150 | 151 | // create a function that prints the all ingredients in the recipe 152 | let printIngredients (recipe:Recipe) = 153 | recipe |> List.iter printIngredientsForStep 154 | 155 | 156 | // create a function that prints the "Preheat oven to 200 C", if it is an OvenStep, otherwise do nothing 157 | let printPreparationForStep (step:RecipeStep) = 158 | match step with 159 | | IngredientStep _ -> () 160 | | InstructionStep _ -> () 161 | | OvenStep (temp,unit) -> printfn "Preheat oven to %i %A" temp unit 162 | | TimedStep _ -> () 163 | 164 | // create a function that prints the all preperation steps in the recipe 165 | let printPreparation (recipe:Recipe) = 166 | recipe |> List.iter printPreparationForStep 167 | 168 | // create a function that prints the instructions for each step 169 | let printInstructionForStep (step:RecipeStep) = 170 | 171 | // private helper method 172 | let rec printSingleIngredient ingredient = 173 | match ingredient with 174 | | IngredientThing (_,ingr) -> printf "%s, " ingr 175 | | IngredientStuff (_,_,ingr) -> printf "%s, " ingr 176 | 177 | 178 | match step with 179 | | IngredientStep (comment, ingredients) -> 180 | printf "%s " comment 181 | ingredients |> List.iter printSingleIngredient 182 | printfn "" 183 | | InstructionStep (comment) -> 184 | printfn "%s" comment 185 | | OvenStep (temp,unit) -> 186 | printfn "Bake for %i %A" temp unit 187 | | TimedStep(comment,time,unit) -> 188 | printfn "%s %i %A" comment time unit 189 | 190 | // create a function that prints the all instructions in the recipe 191 | let printInstructions (recipe:Recipe) = 192 | recipe |> List.iter printInstructionForStep 193 | 194 | // put all three functions together 195 | let rec printRecipe recipe = 196 | printfn "==== Ingredients =====" 197 | printIngredients recipe 198 | printfn "\n===== Preparation =====" 199 | printPreparation recipe 200 | printfn "\n===== Instructions =====" 201 | printInstructions recipe 202 | 203 | // ----------------------------------------- 204 | // print the recipe for chocolateCake 205 | // ----------------------------------------- 206 | printRecipe chocolateCake 207 | 208 | (* 209 | ==== Ingredients ===== 210 | 225 Grams flour 211 | 350 Grams sugar 212 | 85 Grams cocoa 213 | 2 Tsps baking soda 214 | 1 Tsp baking powder 215 | 1 Tsp salt 216 | 2 eggs 217 | 125 Mils oil 218 | 250 Mils milk 219 | 220 | ===== Preparation ===== 221 | Preheat oven to 175 C 222 | 223 | ===== Instructions ===== 224 | Combine in a large bowl: flour, sugar, cocoa, baking soda, baking powder, salt, 225 | Then make a well in the centre 226 | Add eggs, oil, milk, 227 | Beat for 2 Mins 228 | Bake for 175 C 229 | Then add icing 230 | *) 231 | 232 | // ----------------------------------------- 233 | // print the recipe for pastaWithSauce 234 | // ----------------------------------------- 235 | printRecipe pastaWithSauce 236 | 237 | (* 238 | ==== Ingredients ===== 239 | 400 Grams pasta 240 | 100 Grams tomato sauce 241 | 1 Tsp pepper 242 | 243 | ===== Preparation ===== 244 | 245 | ===== Instructions ===== 246 | Start with pasta, 247 | Cook for 8 Mins 248 | Then drain. 249 | Add tomato sauce, pepper, 250 | Then serve hot 251 | *) 252 | 253 | 254 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DSL Exercise 3 - Recipe.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // DSL Exercise: Create a DSL to represent a recipe 3 | // 4 | // 5 | // ================================================ 6 | 7 | (* 8 | A Recipe consists of a list of steps. 9 | 10 | Each step is one of the following: 11 | * add some ingredients (e.g "take 2 eggs") 12 | * follow an instruction (e.g. "beat for 1 minute") 13 | * oven (e.g. "put in 200C oven") 14 | * timed (e.g. "bake for 20 minutes") 15 | 16 | An ingredient is either a Thing (e.g. an egg) or Stuff (e.g. milk) 17 | 18 | 1) Create a vocabulary for a creating Recipes 19 | 20 | 2) Write code that will make the example recipes (shown below) work 21 | 22 | 23 | *) 24 | 25 | 26 | // ----------------------------------------- 27 | // The core domain types 28 | // ----------------------------------------- 29 | 30 | 31 | // define the types 32 | type IngredientName = string 33 | type IngredientAmount = int 34 | type UnitOfMeasure = Grams | Mils | Tsp | Tsps | Tbsp | Tbsps 35 | 36 | type Ingredient = 37 | // Ingredient is a Thing (which is counted) 38 | | IngredientThing ?? 39 | // OR it is Stuff (which is measured) 40 | | IngredientStuff ?? 41 | 42 | type Comment = string 43 | type TemperatureUnit = C | F 44 | type TemperatureLevel = int 45 | type TimeDuration = int 46 | type TimeUnit = Mins | Hours 47 | 48 | type RecipeStep = 49 | | IngredientStep ?? 50 | | InstructionStep ?? 51 | 52 | type Recipe = RecipeStep list 53 | 54 | // ----------------------------------------- 55 | // Helper functions form the DSL vocabulary 56 | // ----------------------------------------- 57 | 58 | // create an IngredientThing with eggs 59 | let takeEggs amount = IngredientThing (amount, "eggs") 60 | 61 | // create an IngredientStuff 62 | let take amount unit ingredientName = ?? 63 | 64 | // create an IngredientStep 65 | let combine str ingredients = IngredientStep ("Combine "+str,ingredients) 66 | let toStart ingredient = ?? 67 | let thenAdd ingredients = ?? 68 | 69 | // create an InstructionStep 70 | let thenDo action = ?? 71 | 72 | // create a OvenStep 73 | let bakeAt temp unit = ?? 74 | 75 | // create a TimedStep 76 | let beatFor time unit = TimedStep ("Beat for",time, unit) 77 | let cookFor time unit = ?? 78 | 79 | 80 | // ----------------------------------------- 81 | // Examples of recipes using the DSL 82 | // ----------------------------------------- 83 | 84 | // create a recipe for chocolate cake 85 | let chocolateCake = [ 86 | combine "in a large bowl: " [ 87 | take 225 Grams "flour" 88 | take 350 Grams "sugar" 89 | take 85 Grams "cocoa" 90 | take 2 Tsps "baking soda" 91 | take 1 Tsp "baking powder" 92 | take 1 Tsp "salt" ] 93 | thenDo "make a well in the centre" 94 | thenAdd [ 95 | takeEggs 2 96 | take 125 Mils "oil" 97 | take 250 Mils "milk"] 98 | beatFor 2 Mins 99 | bakeAt 175 C 100 | thenDo "add icing" 101 | ] 102 | 103 | 104 | // create a recipe for pasta with sauce 105 | let pastaWithSauce = [ 106 | take 400 Grams "pasta" |> toStart 107 | cookFor 8 Mins 108 | thenDo "drain." 109 | thenAdd [ 110 | take 100 Grams "tomato sauce" 111 | take 1 Tsp "pepper"] 112 | thenDo "serve hot" 113 | ] 114 | 115 | // Add your own examples from a recipe site, such as 116 | // http://www.bbc.co.uk/food/recipes/easy_chocolate_cake_31070 117 | 118 | // ----------------------------------------- 119 | // Create utility functions that work with recipes to 120 | // * print the ingredient list 121 | // * print the preperation (e.g. preheat oven) 122 | // * print the steps 123 | // 124 | // See below for examples of the output 125 | // ----------------------------------------- 126 | 127 | // create a function that takes a single ingredient and prints it in the form 128 | // "400 Grams pasta" 129 | // "2 Eggs" 130 | let printSingleIngredient (ingredient:Ingredient) = 131 | match ingredient with 132 | | IngredientThing (amount,ingr) -> 133 | printfn ?? 134 | | IngredientStuff (amount,unit,ingr) -> 135 | printfn ?? 136 | 137 | // create a function that prints the ingredients, if it is an ingredient step, otherwise do nothing 138 | let printIngredientsForStep (step:RecipeStep) = 139 | match step with 140 | | IngredientStep ?? -> 141 | // print how? See list syntax in "all you need to know" 142 | | InstructionStep _ -> // use underscore to ignore data 143 | () // return unit for ignored branches 144 | | OvenStep _ -> 145 | () 146 | | TimedStep _ -> 147 | () 148 | 149 | // create a function that prints the all ingredients in the recipe 150 | let printIngredients (recipe:Recipe) = 151 | recipe |> List.iter printIngredientsForStep 152 | 153 | 154 | // create a function that prints the "Preheat oven to 200 C", if it is an OvenStep, otherwise do nothing 155 | let printPreparationForStep (step:RecipeStep) = 156 | match step with 157 | | IngredientStep ?? 158 | | InstructionStep ?? 159 | | OvenStep ?? 160 | | TimedStep ?? 161 | 162 | // create a function that prints the all preperation steps in the recipe 163 | let printPreparation (recipe:Recipe) = 164 | recipe |> List.iter printPreparationForStep 165 | 166 | // create a function that prints the instructions for each step 167 | let printInstructionForStep (step:RecipeStep) = 168 | 169 | // private helper method 170 | let rec printSingleIngredient ingredient = 171 | match ingredient with 172 | | IngredientThing (_,ingr) -> printf "%s, " ingr 173 | | IngredientStuff (_,_,ingr) -> printf "%s, " ingr 174 | 175 | match step with 176 | | IngredientStep ?? 177 | | InstructionStep ?? 178 | | OvenStep ?? 179 | | TimedStep ?? 180 | 181 | // create a function that prints the all instructions in the recipe 182 | let printInstructions (recipe:Recipe) = 183 | recipe |> List.iter printInstructionForStep 184 | 185 | // put all three functions together 186 | let rec printRecipe recipe = 187 | printfn "==== Ingredients =====" 188 | printIngredients recipe 189 | printfn "\n===== Preparation =====" 190 | printPreparation recipe 191 | printfn "\n===== Instructions =====" 192 | printInstructions recipe 193 | 194 | // ----------------------------------------- 195 | // print the recipe for chocolateCake 196 | // ----------------------------------------- 197 | printRecipe chocolateCake 198 | 199 | (* 200 | ==== Ingredients ===== 201 | 225 Grams flour 202 | 350 Grams sugar 203 | 85 Grams cocoa 204 | 2 Tsps baking soda 205 | 1 Tsp baking powder 206 | 1 Tsp salt 207 | 2 eggs 208 | 125 Mils oil 209 | 250 Mils milk 210 | 211 | ===== Preparation ===== 212 | Preheat oven to 175 C 213 | 214 | ===== Instructions ===== 215 | Combine in a large bowl: flour, sugar, cocoa, baking soda, baking powder, salt, 216 | Then make a well in the centre 217 | Add eggs, oil, milk, 218 | Beat for 2 Mins 219 | Bake for 175 C 220 | Then add icing 221 | *) 222 | 223 | // ----------------------------------------- 224 | // print the recipe for pastaWithSauce 225 | // ----------------------------------------- 226 | printRecipe pastaWithSauce 227 | 228 | (* 229 | ==== Ingredients ===== 230 | 400 Grams pasta 231 | 100 Grams tomato sauce 232 | 1 Tsp pepper 233 | 234 | ===== Preparation ===== 235 | 236 | ===== Instructions ===== 237 | Start with pasta, 238 | Cook for 8 Mins 239 | Then drain. 240 | Add tomato sauce, pepper, 241 | Then serve hot 242 | *) 243 | 244 | 245 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/DomainModelingInFSharp.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 2.0 8 | 41a07c34-53d6-4b60-b65d-c0d11941ec02 9 | Library 10 | DomainModelingInFSharp 11 | DomainModelingInFSharp 12 | v4.5 13 | DomainModelingInFSharp 14 | 4.3.0.0 15 | 16 | 17 | true 18 | full 19 | false 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | 3 24 | bin\Debug\DomainModelingInFSharp.XML 25 | 26 | 27 | pdbonly 28 | true 29 | true 30 | bin\Release\ 31 | TRACE 32 | 3 33 | bin\Release\DomainModelingInFSharp.XML 34 | 35 | 36 | 37 | True 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 11 91 | 92 | 93 | 94 | 95 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets 96 | 97 | 98 | 99 | 100 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 101 | 102 | 103 | 104 | 105 | 112 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/FSM Exercise 1 - Verified Email (answers).fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // FSM Exercise: A simple 2-state transition for Unverified and Verified email 3 | // 4 | // See "Email transition diagram.png" 5 | // 6 | // ================================================ 7 | 8 | // Here are the EmailAddress types from the slide 9 | 10 | module EmailDomain = 11 | 12 | type EmailAddress = 13 | EmailAddress of string 14 | 15 | type VerifiedEmail = 16 | VerifiedEmail of EmailAddress 17 | 18 | type EmailContactInfo = 19 | | Unverified of EmailAddress 20 | | Verified of VerifiedEmail 21 | 22 | 23 | // Create a function that (maybe) creates a VerifiedEmail 24 | let verify (hash:string) (email:EmailAddress) :VerifiedEmail option = 25 | if hash="OK" then 26 | Some (VerifiedEmail email) 27 | else 28 | None 29 | 30 | // ================================================ 31 | // Now write some client code that uses this API 32 | // ================================================ 33 | 34 | module EmailClient = 35 | open EmailDomain 36 | 37 | // Create a "verifyContactInfo" function that transitions from Unverified state to Verified state 38 | let verifyContactInfo (hash:string) (email:EmailContactInfo) :EmailContactInfo = 39 | match email with 40 | | Unverified emailAddress -> 41 | let verifiedOrNone = 42 | emailAddress |> verify hash 43 | match verifiedOrNone with 44 | | Some verifiedEmail -> 45 | printfn "the email was verified" 46 | Verified verifiedEmail 47 | | None -> 48 | printfn "the email was not verified" 49 | // return original state 50 | email 51 | | Verified _ -> 52 | printfn "the email is already verified" 53 | // return original state 54 | email 55 | 56 | // Create a "sendVerificationMessage" function 57 | // Rule: "You can't send a verification message to a verified email" 58 | let sendVerificationMessage (email:EmailContactInfo) = 59 | match email with 60 | | Unverified emailAddress -> 61 | printfn "Sending verification email to %A" emailAddress 62 | | Verified _ -> 63 | printfn "The email is already verified" 64 | 65 | // Create a "sendPasswordResetMessage " function 66 | // Rule: "You can't send a password reset message to a unverified email " 67 | let sendPasswordResetMessage (email:EmailContactInfo) = 68 | match email with 69 | | Unverified emailAddress -> 70 | printfn "Can't send reset email to unverified %A" emailAddress 71 | | Verified verifiedEmailAddress -> 72 | printfn "Sending reset email to %A" verifiedEmailAddress 73 | 74 | 75 | // ================================================ 76 | // Now write some test code 77 | // ================================================ 78 | 79 | open EmailDomain 80 | open EmailClient 81 | 82 | let email = EmailAddress "x@example.com" 83 | let unverified = Unverified email 84 | 85 | unverified |> sendVerificationMessage 86 | 87 | 88 | let verifiedOk = 89 | let hash = "OK" 90 | unverified |> verifyContactInfo hash 91 | 92 | verifiedOk |> sendPasswordResetMessage 93 | 94 | 95 | // errors 96 | verifiedOk |> sendVerificationMessage 97 | unverified |> sendPasswordResetMessage 98 | 99 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/FSM Exercise 1 - Verified Email transition diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swlaschin/DomainModellingInFsharp/ed21291dd1329972160dee814084a02c2f0f9e48/src/DomainModelingInFSharp/FSM Exercise 1 - Verified Email transition diagram.png -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/FSM Exercise 1 - Verified Email.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // FSM Exercise: A simple 2-state transition for Unverified and Verified email 3 | // 4 | // See "Email transition diagram.png" 5 | // 6 | // ================================================ 7 | 8 | // Here are the EmailAddress types from the slide 9 | 10 | type EmailAddress = 11 | EmailAddress of string 12 | 13 | type VerifiedEmail = 14 | VerifiedEmail of EmailAddress 15 | 16 | type EmailContactInfo = 17 | | Unverified of EmailAddress 18 | | Verified of VerifiedEmail 19 | 20 | 21 | // Create a function that (maybe) creates a VerifiedEmail 22 | let verify (hash:string) (email:EmailAddress) :VerifiedEmail option = 23 | if hash = "OK" then 24 | // then what 25 | else 26 | // else what 27 | // remember, this function returns a new state 28 | 29 | // ================================================ 30 | // Now write some client code that uses this API 31 | // ================================================ 32 | 33 | // Create a "verifyContactInfo" function that transitions from Unverified state to Verified state 34 | let verifyContactInfo (hash:string) (email:EmailContactInfo) :EmailContactInfo = 35 | match email with 36 | // what 37 | 38 | // Create a "sendVerificationMessage" function 39 | // Rule: "You can't send a verification message to a verified email" 40 | let sendVerificationMessage (email:EmailContactInfo) = 41 | match email with 42 | // what 43 | 44 | // Create a "sendPasswordResetMessage " function 45 | // Rule: "You can't send a password reset message to a unverified email " 46 | let sendPasswordResetMessage (email:EmailContactInfo) = 47 | match email with 48 | // what 49 | 50 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/FSM Exercise 2 - Shipments (answers).fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // FSM Exercise: Modeling package delivery transitions 3 | // 4 | // See Shipments transition diagram.png 5 | /// 6 | // ================================================ 7 | 8 | (* 9 | Exercise: create types that model package delivery transitions 10 | 11 | Rule: "You can't put a package on a truck if it is already out for delivery" 12 | Rule: "You can't sign for a package that is already delivered" 13 | 14 | States are: 15 | * Undelivered 16 | * OutForDelivery 17 | * Delivered 18 | *) 19 | 20 | open System 21 | 22 | module ShipmentsDomain = 23 | 24 | // 1) Start with the domain types that are independent of state 25 | 26 | type Package = string // placeholder for now 27 | type TruckId = int 28 | type SentUtc = DateTime 29 | type DeliveredUtc = DateTime 30 | type Signature = string 31 | 32 | // 2) Create types to represent the data stored for each state 33 | 34 | type UndeliveredData = Package 35 | type OutForDeliveryData = Package * TruckId * SentUtc 36 | type DeliveredData = Package * Signature * DeliveredUtc 37 | 38 | // 3) Create a type that represent the choice of all the states 39 | 40 | type Shipment = 41 | | UndeliveredState of UndeliveredData 42 | | OutForDeliveryState of OutForDeliveryData 43 | | DeliveredState of DeliveredData 44 | 45 | // 4) Create transition functions that transition from one state type to another 46 | 47 | let sendOutForDelivery (package:UndeliveredData) (truckId:TruckId) :Shipment = 48 | let utcNow = System.DateTime.UtcNow 49 | // return new state 50 | let outForDeliveryData = package, truckId, utcNow 51 | OutForDeliveryState outForDeliveryData 52 | 53 | let addressNotFound (outForDeliveryData:OutForDeliveryData) :Shipment = 54 | let package, truckId, utcNow = outForDeliveryData 55 | // return new state 56 | let undeliveredData = package 57 | UndeliveredState undeliveredData 58 | 59 | let signedFor (outForDeliveryData:OutForDeliveryData) (signature:Signature) :Shipment = 60 | let utcNow = System.DateTime.UtcNow 61 | let package, truckId, utcNow = outForDeliveryData 62 | // return new state 63 | let deliveredData = package,signature,utcNow 64 | DeliveredState deliveredData 65 | 66 | 67 | // ================================================ 68 | // Now write some client code that uses this API 69 | // ================================================ 70 | 71 | module ShipmentsClient = 72 | open ShipmentsDomain 73 | 74 | let putShipmentOnTruck (truckId:TruckId) state = 75 | match state with 76 | | UndeliveredState package -> 77 | sendOutForDelivery package truckId 78 | | OutForDeliveryState _ -> 79 | printfn "package already out" 80 | // return original state 81 | state 82 | | DeliveredState _ -> 83 | printfn "package already delivered" 84 | // return original state 85 | state 86 | 87 | let markAsDelivered (signature:Signature) state = 88 | match state with 89 | | UndeliveredState _ -> 90 | printfn "package not out" 91 | // return original state 92 | state 93 | | OutForDeliveryState data -> 94 | signedFor data signature 95 | | DeliveredState _ -> 96 | printfn "package already delivered" 97 | // return original state 98 | state 99 | 100 | 101 | // ================================================ 102 | // Now write some test code 103 | // ================================================ 104 | 105 | open ShipmentsDomain 106 | open ShipmentsClient 107 | 108 | let package = "My Package" 109 | let newShipment = UndeliveredState package 110 | 111 | let truckId = 123 112 | let outForDelivery = 113 | newShipment |> putShipmentOnTruck truckId 114 | 115 | let signature = "Scott" 116 | let delivered = 117 | outForDelivery |> markAsDelivered signature 118 | 119 | // errors when using the wrong state 120 | delivered |> markAsDelivered signature 121 | delivered |> putShipmentOnTruck truckId -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/FSM Exercise 2 - Shipments transition diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swlaschin/DomainModellingInFsharp/ed21291dd1329972160dee814084a02c2f0f9e48/src/DomainModelingInFSharp/FSM Exercise 2 - Shipments transition diagram.png -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/FSM Exercise 2 - Shipments.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // FSM Exercise: Modeling package delivery transitions 3 | // 4 | // See Shipments transition diagram.png 5 | /// 6 | // ================================================ 7 | 8 | (* 9 | Exercise: create types that model package delivery transitions 10 | 11 | Rule: "You can't put a package on a truck if it is already out for delivery" 12 | Rule: "You can't sign for a package that is already delivered" 13 | 14 | States are: 15 | * Undelivered 16 | * OutForDelivery 17 | * Delivered 18 | *) 19 | 20 | open System 21 | 22 | 23 | // 1) Start with the domain types that are independent of state 24 | 25 | type Package = string // placeholder for now 26 | type DeliveredUtc = DateTime 27 | type Signature = string 28 | 29 | // 2) Create types to represent the data stored for each state 30 | 31 | type UndeliveredData = what? 32 | type OutForDeliveryData = 33 | type DeliveredData = 34 | 35 | // 3) Create a type that represent the choice of all the states 36 | 37 | type Shipment = 38 | | what? 39 | | what? 40 | | what? 41 | 42 | // 4) Create transition functions that transition from one state type to another 43 | 44 | let sendOutForDelivery (package:Package) (anythingElse:whatType) :whatReturnType = 45 | what?? 46 | 47 | let addressNotFound (outForDelivery:whatInputType) :whatReturnType = 48 | what?? 49 | 50 | let signedFor (outForDelivery:whatInputType) :whatReturnType = 51 | what?? 52 | 53 | // ================================================ 54 | // Now write some client code that uses this API 55 | // ================================================ 56 | 57 | let putShipmentOnTruck extraData state = 58 | match state with 59 | // | undelivered -> change state 60 | // | outfordelivery -> ignore? 61 | // | delivered -> ignore? 62 | 63 | 64 | let markAsDelivered (signature:Signature) state = 65 | // | undelivered -> ignore? 66 | // | outfordelivery -> change state 67 | // | delivered -> ignore? 68 | 69 | 70 | let package = "My Package" 71 | let newShipment = UndeliveredState package 72 | 73 | let truckId = 123 74 | let outForDelivery = 75 | newShipment |> putShipmentOnTruck truckId 76 | 77 | let signature = "Scott" 78 | let delivered = 79 | outForDelivery |> markAsDelivered signature 80 | 81 | // errors when using the wrong state 82 | delivered |> markAsDelivered signature 83 | delivered |> putShipmentOnTruck truckId -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/FSM Exercise 3 - Shopping cart (answers).fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // FSM Exercise: Modeling e-commerce shopping cart transitions 3 | // 4 | // See Shopping cart transition diagram.png 5 | // 6 | // ================================================ 7 | 8 | (* 9 | Exercise: create types that model an e-commerce shopping cart 10 | 11 | Rule: "You can't remove an item from an empty cart" 12 | Rule: "You can't change a paid cart" 13 | Rule: "You can't pay for a cart twice" 14 | 15 | States are: 16 | * Empty 17 | * ActiveCartData 18 | * PaidCartData 19 | *) 20 | 21 | 22 | module ShoppingCartDomain = 23 | 24 | // 1) Start with the domain types that are independent of state 25 | 26 | type Product = string // placeholder for now 27 | type CartContents = Product list // placeholder for now 28 | type Payment = float // placeholder for now 29 | 30 | // 2) Create a type to represent the data stored for each type 31 | 32 | // type EmptyCartData = not needed 33 | type ActiveCartData = CartContents 34 | type PaidCartData = CartContents * Payment 35 | 36 | // 3) Create a type that represent the choice of all the states 37 | 38 | type ShoppingCart = 39 | | EmptyCartState 40 | | ActiveCartState of ActiveCartData 41 | | PaidCartState of PaidCartData 42 | 43 | // 4) Create transition functions that transition from one state to another 44 | 45 | // "initCart" creates a new cart when adding the first item 46 | // The function signature should be 47 | // Product -> ShoppingCart 48 | 49 | let initCart (itemToAdd:Product) :ShoppingCart = 50 | let activeData = [itemToAdd] 51 | ActiveCartState activeData 52 | 53 | // "addToActive" creates a new state from active data and a new item 54 | // function signature should be 55 | // Product -> ActiveCartData -> ShoppingCart 56 | 57 | let addToActive (itemToAdd:Product) (activeCartData:ActiveCartData) :ShoppingCart = 58 | let newActiveData = itemToAdd::activeCartData 59 | ActiveCartState newActiveData 60 | 61 | // "pay" creates a new state from active data and a payment amount 62 | // function signature should be 63 | // Payment -> ActiveCartData -> ShoppingCart 64 | 65 | let pay (payment:Payment) (activeCartData:ActiveCartData) :ShoppingCart = 66 | let paidData = activeCartData, payment 67 | PaidCartState paidData 68 | 69 | // "removeFromActive" creates a new state from active data after removing an item 70 | // function signature should be 71 | // Product -> ActiveCartData -> ShoppingCart 72 | 73 | // removeItem is tricky -- you need to test the card contents after removal to find out what the new state is! 74 | 75 | // you'll need this helper for removeItem transition 76 | let removeItemFromContents (productToRemove:Product) (cart:CartContents) :CartContents = 77 | cart |> List.filter (fun prod -> prod <> productToRemove) 78 | 79 | let removeFromActive (itemToRemove:Product) (activeCartData:ActiveCartData) :ShoppingCart = 80 | let newContents = removeItemFromContents itemToRemove activeCartData 81 | match newContents with 82 | | [] -> EmptyCartState 83 | | smallerData -> ActiveCartState smallerData 84 | 85 | // ================================================ 86 | // Now write some client code that uses this API 87 | // ================================================ 88 | module ShoppingCartClient = 89 | 90 | open ShoppingCartDomain 91 | 92 | // "clientAddItem" changes the cart state after adding an item 93 | // function signature should be 94 | // Product -> ShoppingCart-> ShoppingCart 95 | 96 | let clientAddItem (newItem:Product) (cart:ShoppingCart) :ShoppingCart = 97 | match cart with 98 | | EmptyCartState -> 99 | printfn "Adding item %s to empty cart" newItem 100 | initCart newItem 101 | | ActiveCartState data -> 102 | printfn "Adding item %s to active cart" newItem 103 | addToActive newItem data 104 | | PaidCartState data -> 105 | printfn "Can't modify paid cart" 106 | cart // return original cart 107 | 108 | // "clientPayForCart " changes the cart state after paying 109 | // function signature should be 110 | // Payment -> ShoppingCart-> ShoppingCart 111 | 112 | let clientPayForCart (payment:Payment) (cart:ShoppingCart) :ShoppingCart = 113 | match cart with 114 | | EmptyCartState -> 115 | printfn "Can't pay for empty cart" 116 | cart // return original cart 117 | | ActiveCartState data -> 118 | printfn "Paying %g for active cart" payment 119 | pay payment data 120 | | PaidCartState data -> 121 | printfn "Cart already paid for" 122 | cart // return original cart 123 | 124 | 125 | // ================================================ 126 | // Now write some test code 127 | // ================================================ 128 | 129 | open ShoppingCartDomain 130 | open ShoppingCartClient 131 | 132 | let item1 = "Book" 133 | let item2 = "Dvd" 134 | let item3 = "Headphones" 135 | 136 | let cart0 = EmptyCartState 137 | let cart1 = clientAddItem item1 cart0 138 | let cart2 = clientAddItem item2 cart1 139 | let cart3 = clientPayForCart 20.00 cart2 140 | 141 | // errors 142 | clientAddItem item2 cart3 143 | clientPayForCart 20.00 cart0 144 | clientPayForCart 20.00 cart3 145 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/FSM Exercise 3 - Shopping cart transition diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swlaschin/DomainModellingInFsharp/ed21291dd1329972160dee814084a02c2f0f9e48/src/DomainModelingInFSharp/FSM Exercise 3 - Shopping cart transition diagram.png -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/FSM Exercise 3 - Shopping cart.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // FSM Exercise: Modeling e-commerce shopping cart transitions 3 | // 4 | // See Shopping cart transition diagram.png 5 | // 6 | // ================================================ 7 | 8 | (* 9 | Exercise: create types that model an e-commerce shopping cart 10 | 11 | Rule: "You can't remove an item from an empty cart" 12 | Rule: "You can't change a paid cart" 13 | Rule: "You can't pay for a cart twice" 14 | 15 | States are: 16 | * Empty 17 | * ActiveCartData 18 | * PaidCartData 19 | *) 20 | 21 | module ShoppingCartDomain = 22 | 23 | // 1) Start with the domain types that are independent of state 24 | 25 | type Product = string // placeholder for now 26 | type CartContents = Product list // placeholder for now 27 | type Payment = float // placeholder for now 28 | 29 | // 2) Create a type to represent the data stored for each type 30 | 31 | type EmptyCartData = what data to store? 32 | type ActiveCartData = what data to store? 33 | type PaidCartData = what data to store? 34 | 35 | // 3) Create a type that represent the choice of all the states 36 | 37 | type ShoppingCart = 38 | | what? 39 | | what? 40 | | what? 41 | 42 | // 4) Create transition functions that transition from one state to another 43 | 44 | // "initCart" creates a new cart when adding the first item 45 | // The function signature should be 46 | // Product -> ShoppingCart 47 | 48 | let initCart (itemToAdd:Product) :ShoppingCart = 49 | what goes here? 50 | 51 | // "addToActive" creates a new state from active data and a new item 52 | // function signature should be 53 | // Product -> ActiveCartData -> ShoppingCart 54 | 55 | let addToActive (itemToAdd:Product) (activeCartData:ActiveCartData) :ShoppingCart = 56 | what goes here? 57 | 58 | // "pay" creates a new state from active data and a payment amount 59 | // function signature should be 60 | // Payment -> ActiveCartData -> ShoppingCart 61 | 62 | let pay (payment:Payment) (activeCartData:ActiveCartData) :ShoppingCart = 63 | what goes here? 64 | 65 | // "removeFromActive" creates a new state from active data after removing an item 66 | // function signature should be 67 | // Product -> ActiveCartData -> ShoppingCart 68 | 69 | // removeItem is tricky -- you need to test the card contents after removal to find out what the new state is! 70 | 71 | // you'll need this helper for removeItem transition 72 | let removeItemFromContents (productToRemove:Product) (cart:CartContents) :CartContents = 73 | cart |> List.filter (fun prod -> prod <> productToRemove) 74 | 75 | let removeFromActive (itemToRemove:Product) (activeCartData:ActiveCartData) :ShoppingCart = 76 | what goes here? 77 | 78 | // ================================================ 79 | // Now write some client code that uses this API 80 | // ================================================ 81 | module ShoppingCartClient = 82 | 83 | open ShoppingCartDomain 84 | 85 | // "clientAddItem" changes the cart state after adding an item 86 | // function signature should be 87 | // Product -> ShoppingCart-> ShoppingCart 88 | 89 | let clientAddItem (newItem:Product) (cart:ShoppingCart) :ShoppingCart = 90 | match cart with 91 | // | empty -> 92 | let new cart contents = what?? 93 | return what new state 94 | 95 | // | active -> 96 | let new cart contents = what?? 97 | return what new state 98 | 99 | // | paid -> 100 | 101 | // "clientPayForCart " changes the cart state after paying 102 | // function signature should be 103 | // Payment -> ShoppingCart-> ShoppingCart 104 | 105 | let clientPayForCart (payment:Payment) (cart:ShoppingCart) :ShoppingCart = 106 | match cart with 107 | // | empty -> 108 | // | active -> return new state 109 | // | paid -> 110 | 111 | 112 | 113 | 114 | // ================================================ 115 | // Now write some test code 116 | // ================================================ 117 | 118 | open ShoppingCartDomain 119 | open ShoppingCartClient 120 | 121 | let item1 = "Book" 122 | let item2 = "Dvd" 123 | let item3 = "Headphones" 124 | 125 | let cart0 = EmptyCartState 126 | let cart1 = clientAddItem item1 cart0 127 | let cart2 = clientAddItem item2 cart1 128 | let cart3 = clientPayForCart 20.00 cart2 129 | 130 | // errors 131 | clientAddItem item2 cart3 132 | clientPayForCart 20.00 cart0 133 | clientPayForCart 20.00 cart3 134 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/FSM Exercises - State machine diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swlaschin/DomainModellingInFsharp/ed21291dd1329972160dee814084a02c2f0f9e48/src/DomainModelingInFSharp/FSM Exercises - State machine diagram.png -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/FSM Exercises - Template to work from.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // FSM Exercises: 3 | // 4 | // These are a series of exercises for building finite state machines (FSMs) in F#. 5 | // See "State machine diagram.png" 6 | // 7 | // ================================================ 8 | 9 | 10 | (* 11 | Here is a general template for doing these. 12 | 13 | First, create a union type that represents all the states. 14 | For example, if there are three states called "A", "B" and "C", the type would look like this: 15 | 16 | type State = 17 | | AState 18 | | BState 19 | | CState 20 | 21 | In many cases, each state will need to store some data that is relevant to that state. 22 | So you will need to create types to hold that data as well. 23 | *) 24 | 25 | 26 | type State = 27 | | AState of AStateData 28 | | BState of BStateData 29 | | CState 30 | and AStateData = 31 | {something:int} 32 | and BStateData = 33 | {somethingElse:int} 34 | 35 | 36 | (* 37 | Next, all possible events that can happen are defined in another union type. 38 | If events have data associated with them, add that as well. 39 | *) 40 | 41 | type InputEvent = 42 | | XEvent 43 | | YEvent of YEventData 44 | | ZEvent 45 | and YEventData = 46 | {eventData:string} 47 | 48 | (* 49 | Finally, create a "transition" function that, given a current state and input event, returns a new state. 50 | 51 | 52 | let transition (currentState,inputEvent) = 53 | match currentState,inputEvent with 54 | | AState, XEvent -> // new state 55 | | AState, YEvent -> // new state 56 | | AState, ZEvent -> // new state 57 | | BState, XEvent -> // new state 58 | | BState, YEvent -> // new state 59 | | CState, XEvent -> // new state 60 | | CState, ZEvent -> // new state 61 | 62 | 63 | Forcing yourself to consider every possible combination is thus a helpful design practice. 64 | 65 | Now, even with a small number of states and events, the number of possible combinations gets large very quickly. 66 | To make it more manageable in practice, you should create a series of helper functions, one for each state, like this: 67 | *) 68 | 69 | 70 | let aStateHandler stateData inputEvent = 71 | match inputEvent with 72 | | XEvent -> // new state 73 | | YEvent _ -> // new state 74 | | ZEvent -> // new state 75 | 76 | let bStateHandler stateData inputEvent = 77 | match inputEvent with 78 | | XEvent -> // new state 79 | | YEvent _ -> // new state 80 | | ZEvent -> // new state 81 | 82 | let cStateHandler inputEvent = 83 | match inputEvent with 84 | | XEvent -> // new state 85 | | YEvent _ -> // new state 86 | | ZEvent -> // new state 87 | 88 | (* 89 | And then assembly them into a single transition function 90 | *) 91 | 92 | 93 | let transition (currentState,inputEvent) = 94 | match currentState with 95 | | AState stateData -> 96 | // new state 97 | aStateHandler stateData inputEvent 98 | | BState stateData -> 99 | // new state 100 | bStateHandler stateData inputEvent 101 | | CState -> 102 | // new state 103 | cStateHandler inputEvent 104 | 105 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/Final Exercise - Model your own domain.fsx: -------------------------------------------------------------------------------- 1 | (* 2 | Create types that model a domain that you are expert in. 3 | 4 | Are there any business/domain rules that you can encode in types? 5 | 6 | Do ask for help or advice! 7 | 8 | When you're done, we can do a show and tell! 9 | *) -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/HelloWorld.fsx: -------------------------------------------------------------------------------- 1 | // ================================ 2 | // My first F# program 3 | // ================================ 4 | 5 | printfn "hello world" 6 | 7 | // define a value 8 | let myName = "Scott" 9 | 10 | printfn "my name is %s" myName 11 | 12 | // define a function 13 | let printName myName = 14 | printfn "my name is %s" myName 15 | 16 | // call the function 17 | printName "Scott" 18 | 19 | // define a function 20 | let add x y = 21 | x + y 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/OpaqueApiClient.fs: -------------------------------------------------------------------------------- 1 | module OpaqueApiClient 2 | 3 | //============================ 4 | // This module demonstrates how a client can use a opaque type defined in the previous module. 5 | // 6 | // The client is forced to use only the constructor functions provided 7 | // 8 | //============================ 9 | 10 | // won't compile 11 | let s50_bad = OpaqueApiExample.String50 "123" 12 | 13 | // will compiler 14 | let s50_good = OpaqueApiExample.createString50 "123" 15 | 16 | let email1 = OpaqueApiExample.createEmailAddress "bad" 17 | let email2 = OpaqueApiExample.createEmailAddress "abc@example.com" 18 | 19 | 20 | 21 | // ================================ 22 | // Another quick way of hiding implementation is 23 | // to create a "private" module with your code in 24 | // and then have a "public" module that exposes only 25 | // the functions that you want clients to use. 26 | // ================================ 27 | module _PrivateImplementation = 28 | let privateAdd x y = 29 | x + y 30 | let publicPrintAdd x y = 31 | privateAdd x y |> printfn "%i + %i = %i" x y 32 | 33 | module PublicInterface = 34 | // Documentation for public interface 35 | let printAdd = _PrivateImplementation.publicPrintAdd 36 | 37 | module Client = 38 | open PublicInterface 39 | printAdd 1 2 40 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/OpaqueApiExample.fs: -------------------------------------------------------------------------------- 1 | module OpaqueApiExample 2 | 3 | //============================ 4 | // This module demonstrates how you can hide the implementation of a type 5 | // and force clients to use only the constructor functions you provide 6 | // 7 | // This is the implementation side. 8 | //============================ 9 | 10 | open System.Text.RegularExpressions 11 | 12 | type String50 = String50 of string 13 | type EmailAddress = EmailAddress of string 14 | 15 | let createString50 (s:string) = 16 | if s.Length <= 50 17 | then Some (String50 s) 18 | else None 19 | // val createString50 : s:string -> String50 option 20 | 21 | let createEmailAddress (s:string) = 22 | if Regex.IsMatch(s,@"^\S+@\S+\.\S+$") 23 | then Some (EmailAddress s) 24 | else None 25 | // val createEmailAddress : s:string -> EmailAddress option 26 | 27 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/OpaqueApiExample.fsi: -------------------------------------------------------------------------------- 1 | module OpaqueApiExample 2 | 3 | //============================ 4 | // This module demonstrates how you can hide the implementation of a type 5 | // and force clients to use only the constructor functions you provide 6 | // 7 | // This is the interface side (the signature file) 8 | //============================ 9 | 10 | type String50 11 | type EmailAddress 12 | 13 | val createString50 : s:string -> String50 option 14 | val createEmailAddress : s:string -> EmailAddress option 15 | 16 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/Rop.fsx: -------------------------------------------------------------------------------- 1 | // ============================================== 2 | // This is a utility library for managing Success/Failure results 3 | // 4 | // See http://fsharpforfunandprofit.com/rop 5 | // See https://github.com/swlaschin/Railway-Oriented-Programming-Example 6 | // ============================================== 7 | 8 | /// A Result is a success or failure 9 | /// The Success case has a success value 10 | /// The Failure case has a list of messages 11 | type RopResult<'TSuccess> = 12 | | Success of 'TSuccess 13 | | Failure of string list 14 | 15 | /// create a Success with no messages 16 | let succeed x = 17 | Success x 18 | 19 | /// create a Failure with a message 20 | let fail msg = 21 | Failure [msg] 22 | 23 | /// given a function that generates a new RopResult 24 | /// apply it only if the result is on the Success branch 25 | /// merge any existing messages with the new result 26 | let bindR f result = 27 | match result with 28 | | Success x -> f x 29 | | Failure errors -> Failure errors 30 | 31 | /// given a function wrapped in a result 32 | /// and a value wrapped in a result 33 | /// apply the function to the value only if both are Success 34 | let applyR f result = 35 | match f,result with 36 | | Success f, Success x -> 37 | f x |> Success 38 | | Failure errs, Success _ 39 | | Success _, Failure errs -> 40 | errs |> Failure 41 | | Failure errs1, Failure errs2 -> 42 | errs1 @ errs2 |> Failure 43 | 44 | /// infix version of apply 45 | let (<*>) = applyR 46 | 47 | /// given a function that transforms a value 48 | /// apply it only if the result is on the Success branch 49 | let liftR f result = 50 | let f' = f |> succeed 51 | applyR f' result 52 | 53 | /// given two values wrapped in results apply a function to both 54 | let lift2R f result1 result2 = 55 | let f' = liftR f result1 56 | applyR f' result2 57 | 58 | /// given three values wrapped in results apply a function to all 59 | let lift3R f result1 result2 result3 = 60 | let f' = lift2R f result1 result2 61 | applyR f' result3 62 | 63 | /// given four values wrapped in results apply a function to all 64 | let lift4R f result1 result2 result3 result4 = 65 | let f' = lift3R f result1 result2 result3 66 | applyR f' result4 67 | 68 | /// infix version of liftR 69 | let () = liftR 70 | 71 | /// synonym for liftR 72 | let mapR = liftR 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/Syntax-help-Basic.fsx: -------------------------------------------------------------------------------- 1 | (* 2 | The syntax of F# is different from C-style languages in TWO key ways: 3 | 4 | 1) Curly braces are not used to delimit blocks of code. Instead, indentation is used (like Python). 5 | 2) Whitespace is used to separate parameters rather than commas. 6 | 7 | Other tips: 8 | 9 | * "let" is used instead of "var" 10 | * "let" is also used for defining functions 11 | * "type" is used instead of "class", "enum", etc 12 | * In non-parameter usage, commas are replaced by semicolons in most places. 13 | *) 14 | 15 | 16 | // ================================================ 17 | // Comments 18 | // ================================================ 19 | 20 | 21 | // single line comments use a double slash 22 | (* multi line comments use (* . . . *) pair 23 | 24 | -end of multi line comment- *) 25 | 26 | 27 | 28 | // ================================================ 29 | // Syntax for simple values 30 | // ================================================ 31 | 32 | // The "let" keyword defines an (immutable) value 33 | // Think of "let" being used everywhere you would use "var" in C# 34 | 35 | // try running this 36 | let myInt = 5 37 | let myFloat = 3.14 38 | let myString = "hello" //note that no types needed 39 | let myBool = true 40 | 41 | // pay attention to the output of the interactive window! 42 | // val myInt : int = 5 43 | // val myFloat : float = 3.14 44 | // val myString : string = "hello" 45 | // val myBool : bool = true 46 | // 47 | // the format is always "val" [name] ":" [type] "=" [value] 48 | 49 | 50 | // now try running both these lines at once -- what happens? 51 | let y=2 52 | y=3 53 | 54 | // the answer is that "=" operator in a "let" is not assignment but "binding" -- connecting a name with a value. 55 | // the first time, in "let y=2", y is unknown and is "bound" to "2" 56 | // the second time, in "y=3", y is known and is compared to "3", giving the answer "false" 57 | 58 | 59 | 60 | 61 | // ================================================ 62 | // Printing 63 | // ================================================ 64 | 65 | // The printf/printfn functions are similar to the 66 | // Console.Write/WriteLine functions in C#. 67 | 68 | // Try running this: 69 | printfn "Printing an int %i, a float %f, a bool %b" 1 2.0 true 70 | 71 | // Try running this: 72 | printfn "A string %s, and an F# native structure like a list %A" "hello" [1;2;3;4] 73 | 74 | // There are also sprintf/sprintfn functions for formatting data 75 | // into a string, similar to String.Format in C#. 76 | 77 | // Try running this: 78 | let msg = sprintf "The message is %s" "hello" 79 | 80 | 81 | // ================================================ 82 | // Part 2 - Welcome to the world of F# compiler errors 83 | // ================================================ 84 | 85 | // F# has a lot more compiler errors than you might be used to. 86 | // It's important that you don't get frustrated with them, so it helps to see the common ones! 87 | 88 | // IMPORTANT ints and floats are not compatible. Also bytes, shorts, etc. 89 | let addIntAndFloat = add 1 1.0 90 | 91 | // "int" is a cast that can be used to fix this 92 | let addIntAndFloat2 = add 1 (int 1.0) 93 | 94 | // "float" is a another cast 95 | let addIntAndFloat3 = (float 1) + 1.0 96 | 97 | // "string" is a another cast 98 | let addIntAndString = (string 1) + "hello" 99 | 100 | 101 | // Will this function compile? If not, then why not? 102 | // Can you fix it so that it does compile? 103 | 104 | //let function2a x = 105 | // printfn "x is %f" x 106 | // x + 1 107 | 108 | // will this function compile? If not, then why not? 109 | // Can you fix it so that it does compile? 110 | //let function2b x y = 111 | // printfn "x is %s" x 112 | // printfn "y is %i" y 113 | // x + y 114 | 115 | 116 | // Indentation errors are another common error 117 | 118 | 119 | (* 120 | // uncomment and fix this code to make it compile 121 | let functionWithIndentationError x = 122 | let y = 1 123 | let z = 2 124 | x + y + z 125 | *) 126 | 127 | // Forgetting to have a return value is also a common error. 128 | 129 | (* 130 | // uncomment and fix this code to make it compile 131 | let functionWithNoReturn x = 132 | let y = 1 133 | let z = 2 134 | *) 135 | 136 | 137 | // ================================================ 138 | // Part 3 - Type annotations 139 | // ================================================ 140 | 141 | // When dealing with OO code, such as the .NET libraries, 142 | // F# often cannot know the type it is dealing with, you will need to help it out. 143 | 144 | // Run this function definition. It fails with "Lookup on object of indeterminate type" 145 | // because the compiler does not know what type "s" is! 146 | (* 147 | let getLength s = s.Length 148 | *) 149 | 150 | // You can add "type annotations" in the form "(param:type)" 151 | // to help the compiler 152 | // 153 | // IMPORTANT: The type annotations are "backwards" compared to C#. 154 | // Rather than "string name", the parameter is declared as "name:string" 155 | 156 | let getStrLength (s:string) = s.Length 157 | 158 | // Now that the compiler knows that "s" is a string, it can look up the "Length" method and know that it returns an int. 159 | // So the output of the interactive window says: 160 | // val getStrLength : s:string -> int 161 | 162 | 163 | // You can't use OO-style polymorphism in functional programming. 164 | 165 | // Functions with different types must have different names! 166 | // So a function that works on arrays has to be named differently. 167 | let getArrayLength (s:int[]) = s.Length 168 | 169 | // The output of the interactive window now says 170 | // val getArrayLength : s:int[] -> int 171 | 172 | // To annotate the *return* type of the function, rather than a parameter, 173 | // put the annotation after all the parameter, and not in parentheses, like this: 174 | 175 | let addTwoFloats f1 f2 :float = f1 + f2 176 | 177 | // The return type can affect the types that are inferred for the parameters. 178 | // What types are "s1" and "s2" going to be now? 179 | let addTwoStrings s1 s2 :string = s1 + s2 180 | 181 | 182 | // Exercise -- create a wrapper function for StreamReader. 183 | // What is the minimum annotation you need? 184 | (* 185 | let openFile path = new System.IO.StreamReader(path) 186 | *) 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | // ================================================ 197 | // Part 10 - Everything is an expression 198 | // ================================================ 199 | 200 | // There are no "statements" in F#. Everything is an expression that can be assigned to a value. 201 | 202 | let printExpression = (printfn "hello") 203 | 204 | let functionExpression = (fun x -> x+1) 205 | 206 | let ifExpression = if true then 1 else 2 207 | 208 | let tryCatchExpression x = 209 | try 210 | 100/x 211 | with 212 | | ex -> 0 213 | 214 | 215 | // For expressions that have multiple branches, all branches must return the *same* type 216 | 217 | // In this example, the "then" branch returns a int but the else branch returns a string, 218 | // which causes a compiler error. 219 | let badIfExpression = 220 | if true then 221 | 1 222 | else 223 | "hello" 224 | 225 | // In this example, the "when" branch returns a unit but the main branch returns a int 226 | // which causes a compiler error. 227 | let badTryCatchExpression x = 228 | try 229 | 100/x 230 | with 231 | | ex -> printfn "error" 232 | 233 | 234 | 235 | // ================================================ 236 | // Part 12 - Pattern Matching 237 | // ================================================ 238 | 239 | // Match..with.. is a supercharged case/switch statement. 240 | let stringPatternMatch = 241 | let x = "a" 242 | match x with 243 | | "a" -> printfn "x is a" 244 | | "b" -> printfn "x is b" 245 | | _ -> printfn "x is something else" // underscore matches anything 246 | 247 | let intPatternMatch = 248 | let x = 1 249 | match x with 250 | | 1 -> printfn "x is 1" 251 | | 2 -> printfn "x is 2" 252 | | _ -> printfn "x is something else" // underscore matches anything 253 | 254 | // each case looks a bit like a lambda, with an arrow after the pattern is matched 255 | // | [choice] -> action 256 | 257 | let tuplePatternMatch = 258 | let x = (1,2) 259 | match x with 260 | | 1,1 -> printfn "x is 1,1" 261 | | 2,_ -> printfn "the first part of x is 2" 262 | | _,2 -> printfn "the second part of x is 2" 263 | | _ -> printfn "x is something else" // underscore matches anything 264 | 265 | 266 | // Exercise - what happens if you leave off the "_" case in "intPatternMatch" above? 267 | 268 | // Exercise - what happens if you put the "_" case FIRST in the list in "intPatternMatch" above? 269 | 270 | 271 | 272 | // ================================================ 273 | // Part 17 - Organizing code with modules 274 | // ================================================ 275 | 276 | // to keep a group of types and functions together 277 | // you can put them in a "module" 278 | 279 | module MyFirstModule = 280 | 281 | type MyType = MyType of string 282 | let myFunction() = 42 283 | 284 | 285 | module MySecondModule = 286 | 287 | // to access code in another module, use a fully qualified name 288 | let result = MyFirstModule.myFunction() 289 | 290 | // this works for .NET library as well 291 | let path = System.IO.Path.Combine("a","b") 292 | 293 | 294 | module MyThirdModule = 295 | 296 | // to bring code from another module into scope, use "open" 297 | open MyFirstModule 298 | let result = myFunction() 299 | 300 | // of 301 | open System.IO 302 | let path = Path.Combine("a","b") 303 | 304 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/Syntax-help-Functions.fsx: -------------------------------------------------------------------------------- 1 |  2 | // ================================================ 3 | // Syntax for Functions 4 | // ================================================ 5 | 6 | // The "let" keyword also defines a named function. 7 | // Note that no parentheses are used! 8 | // Try running this: 9 | let square x = x * x 10 | 11 | // pay attention to the output of the interactive window! 12 | // val square : x:int -> int 13 | // 14 | // format is always "val" [name] ":" [type] 15 | // where type is something like "int -> int" with an arrow in it. 16 | // "int -> int" means the function takes an int as input and returns an int as output 17 | 18 | // Try running this: 19 | square 3 // Now run the function. Again, no parens. 20 | 21 | // pay attention to the output of the interactive window! 22 | // val it : int = 9 23 | // This result is NOT a function 24 | 25 | // A two parameter function is defined in a similar way 26 | // Try running this: 27 | let add x y = x + y // don't use add (x,y)! It means something 28 | // completely different. 29 | 30 | // pay attention to the output of the interactive window! 31 | // val add : x:int -> y:int -> int 32 | // "int -> int -> int" means the function takes an int as input and another int as input and returns an int as output 33 | 34 | // Try running this: 35 | add 2 3 // Now run the function. 36 | 37 | 38 | 39 | // to define a multiline function, just use indents. No semicolons needed. 40 | // Try running this: 41 | let evens list = 42 | let isEven x = x%2 = 0 // Define "isEven" as a sub function 43 | List.filter isEven list // List.filter is a library function 44 | // with two parameters: a boolean function 45 | // and a list to work on 46 | 47 | // pay attention to the output of the interactive window! 48 | // val evens : list:int list -> int list 49 | // "int list -> int list " means the function takes a list of ints as input and returns a list of ints as output 50 | 51 | // Try running this: 52 | evens oneToFive // Now run the function 53 | 54 | 55 | // Important: a function is not run unless you pass a parameter. 56 | // otherwise it is just a value, like an int or string 57 | 58 | // Run the line below and pay attention to the output of the interactive window. 59 | // Do you get a simple value? 60 | square 61 | 62 | // Run the line below and pay attention to the output of the interactive window 63 | // Do you get a simple value? 64 | square 3 65 | 66 | 67 | 68 | // ================================================ 69 | // Returning from a function 70 | // ================================================ 71 | 72 | // IMPORTANT: In F# there is no "return" keyword. A function always 73 | // returns the value of the last expression used. 74 | let doSomething y = 75 | printfn "y is %i" y 76 | 2+2 77 | 78 | // Anything other than the last line must NOT return a value 79 | // you must use "ignore" in these cases 80 | 81 | // Try running this: 82 | let functionWithoutIgnore x = 83 | let y = 1 84 | 2 + 2 // error. "This expression should have type 'unit', but has type 'int'" 85 | x + y 86 | 87 | // Try running this: 88 | let functionWithIgnore x = 89 | let y = 1 90 | ignore (2 + 2) // ok 91 | x + y 92 | 93 | 94 | // Question - why does this function compile without using ignore? 95 | let functionWithPrint x = 96 | let y = 1 97 | printfn "%i" (2 + 2) 98 | x + y 99 | 100 | 101 | // Predict what the signature of the following function will be. Then run it to find out! 102 | // Tip: what is the type of the input? What is the type of the output? 103 | let functionSig1 x = 104 | x + 1 105 | 106 | //let functionSig2 x = 107 | // printfn "x is %f" x 108 | // x 109 | // 110 | 111 | 112 | // ================================================ 113 | // Part 4 - Introducting "piping" 114 | // ================================================ 115 | 116 | // You can use parentheses in the normal way to clarify precedence. In this example, 117 | // do "add1" first, then do "times2" on the result. 118 | let add1ThenMultiply2 y = 119 | let add1 x = x + 1 120 | let times2 x = x * 2 121 | times2 (add1 y) // do "add1" first, then do "times2" 122 | 123 | // test 124 | // add1ThenMultiply2 4 125 | 126 | // BUT in F# it is more idiomatic to "pipe" the output of one operation to the next using "|>" 127 | // Piping data around is very common in F#, similar to UNIX pipes. 128 | let add1ThenMultiply2Piped y = 129 | let add1 x = x + 1 130 | let times2 x = x * 2 131 | y |> add1 |> times2 // y is passed to add1 and the output of that passed to times2 132 | 133 | // test 134 | // add1ThenMultiply2Piped 4 135 | 136 | // another example 137 | let someMorePiping = 138 | let add1 x = x + 1 139 | let times2 x = x * 2 140 | let square x = x * x 141 | let dividedBy2 x = x / 2 142 | 143 | 4 144 | |> add1 145 | |> times2 146 | |> square 147 | |> dividedBy2 148 | 149 | 150 | 151 | 152 | // Existing .NET library methods can be wrapped to make them suitable for piping 153 | 154 | let replace (oldStr:string) newStr (str:string) = 155 | str.Replace(oldStr,newStr) 156 | 157 | let startsWith (pattern:string) (str:string) = 158 | str.StartsWith(pattern) 159 | 160 | // with these in place, we can do nice things like this: 161 | "hello" |> replace "h" "j" 162 | 163 | "hello" |> replace "h" "j" |> startsWith "jell" 164 | 165 | // ----------------------- 166 | // IMPORTANT - if there is more than one parameter, the piped parameter is always the LAST one! 167 | 168 | // so 169 | "hello" |> replace "h" "j" 170 | // is the same as 171 | replace "h" "j" "hello" 172 | 173 | 174 | // Another example: 175 | let add x y = x+y 176 | 177 | add 4 5 178 | // is the same as 179 | 5 |> add 4 180 | 181 | 182 | // Exercise: Think of a number function 183 | // think of a number 184 | // add 1 185 | // square it 186 | // subtract 1 187 | // divide by the number you first thought of 188 | // subtract the number you first thought of 189 | // the answer is 2! 190 | // 191 | // Challenge, write this using a piping model. 192 | // use the code below as a starting point 193 | 194 | let thinkOfANumber numberYouThoughtOf = 195 | let squareIt x = x * x 196 | let add1 x = x + 1 197 | let subtract1 x = x - 1 198 | 199 | numberYouThoughtOf 200 | |> add1 201 | |> squareIt 202 | |> subtract1 203 | // |> what comes here? 204 | 205 | // Exercise: given this function: 206 | let subtract a b = a - b 207 | 208 | // predict the result of 1 |> subtract 2 209 | // predict the result of 2 |> subtract 1 210 | 211 | // REMEMBER if there is more than one parameter, the piped parameter is always the LAST one! 212 | // Should you rename the function to make it more sensible when used with piping? 213 | 214 | 215 | // ================================================ 216 | // Part 9 - Lambdas 217 | // ================================================ 218 | 219 | // A "lambda" is an "anonymous function" or "inline function". 220 | 221 | // In C# it is written with a double arrow like this: 222 | // aValue => aValue + 1 223 | 224 | // In F# it is written with a "fun" keyword and a *single" arrow like this: 225 | // fun aValue -> aValue + 1 226 | 227 | // A simple example 228 | 4 |> (fun x -> x + 1) // try running this 229 | 230 | // lambdas are often used with list functions 231 | [1..10] |> List.map (fun x -> x*x) // try running this 232 | 233 | // lambdas can be assigned to values and used like a normal function 234 | 235 | // "add1" version 1 236 | let add1 x = x+1 237 | 4 |> add1 238 | 239 | // "add1" version 2 240 | 4 |> fun x -> x+1 241 | 242 | // "add1" version 3 243 | let add1_lambda = fun x -> x+1 244 | 4 |> add1_lambda 245 | 246 | // "map square" version 1 247 | let square x = x*x 248 | [1..10] |> List.map square 249 | 250 | // "map square" version 2 251 | [1..10] |> List.map (fun x -> x*x) 252 | 253 | // "map square" version 3 254 | let square_lambda = fun x -> x*x 255 | [1..10] |> List.map square_lambda 256 | 257 | 258 | // Exercise - what is the difference between these three definitions? 259 | let add_v1 = 260 | fun x y -> x + y 261 | 262 | let add_v2 x = 263 | fun y -> x + y 264 | 265 | let add_v3 x y = 266 | x + y 267 | 268 | // try running each one of them 269 | add_v1 2 3 270 | add_v2 2 3 271 | add_v3 2 3 -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/Syntax-help-Generics.fsx: -------------------------------------------------------------------------------- 1 |  2 | 3 | // ================================================ 4 | // Part 8. Special Types - Automatic generalization 5 | // ================================================ 6 | 7 | // What is the type for this? 8 | let same x = x 9 | 10 | // It works for *any* type, so F# compiler *automatically* generalizes it to a generic type. 11 | // Generic types have a letter and a tick in front, like 'a and 'b. 12 | // 13 | // The C# equivalent would be 14 | // T Same(T x) { return x; } 15 | 16 | // So what is the type for? 17 | let makeTuple x y = (x,y) 18 | 19 | // And what is the type for this? 20 | List.length 21 | 22 | 23 | // Exercise 8a - predict the type of this function 24 | let example8a x = (x,x) 25 | 26 | // Exercise 8b - predict the type of this function 27 | let example8b x = [x] 28 | 29 | // Exercise 8c - predict the type of this function 30 | let example8c x y = (y,x) 31 | 32 | // Exercise 8d - predict the type of this function 33 | let example8d x y = (y,x+1) 34 | 35 | // Exercise 8e - predict the type of this function 36 | let example8e f x = 37 | f x 38 | 39 | // Exercise 8f - predict the type of this function 40 | let example8f f x = 41 | x |> f |> f 42 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/Syntax-help-Lists.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // Syntax for List literals 3 | // ================================================ 4 | 5 | // NOTE: In non-parameter usage, F# uses semicolons where C# uses commas. 6 | // So for example, list literals use semicolons rather than commas. 7 | 8 | // try running this: 9 | let twoToFive = [2;3;4;5] // Square brackets create a list with 10 | // semicolon delimiters. 11 | 12 | // notice how the interactive output changes 13 | // val twoToFive : int list = [2; 3; 4; 5] 14 | 15 | // try running this: 16 | let oneToFive = 1 :: twoToFive // :: creates list with new 1st element 17 | 18 | // The result is [1;2;3;4;5] 19 | // val oneToFive : int list = [1; 2; 3; 4; 5] 20 | 21 | // try running this: 22 | let zeroToFive = [0;1] @ twoToFive // @ concats two lists 23 | 24 | // The result is 25 | // val zeroToFive : int list = [0; 1; 2; 3; 4; 5] 26 | 27 | // IMPORTANT: commas are never used as delimiters, only semicolons! 28 | 29 | // you can also use "m..n" syntax for a range 30 | let oneToTen = [1..10] // try running this 31 | 32 | // you can also use list comprehension. Try running this: 33 | let twoToTwenty = [for i in 1..10 do yield i*2 ] 34 | let twoToTen = [for i in 1..10 do if i%2=0 then yield i] 35 | 36 | // ================================================ 37 | // Part 5 - List functions - map, filter, sortBy 38 | // ================================================ 39 | 40 | // There are some very useful functions that work on lists. 41 | 42 | // They are very similar to the LINQ ones 43 | // List.map similar to LINQ.Select 44 | // List.filter similar to LINQ.Where 45 | // List.sortBy similar to LINQ.OrderBy 46 | // etc 47 | 48 | // They normally have two parameters: a function that acts on each item, and the list itself. 49 | // List.map [selectFunction] [listToOperateOn] 50 | // List.filter [whereFunction] [listToOperateOn] 51 | // List.sortBy [orderByFunction] [listToOperateOn] 52 | 53 | // In this example, the "selectFunction" is "add1" and the listToOperateOn is "1..10" 54 | let add1 x = x + 1 55 | List.map add1 [1..10] 56 | 57 | // In this example, do "map" first, with two args, then do "sum" on the result. 58 | let sumOfSquaresTo100_v1 = 59 | let square x = x * x 60 | let squares = List.map square [1..100] 61 | List.sum squares 62 | 63 | // The List.map could be inlined, but it needs parentheses! 64 | // Without the parens, "List.map" would be passed as an arg to List.sum 65 | let sumOfSquaresTo100_v2 = 66 | let square x = x * x 67 | List.sum ( List.map square [1..100] ) 68 | 69 | // Generally though, list operations are written using pipes, which makes them easier to read. 70 | 71 | // Here is the same sumOfSquares function written using pipes 72 | let sumOfSquaresTo100_piped = 73 | let square x = x * x 74 | [1..100] |> List.map square |> List.sum // "square" was defined earlier 75 | 76 | 77 | // And here are other examples of List functions. 78 | // Run each one in turn and make sure you understand what is happening. 79 | let add1 x = x + 1 80 | [1..10] |> List.map add1 81 | 82 | let square x = x * x 83 | [1..10] |> List.map square 84 | 85 | let isEven x = x % 2 = 0 86 | [1..10] |> List.filter isEven 87 | 88 | let isGreaterThan5 x = x > 5 89 | [1..10] |> List.filter isGreaterThan5 90 | 91 | let negative x = -x 92 | [1..10] |> List.sortBy negative 93 | 94 | // sort by boolean result 95 | [1..10] |> List.sortBy isEven 96 | 97 | // we can also use the string wrapper functions we created earlier in conjunction with the list functions 98 | ["hello"; "goodbye"] |> List.filter (startsWith "h") 99 | ["hello"; "goodbye"] |> List.map (replace "o" "-") 100 | 101 | // other useful functions are "length" and "head" (first element) and "sum" and "average" 102 | [1..10] |> List.head 103 | [1..10] |> List.sortBy negative |> List.head 104 | [1..10] |> List.filter isGreaterThan5 |> List.head 105 | 106 | [1..10] |> List.filter isEven |> List.length 107 | [1..10] |> List.filter isGreaterThan5 |> List.length 108 | 109 | [1..10] |> List.sum 110 | 111 | [1..10] |> List.map float |> List.average // average only works with floats. How did I convert the ints to floats? 112 | 113 | 114 | // Exercise: Use List.filter find strings that contain a "h" 115 | 116 | (* 117 | ["alice"; "bob"; "hello"; "hi"] |> List.filter what?? 118 | *) 119 | 120 | // Follow up: Use List.filter find strings that contain both an "h" and an "o" 121 | 122 | 123 | 124 | // ================================================ 125 | // Part 13 - Pattern Matching with Lists 126 | // ================================================ 127 | 128 | // Lists have their own pattern matching syntax: 129 | 130 | // [] matches an empty list 131 | // [x] matches a list with exactly one element 132 | // [x;y] matches a list with exactly two elements 133 | 134 | // first::rest matches a list at least one element. 135 | // "first" is bound to the first element 136 | // "rest" is bound to the the rest of the list (which might be empty) 137 | 138 | let listMatchingExample aList = 139 | match aList with 140 | | [] -> printfn "the list is empty" 141 | | [x] -> printfn "the list has one element and it is %i" x 142 | | first::rest -> 143 | printfn "the list has more than one element and the first element is %i" first 144 | 145 | // try running this 146 | listMatchingExample [] 147 | 148 | // try running this 149 | listMatchingExample [1] 150 | 151 | // try running this 152 | listMatchingExample [1;2] 153 | 154 | // try running this 155 | listMatchingExample [1..10] 156 | 157 | // try running this -- why does it not compile? 158 | listMatchingExample ["hello"] 159 | 160 | 161 | // Exercise - what happens if the "first::rest" case is moved above the "[x]" case in the example? 162 | 163 | // ================================================ 164 | // Part 14 - Recursion with lists 165 | // ================================================ 166 | 167 | // A function can call itself to run the same logic on a smaller set of data. 168 | 169 | let rec listMatchingExampleWithRecursion aList = 170 | match aList with 171 | | [] -> printfn "the list is empty" 172 | | [x] -> printfn "the list has one element and it is %i" x 173 | | first::rest -> 174 | printfn "the list has more than one element and the first element is %i" first 175 | listMatchingExampleWithRecursion rest 176 | 177 | listMatchingExampleWithRecursion [1;2] 178 | listMatchingExampleWithRecursion [1;2;3] 179 | 180 | // Tips: 181 | // * Use the "rec" keyword 182 | // * You often need to pass in some extra parameters to keep track of the state 183 | 184 | // For example, to count the elements of list, we have a list and a sumSoFar 185 | 186 | let rec sumOfElements aList sumSoFar = 187 | match aList with 188 | | [] -> // we're done, return the sumSoFar 189 | sumSoFar 190 | | first::rest -> // we're not done, so take the first and add it to the sumSoFar 191 | let newSumSoFar = first + sumSoFar 192 | let smallerList = rest 193 | sumOfElements smallerList newSumSoFar 194 | 195 | // try running this 196 | let sumSoFar = 0 197 | let list = [1..10] 198 | let sum = sumOfElements list sumSoFar 199 | 200 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/Syntax-help-Options-and-Choices.fsx: -------------------------------------------------------------------------------- 1 |  2 | // ================================================ 3 | // Part 15 - "Option" types 4 | // ================================================ 5 | 6 | // F# doesn't allow nulls by default -- you must use an Option type 7 | // and then pattern match. 8 | // Some(..) and None are roughly analogous to Nullable wrappers 9 | let validValue = Some 99 10 | let invalidValue = None 11 | 12 | // In this example, match..with matches the "Some" and the "None", 13 | // and also unpacks the value in the "Some" at the same time. 14 | let printOption input = 15 | match input with 16 | | Some i -> printfn "input is an int=%d" i 17 | | None -> printfn "input is missing" 18 | 19 | printOption validValue 20 | printOption invalidValue 21 | 22 | // Exercise - Why don't you need a "_" case in addition to Some and None? 23 | 24 | // Exercise - what happens if you leave off the Some case in "printOption" above? 25 | 26 | // Exercise - what happens if you leave off the None case in "printOption" above? 27 | 28 | 29 | // ================================================ 30 | // Part 16 - "Choice" types 31 | // ================================================ 32 | 33 | // Choice types (called "Discriminated Unions") are data structures with a set of choices 34 | // A value must be only *one* of these choices at a time. 35 | 36 | type CardType = 37 | | Visa 38 | | Mastercard 39 | 40 | // They look like C# enums, but unlike enums, they can have data associated with each choice 41 | type PaymentMethod = 42 | | Cash 43 | | Cheque of int 44 | | Card of CardType * string // recognize this type as a tuple? 45 | 46 | 47 | // To create a choice type, use the "constructor" for that choice 48 | 49 | // for enum-style choices, it is simple 50 | let visa = Visa 51 | let mc = Mastercard 52 | 53 | // for choices with extra data, call the constructor, passing in the required data at the same time 54 | 55 | let cheque = Cheque 42 56 | let card = Card (visa,"1234") 57 | 58 | // to get data out, you must pattern matching for each case 59 | let printPayment paymentMethod = 60 | match paymentMethod with 61 | | Cash -> printfn "Paid in cash" 62 | | Cheque checkNo -> printfn "Paid by cheque: %i" checkNo 63 | | Card (cardType,cardNo) -> printfn "Paid with %A %A" cardType cardNo 64 | 65 | 66 | // each case looks a bit like a lambda, with an arrow after the pattern is matched 67 | // | [choice] [associated data] -> action 68 | 69 | // example 70 | let paymentMethod1 = Card(Visa, "1234") 71 | printPayment paymentMethod1 72 | 73 | let paymentMethod2 = Cheque 42 74 | printPayment paymentMethod2 75 | 76 | let paymentMethod3 = Cash 77 | printPayment paymentMethod3 78 | 79 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/Syntax-help-Records.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // Records 3 | // 4 | // Records are data structures with named fields 5 | // 6 | // ================================================ 7 | 8 | 9 | // They are defined with the "type" keyword 10 | type MyRecord = {id:int; name:string} // Try running this 11 | 12 | // they are instantiated in the same way, but using "let" and assigned values for each field 13 | let myRecord = {id=1; name="Alice"} // Try running this 14 | 15 | // you can clone a new record based on an old one using "with" keyword 16 | let myRecord2 = {myRecord with name="Bob"} // Try running this 17 | 18 | // to get data out, you can use pattern matching similar to tuples 19 | let {id=myId; name=myName} = myRecord 20 | 21 | // or you can dot into them as well, if the compiler knows which type you are using 22 | let myName4 = myRecord.name 23 | 24 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/Syntax-help-Tuples.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // Tuples 3 | // 4 | // A tuple is as a pair, a triplet, etc. 5 | // 6 | // ================================================ 7 | 8 | // Tuples are defined with a multiplication symbol 9 | type IntAndInt = int * int 10 | type IntAndString = int * string 11 | 12 | // Tuples are a *single* value, created by combining two (or three) other values 13 | // using a comma. 14 | let myTuple = 1,2 // try running this 15 | 16 | // The components of the tuple can be different types 17 | let intAndStringTuple = 1,"hello" // try running this 18 | let intAndBoolTuple = 1,false // try running this 19 | let intAndStringAndBoolTuple = 1,"hello",true // try running this 20 | 21 | 22 | // pay attention to the output of the interactive window! 23 | // Tuple types have a "*" between each type, as if the types were being "multiplied" together! 24 | 25 | // val myTuple : int * int = (1, 2) 26 | // val intAndStringTuple : int * string = (1, "hello") 27 | // val intAndBoolTuple : int * bool = (1, false) 28 | // val intAndStringAndBoolTuple : int * string * bool = (1, "hello", true) 29 | 30 | // Tuples can be deconstructed in the same way that they are constructed: 31 | 32 | let tuple1 = 1,"hello" // try running this 33 | let x1,y1 = tuple1 // run this to deconstruct the tuple 34 | 35 | let tuple2 = 1,false,"hello" // try running this 36 | let x2,y2,z2 = tuple2 // run this to deconstruct the tuple 37 | 38 | // You can ignore values you don't care about with "_" 39 | let tuple3 = "hello",42 // try running this 40 | let _,theAnswer = tuple3 // run this to deconstruct the tuple 41 | 42 | // You can't mix tuples with different sizes and different types, 43 | // as you will get a compiler error! 44 | 45 | let tuple4 = 1,false // try running this 46 | let x4,y4,z4 = tuple4 // run this to deconstruct the tuple 47 | 48 | // If tuples have the same size and types, then equality is defined automatically 49 | let tuple5a = 1,"hello" 50 | let tuple5b = 1,"hello" 51 | let isTuple5bEqual = (tuple5a = tuple5b) 52 | 53 | let tuple5c = 1,"goodbye" 54 | let isTuple5cEqual = (tuple5a = tuple5c) 55 | 56 | // But again, you can't mix tuples with different sizes and different types, 57 | // as you will get a compiler error! 58 | let tuple5d = 2,42 59 | let isTuple5dEqual = (tuple5a = tuple5d) 60 | 61 | let tuple5e = 1,"hello",42 62 | let isTuple5eEqual = (tuple5a = tuple5e) 63 | 64 | // IMPORTANT tuples are one value not two 65 | // Look at the signature of this function: 66 | let addTuple (x,y) = x + y 67 | // it is: 68 | // val addTuple : x:int * y:int -> int 69 | // Note that it has only *one* parameter! 70 | // The input is of type "int * int" 71 | // The output is of type "int" 72 | 73 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/Syntax-help-Unit-type.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // Part 6. Special Types - unit 3 | // ================================================ 4 | 5 | // F# has some special types that might be new to you: unit, tuples, and automatic generics. 6 | 7 | // Let's start with the "unit" type. 8 | 9 | // The unit type is similar to "void" and is used for inputs and outputs that have no value. 10 | // The type "unit" has one value, written as "()" 11 | 12 | // Try running this: 13 | let myUnit = () 14 | 15 | // pay attention to the output of the interactive window 16 | // val myUnit : unit = () 17 | 18 | // "unit" is the type. "()" is the value, 19 | 20 | 21 | 22 | // Try running this: 23 | let myUnit2 = printfn "The printfn returns nothing, so unit is used" 24 | 25 | // pay attention to the output of the interactive window 26 | // val myUnit2 : unit = () 27 | // The result of printfn is nothing 28 | 29 | 30 | // This function has an input but no output. Try running it: 31 | let unitOutput x = printfn "x=%i" x 32 | // pay attention to the output of the interactive window 33 | // val unitOutput : x:int -> unit 34 | 35 | // This function has an output but no input. Try running it: 36 | let unitInput() = 1 37 | // pay attention to the output of the interactive window 38 | // val unitInput : unit -> int 39 | 40 | 41 | // Predict what the signature of this will be. 42 | // Also, predict whether "hello" will be printed immediately 43 | let example6a = 44 | printfn "hello" 45 | 46 | // Predict what the signature of this will be. 47 | // Also, predict whether "hello" will be printed immediately 48 | let example6b() = 49 | printfn "hello" 50 | 51 | // Predict what the signature of this will be. 52 | // Also, predict whether "hello" will be printed immediately 53 | let example6c = 54 | printfn "hello" 55 | 1 56 | 57 | // Predict what the signature of this will be. 58 | // Also, predict whether "hello" will be printed immediately 59 | let example6d() = 60 | printfn "hello" 61 | 1 62 | 63 | // Predict what happens if I evaluate "example6a" twice. 64 | // Also, predict whether "hello" will be printed or not. 65 | // (Try running the code below) 66 | example6a 67 | example6a 68 | 69 | // Predict what happens if I evaluate "example6b" twice. 70 | // Also, predict whether "hello" will be printed or not. 71 | example6b 72 | example6b 73 | 74 | // Predict what happens if I evaluate "example6b()" twice. 75 | // Also, predict whether "hello" will be printed or not. 76 | example6b() 77 | example6b() 78 | 79 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/Validation - Domain input and output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swlaschin/DomainModellingInFsharp/ed21291dd1329972160dee814084a02c2f0f9e48/src/DomainModelingInFSharp/Validation - Domain input and output.png -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/Validation 1 - Roman Numerals.fsx: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // Validation -- this demonstrates how to convert untrusted input into a pure domain 3 | // 4 | // ================================================ 5 | 6 | 7 | 8 | // ============================================== 9 | // Set up the core, pure domain 10 | // ============================================== 11 | 12 | module RomanNumeralDomain = 13 | 14 | type RomanNumeral = RomanNumeral of RomanDigit list 15 | and RomanDigit = I | IV | V | IX | X | XC | IC | C 16 | // This is guaranteed clean and pure! 17 | // I cannot have a bad value in the domain 18 | 19 | 20 | // ============================================== 21 | // Set up a service that interacts with the outside world 22 | // ============================================== 23 | 24 | module RomanNumeralService = 25 | open RomanNumeralDomain 26 | 27 | /// Convert a string like "CXCVII" into a RomanNumeral 28 | let toRomanNumeral (input:string) :RomanNumeral = 29 | // loop through the ASCII chars, accumulating into a list RomanDigits as we go 30 | let rec loopThroughChars (chars:char list) (digits:RomanDigit list) = 31 | match chars with 32 | | [] -> 33 | // End of list. Return the accumulated list of RomanDigits 34 | // (but has to be reversed first!) 35 | digits |> List.rev 36 | | 'i'::'v'::rest -> 37 | let newDigits = IV :: digits 38 | loopThroughChars rest newDigits 39 | | 'i'::'x'::rest -> 40 | let newDigits = IX :: digits 41 | loopThroughChars rest newDigits 42 | | 'i'::'c'::rest -> 43 | let newDigits = IC :: digits 44 | loopThroughChars rest newDigits 45 | | 'x'::'c'::rest -> 46 | let newDigits = XC :: digits 47 | loopThroughChars rest newDigits 48 | | 'i'::rest -> 49 | let newDigits = I::digits 50 | loopThroughChars rest newDigits 51 | | 'v'::rest -> 52 | let newDigits = V::digits 53 | loopThroughChars rest newDigits 54 | | 'x'::rest -> 55 | let newDigits = X::digits 56 | loopThroughChars rest newDigits 57 | | 'c'::rest -> 58 | let newDigits = C::digits 59 | loopThroughChars rest newDigits 60 | 61 | // convert the input into a list of chars 62 | let chars = input.ToLower().ToCharArray() |> List.ofArray 63 | 64 | // loop through the list of chars, returning a list of Digits 65 | let digits = loopThroughChars chars [] 66 | 67 | // wrap the digits in a RomanNumeral 68 | RomanNumeral digits 69 | 70 | /// Convert a RomanNumeral into a number 71 | let toInt (input:RomanNumeral) :int = 72 | 73 | let rec loopThroughDigits (digits:RomanDigit list) (sum:int)= 74 | match digits with 75 | | [] -> sum 76 | | I::rest -> loopThroughDigits rest (sum+1) 77 | | IV::rest -> loopThroughDigits rest (sum+4) 78 | | V::rest -> loopThroughDigits rest (sum+5) 79 | | IX::rest -> loopThroughDigits rest (sum+9) 80 | | X::rest -> loopThroughDigits rest (sum+10) 81 | | XC::rest -> loopThroughDigits rest (sum+90) 82 | | IC::rest -> loopThroughDigits rest (sum+99) 83 | | C::rest -> loopThroughDigits rest (sum+100) 84 | 85 | let (RomanNumeral digits) = input 86 | let sum = loopThroughDigits digits 0 87 | sum 88 | 89 | 90 | // ============================================== 91 | // test 92 | // ============================================== 93 | 94 | open RomanNumeralDomain 95 | 96 | // good cases 97 | "cxciii" |> RomanNumeralService.toRomanNumeral 98 | 99 | RomanNumeral [C;XC;I;I;I] |> RomanNumeralService.toInt 100 | 101 | 102 | // bad case 103 | "cxa" |> RomanNumeralService.toRomanNumeral 104 | 105 | 106 | // ============================================== 107 | // Version 2 - Set up a service with error handling 108 | // ============================================== 109 | 110 | module RomanNumeralService_V2 = 111 | open RomanNumeralDomain 112 | 113 | type RomanNumeralResult = 114 | | Success of RomanNumeral 115 | | Failure of string 116 | 117 | /// Convert a string like "CXCVII" into a RomanNumeral OR error 118 | let toRomanNumeral (input:string) :RomanNumeralResult = 119 | let rec loopThroughChars (chars:char list) (digits:RomanDigit list) :RomanNumeralResult = 120 | match chars with 121 | | [] -> 122 | digits |> List.rev |> RomanNumeral |> Success 123 | | 'i'::'v'::rest -> 124 | let newDigits = IV :: digits 125 | loopThroughChars rest newDigits 126 | | 'i'::'x'::rest -> 127 | let newDigits = IX :: digits 128 | loopThroughChars rest newDigits 129 | | 'i'::'c'::rest -> 130 | let newDigits = IC :: digits 131 | loopThroughChars rest newDigits 132 | | 'x'::'c'::rest -> 133 | let newDigits = XC :: digits 134 | loopThroughChars rest newDigits 135 | | 'i'::rest -> 136 | let newDigits = I::digits 137 | loopThroughChars rest newDigits 138 | | 'v'::rest -> 139 | let newDigits = V::digits 140 | loopThroughChars rest newDigits 141 | | 'x'::rest -> 142 | let newDigits = X::digits 143 | loopThroughChars rest newDigits 144 | | 'c'::rest -> 145 | let newDigits = C::digits 146 | loopThroughChars rest newDigits 147 | | badChar::_ -> 148 | let errMsg = sprintf "Error parsing '%s': '%c' is not a valid roman digit" input badChar 149 | errMsg |> Failure 150 | 151 | let chars = input.ToLower().ToCharArray() |> List.ofArray 152 | let result = loopThroughChars chars [] 153 | result 154 | 155 | 156 | // ============================================== 157 | // test again 158 | // ============================================== 159 | 160 | 161 | // good cases 162 | "cxciii" |> RomanNumeralService_V2.toRomanNumeral 163 | 164 | // bad case 165 | "cxa" |> RomanNumeralService_V2.toRomanNumeral 166 | -------------------------------------------------------------------------------- /src/DomainModelingInFSharp/Validation 2 - Contact.fsx: -------------------------------------------------------------------------------- 1 | // if using in Visual Studio, this helps to set the current directory correctly 2 | System.IO.Directory.SetCurrentDirectory __SOURCE_DIRECTORY__ 3 | 4 | // load the Railway Oriented Programming utility library 5 | // See http://fsharpforfunandprofit.com/rop 6 | // See https://github.com/swlaschin/Railway-Oriented-Programming-Example 7 | #load "Rop.fsx" 8 | 9 | 10 | // ============================================== 11 | // Set up the primitive types: String10, EmailAddress, etc 12 | // ============================================== 13 | 14 | module PrimitiveTypes = 15 | 16 | // ------------------------------ 17 | // String10 18 | 19 | // NOTE: this type would normally be opaque 20 | type String10 = String10 of string 21 | 22 | // Create a String10 from a string. 23 | // Pass in an error message to use in case of error 24 | let createString10 errorStr (s:string) = 25 | match s with 26 | | null -> Rop.fail errorStr 27 | | _ when s.Length > 10 -> Rop.fail errorStr 28 | | _ -> Rop.succeed (String10 s) 29 | 30 | // apply a function to the contents of the String10 31 | let applyString10 f (String10 s) = f s 32 | 33 | // ------------------------------ 34 | // EmailAddress 35 | 36 | // NOTE: this type would normally be opaque 37 | type EmailAddress = EmailAddress of string 38 | 39 | // Create a EmailAddress from a string 40 | let createEmailAddress (s:string) = 41 | match s with 42 | | null -> 43 | Rop.fail "Email must not be null" 44 | | _ when s.Length > 20 -> 45 | Rop.fail "Email must not be more than 20 chars" 46 | | _ -> 47 | if s.Contains("@") then 48 | Rop.succeed (EmailAddress s) 49 | else 50 | Rop.fail "Email must contain @ sign" 51 | 52 | // apply a function to the contents of the EmailAddress 53 | let applyEmailAddress f (EmailAddress s) = f s 54 | 55 | // ------------------------------ 56 | // ContactId 57 | 58 | // NOTE: this type would normally be opaque 59 | type ContactId = ContactId of int 60 | 61 | // Create a ContactId from an int 62 | let createContactId (i: int) = 63 | if i < 1 then 64 | Rop.fail "ContactId must be positive integer" 65 | else 66 | Rop.succeed (ContactId i) 67 | 68 | // apply a function to the contents of the ContactId 69 | let applyContactId f (ContactId s) = f s 70 | 71 | // ============================================== 72 | // Set up the domain level types: PersonalName, Contact 73 | // ============================================== 74 | 75 | module ContactDomain = 76 | open PrimitiveTypes 77 | 78 | // NOTE: these types do NOT have to be opaque 79 | type PersonalName = { 80 | FirstName: String10 81 | LastName: String10 82 | } 83 | 84 | type Contact = { 85 | Id: ContactId 86 | Name: PersonalName 87 | Email: EmailAddress 88 | } 89 | 90 | let createFirstName firstName = 91 | let errMsg = "First name is required and must be less than 10 chars" 92 | createString10 errMsg firstName 93 | 94 | let createLastName lastName = 95 | let errMsg = "Last name is required and must be less than 10 chars" 96 | createString10 errMsg lastName 97 | 98 | let createPersonalName firstName lastName = 99 | {FirstName = firstName; LastName = lastName} 100 | 101 | let createContact custId name email = 102 | {Id = custId; Name = name; Email = email} 103 | 104 | 105 | // ============================================== 106 | // Set up the DTO types: PersonalName, Contact 107 | // ============================================== 108 | 109 | module ContactDTO = 110 | open PrimitiveTypes 111 | open ContactDomain 112 | 113 | 114 | /// Represents a DTO that is exposed on the wire. 115 | /// This is a regular POCO class which can be null. 116 | /// To emulate the C# class, all the properties are initialized to null by default 117 | /// 118 | /// Note that in F# you have to make quite an effort to create nullable classes with nullable fields 119 | [] 120 | type ContactDto() = 121 | member val Id = 0 with get, set 122 | member val FirstName : string = null with get, set 123 | member val LastName : string = null with get, set 124 | member val Email : string = null with get, set 125 | 126 | /// Convert a domain Contact into a DTO. 127 | /// There is no possibility of an error 128 | /// because the Contact type has stricter constraints than DTO. 129 | let contactToDto(cust:Contact) = 130 | // extract the raw int id from the ContactId wrapper 131 | let custIdInt = cust.Id |> applyContactId id 132 | 133 | // create the object and set the properties 134 | let contactDto = ContactDto() 135 | contactDto.Id <- custIdInt 136 | contactDto.FirstName <- cust.Name.FirstName |> applyString10 id 137 | contactDto.LastName <- cust.Name.LastName |> applyString10 id 138 | contactDto.Email <- cust.Email |> applyEmailAddress id 139 | contactDto 140 | 141 | /// Convert a DTO into a domain contact. 142 | /// 143 | /// We MUST handle the possibility of one or more errors 144 | /// because the Contact type has stricter constraints than ContactDto 145 | /// and the conversion might fail. 146 | let dtoToContact (dto: ContactDto) = 147 | if dto = null then 148 | Rop.fail "Contact is required" 149 | else 150 | // This is an example of the power of composition! 151 | // Each step returns a value OR an error. 152 | // These are then gradually combined to make bigger things, all the while preserving any errors 153 | // that happen. 154 | 155 | // if the id is not valid, the createContactId function will return a Failure 156 | // hover over idOrError and you can see it has type RopResult rather than just ContactId 157 | let idOrError = createContactId dto.Id 158 | 159 | // similarly for first and last name 160 | let firstNameOrError = createFirstName dto.FirstName 161 | let lastNameOrError = createLastName dto.LastName 162 | 163 | // the "createPersonalName" functions takes normal inputs, not inputs with errors, 164 | // but we can use the "lift" function to convert it into one that does handle error input 165 | // the output has also changed from a normal name to one with errors 166 | let personalNameOrError = Rop.lift2R createPersonalName firstNameOrError lastNameOrError 167 | 168 | // similarly try to make an email 169 | let emailOrError = createEmailAddress dto.Email 170 | 171 | // finally add them all together to make a contacts 172 | // the "createContact" takes three params, so use lift3 to convert it 173 | let contactOrError = Rop.lift3R createContact idOrError personalNameOrError emailOrError 174 | contactOrError 175 | 176 | // The code above is very explicit and was designed for beginners to understand. 177 | // Below is a more idiomatic version which uses the and <*> operators rather than "lift". 178 | // 179 | // The and <*> operators make it look complicated, but in fact it is always the same pattern. 180 | // is used for the first param 181 | // <*> is used for the subsequent params 182 | // 183 | // so for example: 184 | // existingFunction firstParam <*> secondParam <*> thirdParam 185 | let () = Rop.() 186 | let (<*>) = Rop.(<*>) 187 | 188 | let dtoToContactIdiomatic (dto: ContactDto) = 189 | if dto = null then 190 | Rop.fail "Contact is required" 191 | else 192 | let contactIdOrError = 193 | createContactId dto.Id 194 | 195 | let nameOrError = 196 | createPersonalName 197 | createFirstName dto.FirstName 198 | <*> createLastName dto.LastName 199 | 200 | createContact 201 | contactIdOrError 202 | <*> nameOrError 203 | <*> createEmailAddress dto.Email //inline this one 204 | 205 | // ============================================== 206 | // examples 207 | // ============================================== 208 | open ContactDomain 209 | open ContactDTO 210 | 211 | let goodDto = ContactDto(Id=1,FirstName="Alice",LastName="Adams",Email="me@example.com") 212 | goodDto |> ContactDTO.dtoToContact 213 | 214 | let badDto = ContactDto(Id=0,FirstName=null,LastName="Adams",Email="xample.com") 215 | badDto |> ContactDTO.dtoToContact 216 | --------------------------------------------------------------------------------