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);
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With