Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Copy Properties Between Records

Tags:

f#

Amongst several other State-related types, I have the following records types in my code:

type SubmittedSuggestionData = {
    SuggestionId : Guid
    SuggestionText : string
    Originator : User
    ParentCategory : Category
    SubmissionDate : DateTime
}

type ApprovedSuggestionData = {
    SuggestionId : Guid
    SuggestionText : string
    Originator : User
    ParentCategory : Category
    SubmissionDate : DateTime
    ApprovalDate : DateTime
}

These then feed into the following:

type Suggestion = 
    | SubmittedSuggestion of SubmittedSuggestionData
    | ApprovedSuggestion of ApprovedSuggestionData

This gives me the ability to work with a State machine style pattern to carry out specific business logic dependent upon State. (This approach was taken from: http://fsharpforfunandprofit.com/posts/designing-with-types-representing-states/)

I have a function that in it's simplest form, changes a SubmittedSuggestion to an ApprovedSuggestion:

let ApproveSuggestion suggestion =
    match suggestion with
    | SubmittedSuggestion suggestion -> ApprovedSuggestion {}

This function is incomplete at the moment as what I'm struggling to comprehend is when a Suggestion changes from Submitted to Approved, how do you copy the properties from the passed in suggestion into the newly created ApprovedSuggestion whilst also populating the new property of ApprovalDate?

I guess it would work if I did something like:

let ApproveSuggestion suggestion =
    match suggestion with
    | SubmittedSuggestion {SuggestionId = suggestionId; SuggestionText = suggestionText; Originator = originator; ParentCategory = category; SubmissionDate = submissionDate} -> 
        ApprovedSuggestion {SuggestionId = suggestionId; SuggestionText = suggestionText; Originator = originator; ParentCategory = category; SubmissionDate = submissionDate; ApprovalDate = DateTime.UtcNow}

but that looks pretty horrific to me.

Is there a cleaner, more succinct way of getting the same outcome? I've tried using the with keyword but it didn't compile.

Thanks

like image 556
Stu1986C Avatar asked Dec 24 '15 11:12

Stu1986C


2 Answers

If there is a large overlap between types, it is often a good idea to think about decomposition. For example, the types could look like this:

type SuggestionData = {
    SuggestionId : Guid
    SuggestionText : string
    Originator : User
    ParentCategory : Category
    SubmissionDate : DateTime
}

type ApprovedSuggestionData = {
    Suggestion : SuggestionData
    ApprovalDate : DateTime
}

Depending on the usage and differences between the types, one could also consider to have the approved type only within the discriminated union, skipping the second type altogether:

type Suggestion = 
    | SubmittedSuggestion of SuggestionData
    | ApprovedSuggestion of SuggestionData * approvalDate : DateTime

A common argument against such decomposition is that access to members of types deeper down the hierarchy becomes more verbose, e.g. approvedSuggestionData.Suggestion.Originator. While this is true, properties can be used to forward commonly used members of constituents if verbosity becomes annoying, and any downside should be weighed against the advantages: there is less code duplication in the types, and any operation that the more granular types offer can be made available from the composed type.

The ability to easily construct an approved suggestion from an unapproved suggestion and the approval date is one case in which this is useful. But there could be more: say, for example, there is an operation that validates the users and categories of all suggestions, approved or not. If the types holding the Originator and ParentCategory members for approved and unapproved suggestions are unrelated, the code that gets these needs to be duplicated. (Or a common interface would need to be created.)

like image 138
Vandroiy Avatar answered Nov 02 '22 23:11

Vandroiy


I would change your suggestion to

type SubmittedSuggestionData = {
    SuggestionId : Guid
    SuggestionText : string
    Originator : User
    ParentCategory : Category
    SubmissionDate : DateTime
    ApprovalDate : DateTime option
}

and then approval becomes

let approve t = {t with AprovalDate =Some(System.DateTime.Now)}
like image 36
John Palmer Avatar answered Nov 02 '22 23:11

John Palmer