I have become obsessed!
I have been working with C# for nearly 7 years already! In my reading around the internet, I had heard of the F# language. I also heard of functional programming. I have been using reactive rx extensions for a while, mainly through ReactiveUI. But finally I looked seriously at F#. It wasn’t long and it had me hooked.
I think it was a combination of the ease at which you can write functional code and the less you have to type to do the same thing as you would in C#. I admit It took some time for me to begin to get my head around it. I still have a long way to go. But there is no better way to learn by doing and then by telling others about it is there? Please be gentle on me! I am still a noob at this stuff, but I hope these articles will be helpful to someone along the way.
Track Your Time!
I decided to call the application “Track Your Time” or “TrackTime” for short. After much reading and digging arround I settled on creating a MVU application using FuncUI which sits on top of AvaloniaUI. AvaloniaUI is a framework I had begun to play with previously. This is an application that I need for my own purposes for billing those customers that I do stuff on an hourly rate and to track the time I spend on fixed-price contract projects that I do (To hopefully improve my quotes).
For the database I chose to use FirebirdSQL which I have loved from my Delphi days. It is still a very good database and allows me to embed the engine with the application for a single user – zero configuration installation if I want to. In this first iteration, the application will work for a single user, which is all I need. I have open sourced the application with an MIT licence, If any other geeks can use it, ( with mayber a little thank you gift from time to time). The repository is hosted on gitub.
To access the Firebird databaseI decided to go with DonaldSQL , which could be described as Dapper type library for F#, although you can very successfully use Dapper with F#.
So lets look at the stucture of the solution.
For now ignore the first project. ‘TrackTime.Core’ and ‘TrackTime’ are the main projects. I have some unit tests using ‘Expecto’ in the test project. So let us look at the models. This is in DataModels.fs. I have tried to use ‘Type Driven Design’ techniques to make invalid states un-representable. I shall link some of my go-to web sites for F# stuff at the end of this article. Let’s take a look at the types to represent an email address and a phone number. These are single case discriminated unions.:
namespace TrackTime
open System
open System.ComponentModel.Design
open System
module DataModels =
let CustomerNameLength = 50
let CustomerEmailLength = 100
let CustomerPhoneNoLength = 20
let WorkItemTitleLength = 50
let WorkItemDescriptionLength = 100
let TimeEntryDescriptionLength = 100
type EmailAddressOptional =
private
| Valid of string option
| Invalid of ErrMsg: string * invalidStr: string
member this.Value =
match this with
| Valid optionalValue -> optionalValue
| _ -> None
override this.ToString() =
match this.Value with
| Some value -> value
| None -> ""
member this.ErrorMsg: string option =
match this with
| Valid _ -> None
| Invalid (errMsg, _) -> Some errMsg
static member None = None |> Valid
static member Create str =
match str with
| None -> None |> Valid
| Some s ->
if String.IsNullOrWhiteSpace s then
None |> Valid
elif (String.length s) > CustomerEmailLength then
($"The email address cannot have more than {CustomerEmailLength} characters", s)
|> Invalid
elif System.Text.RegularExpressions.Regex.IsMatch(s, @"^\S+@\S+\.\S+$") then
s |> Some |> Valid
else
Invalid("Email address must contain an @ sign", s)
member this.IsValidValue =
match this with
| Valid value -> true
| _ -> false
type PhoneNoOptional =
private
| Valid of string option
| Invalid of ErrMsg: string * invalidStr: string
member this.Value =
match this with
| Valid optionalValue -> optionalValue
| _ -> None
override this.ToString() =
match this.Value with
| Some value -> value
| None -> ""
member this.ErrorMsg: string option =
match this with
| Valid _ -> None
| Invalid (errMsg, _) -> Some errMsg
static member None = None |> Valid
static member Create str =
match str with
| None -> None |> Valid
| Some s ->
if String.IsNullOrWhiteSpace s then
None |> Valid
elif (String.length s) > CustomerPhoneNoLength then
($"The phone no cannot have more than {CustomerPhoneNoLength} characters", s)
|> Invalid
else
s |> Some |> Valid
member this.IsValidValue =
match this with
| Valid value -> true
| _ -> false
And then my Name type which is required:
type CustomerName =
private
| Valid of string
| Invalid of ErrMsg: string * invalidStr: string
member this.Value =
match this with
| Valid value -> value
| _ -> ""
override this.ToString() = this.Value
member this.ErrorMsg: string option =
match this with
| Valid _ -> None
| Invalid (errMsg, _) -> Some errMsg
static member Create s =
if String.IsNullOrWhiteSpace s then
Invalid("A name is required.", s)
elif (String.length s) > CustomerNameLength then
Invalid($"A name cannot have more than {CustomerNameLength} characters", s)
else
Valid s
member this.IsValidValue =
match this with
| Valid value -> true
| _ -> false
I have used similar types for customer description, work item title etc. This leads me to my customer record domain type:
type CustomerState =
| InActive = 0
| Active = 1
type Customer =
{ CustomerId: CustomerId
Name: CustomerName
Phone: PhoneNoOptional
Email: EmailAddressOptional
CustomerState: CustomerState
Notes: string option }
static member Empty =
{ CustomerId = 0L
Name = CustomerName.Create ""
Phone = PhoneNoOptional.None
Email = EmailAddressOptional.None
CustomerState = CustomerState.Active
Notes = None }
member this.IsValidValue =
this.Name.IsValidValue
&& this.Phone.IsValidValue
&& this.Email.IsValidValue
member this.ErrorMsgs =
seq {
yield this.Name.ErrorMsg
yield this.Phone.ErrorMsg
yield this.Email.ErrorMsg
}
|> Seq.filter (fun emsg -> emsg.IsSome)
|> Seq.map (fun emsg -> emsg.Value)
This gives you an idea where I am going. Next I will talk about the data layer. See you in Part 2.