Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# alternate constructor assigning values to (mutable) let bindings

Tags:

constructor

f#

Suppose I have this class:

type Pet (name:string) as this =
    let mutable age = 5
    let mutable animal = "dog"

I want to be able to create a new Pet based on some serialized data, which I represent with this record:

type PetData = {
    name : string
    age : int
    animal : string
}

(TLDR: I can't figure out the syntax to make a constructor that'll take a PetData to populate the let bindings. My various attempts follow.)

So I make a new Pet constructor that'll assign values to the let bindings. I try using the class initializer syntax:

new (data:PetData) =
    Pet(name,
        age = data.age,
        animal = data.animal
    )

Hmm, nope: No accessible member or object constructor named 'Pet' takes 1 arguments. The named argument 'age' doesn't correspond to any argument or settable return property for any overload.

I check to make sure I've got all the syntax: no missing commas, correct "assignment" (cough) operator, correct indentation.

Okay the, I'll try the record initializer syntax.

new (data:PetData) =
    {
        name = data.name;
        age = data.age;
        animal = data.name
    }

Error: The type 'Pet' does not contain a field 'name'

Okay, so I need to call the main constructor. I guess there are probably two places I can put it, so let's try both:

new (data:PetData) =
    {
        Pet(data.name);
        age = data.age;
        animal = data.name
    }

Nope: Invalid object, sequence or record expression

new (data:PetData) =
    Pet(data.name)
    {
        age = data.age;
        animal = data.name
    }

And nope: This is not a valid object construction expression. Explicit object constructors must either call an alternate constructor or initialize all fields of the object and specify a call to a super class constructor.

I didn't want to have to do this, but maybe since the fields are mutable anyway, I can just assign values to the object after initializing it:

new (data:PetData) =
    let p = Pet(data.name)
    p.age <- data.age
    p.animal <- data.animal
    p

Type constraint mismatch. The type Pet is not compatible with type PetData The type 'Pet' is not compatible with the type 'PetData'

Lol, what??

Okay, let's try this:

let assign(data:PetData) =
    this.age <- data.age
    this.animal <- data.animal

new (data:PetData) =
    let p = Pet(data.name)
    p.assign(data)
    p

The field, constructor or member 'assign' is not defined

Right, so it can't access let bindings from outside.

Let's try a member then:

new (data:PetData) =
    let p = Pet(data.name)
    p.Assign(data)
    p

member x.Assign(data:PetData) =
    this.age <- data.age
    this.animal <- data.animal

This is not a valid object construction expression. Explicit object constructors must either call an alternate constructor or initialize all fields of the object and specify a call to a super class constructor.

Okay... let's try this whole thing differently then, using explicit fields:

type Pet =
    [<DefaultValue>]val mutable private age : int
    [<DefaultValue>]val mutable private animal : string
    val private name : string

    new(name:string) =
        { name = name }

    new(data:PetData) =
        {
            name = data.name;
            age = data.age;
            animal = data.animal
        }

Extraneous fields have been given values

And that's when I punch my elderly cat in the face.

Any other ideas? These error messages are throwing me off. I can't even find half of them on Google.

like image 750
Rei Miyasaka Avatar asked Dec 17 '11 02:12

Rei Miyasaka


2 Answers

You could do this.

type Pet =
    val mutable private age : int
    val mutable private animal : string
    val private name : string

    new (name:string) =
        { 
            name = name;
            age = 5; // or age = Unchecked.defaultof<_>;
            animal = "dog"; // or animal = Unchecked.defaultof<_>;
        }

    new (data:PetData) =
        {
            name = data.name;
            age = data.age;
            animal = data.animal;
        }

F# has its own style which looks like this.

type Pet(name:string, age:int, animal:string) =
    let mutable age = age
    let mutable animal = animal

    new (name:string) =
        Pet(name, 5, "dog")

    new (data:PetData) =
        Pet(data.name, data.age, data.animal)

Edit

Added an event used in do per comment request.

type Pet(name:string, age:int, animal:string, start:IEvent<string>) =
    let mutable age = age
    let mutable animal = animal

    // all three constructors will call this code.
    do  start.Add (fun _ -> printf "Pet was started")

    new (name:string, start:IEvent<_>) =
        // an example of different logic per constructor
        // this is called before the `do` code.
        let e = start |> Event.map (fun x -> x + " from 'name constructor'")
        Pet(name, 5, "dog", e)

    new (data:PetData, start:IEvent<_>) =
        Pet(data.name, data.age, data.animal, start)
like image 130
gradbot Avatar answered Jan 10 '23 12:01

gradbot


Let bindings in a type are private and there's not much you could do about that. As such you cannot use Named Arguments. By creating properties you can do it like so, but not from inside the Pet type:

type Pet (name:string) =
    let mutable age = 5
    let mutable animal = "dog"

    member x.Age with get () = age and set v = age <- v
    member x.Animal with get () = animal and set v = animal <- v

type PetData = {
    name : string
    age : int
    animal : string
}
with
    member x.ToPet =
        new Pet (x.name, Age = x.age, Animal = x.animal)

The other option would be to create a more general constructor like Gradbot suggested, either accepting a PetData object directly or all three parameters.

like image 31
David Grenier Avatar answered Jan 10 '23 11:01

David Grenier