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.

The Visual Studio 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.

Leave a Reply

Your email address will not be published.