Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In F#, is there a shortcut for creating a record type with a primary key used for equality and sorting comparison?

Tags:

.net

orm

dto

f#

I know that the F# compiler can generate default implementations of IEquatable<T> and IComparable<T>, as well as overrides for the default GetHashCode and Equals methods for records and classes. But these implementations use all fields in comparison. I have a few DTO types I need to create, where equality and sort order should be based solely on a primary key field. Is there some handy mechanism like an attribute for this?

Here is my class: (I've probably made a few F# grammar errors. Still kinda new.)

type Customer() =
    member val Id = 0 with get, set
    member val Name = "" with get, set
    member val PhoneNumber = "" with get, set

Can I do something like this:

[<CompareByKey>]
type Customer() =
    [<PrimaryKey>] member val Id = 0 with get, set
    member val Name = "" with get, set
    member val PhoneNumber = "" with get, set

instead of this:

type Customer() =
    member val Id = 0 with get, set
    member val Name = "" with get, set
    member val PhoneNumber = "" with get, set

    override Equals(obj) = 
        match obj with
        | :? Customer as c -> this.Id = c.Id
        | _ -> false

    override GetHashCode() = hash this.Id

    interface IEquatable<Customer> with
        member Equals(c) = this.Id = c.Id

    interface IComparable<Customer> with
        member CompareTo(c) = this.Id.CompareTo(c.Id)

EDIT:

I think this may be a problem better solved in C# with an abstract base class. (Also implementable in F#, but it kinda starts getting un-idiomatic.)

public abstract class EntityBase<TEntity> : 
    IEquatable<TEntity>, 
    IComparable<TEntity>
    where TEntity : EntityBase<TEntity> {

    protected EntityBase(int id) { 
        this.Id = id; 
    }

    public int Id { get; }

    public sealed override int GetHashCode() => Id.GetHashCode();

    public sealed override bool Equals(object obj) => 
        Equals(this, obj as TEntity);

    public bool Equals(TEntity other) => 
        (other != null) && (this.Id == other.Id);

    public int CompareTo(TEntity other) => 
        (other == null) ? 1 : this.Id.CompareTo(other.Id);

}
like image 546
JamesFaix Avatar asked Oct 18 '22 06:10

JamesFaix


1 Answers

You can use reflection to pull out the primary key. Reflection can be costly, so make sure you are only getting one call to the search for keys per type (I've left some printfn in the code below so that I can double-check)

open System
[<AttributeUsage(AttributeTargets.Property)>]
type PrimaryKeyAttribute() =
    inherit Attribute()
[<CLIMutable()>]
type Customer =
    {
        [<PrimaryKey>] 
        Id: int
        Name: string
    }
let getPrimaryKey<'T, 'U when 'U:> IComparable> (): 'T -> 'U =
    printfn "Starting to search for keys."
    typeof<'T>.GetProperties()
    |> Seq.tryFind (fun p -> 
        let attr = p.GetCustomAttributes(typeof<PrimaryKeyAttribute>, false)
        attr.Length > 0
    )
    |> function 
        | Some p -> 
            (fun t -> p.GetMethod.Invoke(t, [| |]) :?> 'U)
        | _ -> failwith "No PrimaryKey attribute found"

Sorting a list of customers now becomes:

let customers = 
    [
        { Id = 4; Name = "Alice" }
        { Id = 1; Name = "Eve" }
        { Id = 1; Name = "Charlie" }
        { Id = 1; Name = "Bob" }
    ]
let sorted = 
    customers
    |> List.sortBy (getPrimaryKey())

and you'll get:

val sorted : Customer list =
 [{Id = 1;
 Name = "Eve";}; {Id = 1;
                 Name = "Charlie";}; {Id = 1;
                                      Name = "Bob";}; {Id = 4;
                                                       Name = "Alice";}]

Whereas the normal sort would give you Bob; Charlie; Eve; Alice. You may want to pre-compute the key getter for each type:

let getCustomerKey<'U when 'U:> IComparable> : Customer -> 'U = 
    getPrimaryKey() 
let sorted2 = 
    customers
    |> List.sortBy getCustomerKey

And, as others have noted already, [<CLIMutable()>] is helpful when working with code that relies on default constructors and mutation. See the documentation here

like image 125
Anton Schwaighofer Avatar answered Oct 27 '22 16:10

Anton Schwaighofer