New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to define arbitrary constructors within type constraints #631

Open
robkuz opened this Issue Dec 6, 2017 · 2 comments

Comments

Projects
None yet
2 participants
@robkuz

robkuz commented Dec 6, 2017

using the dependent type lib I have to create always 2 (Sometimes 3) classes in order to be able to build a type with a smart constructors.
(yeah still better than writing that all on my own but still...)

let validateLen l (s:string) = s.Length <= l

type LenValidator(config: int) = inherit Validator<int, string>(config, validateLen)

type Size5 () = inherit LenValidator(5) 
type Size50 () = inherit LenValidator(50) 
type Size100 () = inherit LenValidator(100)

type String5 = LimitedValue<Size5, int, string>
type String50 = LimitedValue<Size50, int, string>
type String100 = LimitedValue<Size100, int, string>

Where one class is "just" a container for the validation algorithm and the configuration of that algorithm.

It would be however much much nicer and understandable if one could define these classes like this

let validateLen l (s:string) = s.Length <= l

type String5(v)   = inherit LimitedValue<int, string>(v, validateLen,   5)
type String50(v)  = inherit LimitedValue<int, string>(v, validateLen,  50)
type String100(v) = inherit LimitedValue<int, string>(v, validateLen, 100)

For this to work I would need to define the constructor call within the type constraints for the static member Create

type LimitedValue<'v, 'c>(value: 'v, validator: 'c -> 'v -> Option<'v>, config: 'c) = 
    member this.Value = value 
    member this.Validator = value 
    member this.Config = config
    with
        static member inline Create<'r, 'v, 'c 
                                      when 'r :> LimitedValue<'v, 'c> 
                                      and 'r: (new: 'v -> 'r)>(x:'v) : Option<'r> =
            let value: 'r = new 'r(v)
            let validate = value.Validator
            let config = value.Config
            let result = validate config v
            match result with
            | Some r -> Some value
            | _ -> None

However at the moment it is only allowed to define constructor constraints that take unit as a param and consequently only such constructors calls are allowed in the method body.

I tried to circumvent this using an inline function that calls a constructor with arbitrary parameters - which by the way is possible and legal.

let inline newValue (x: ^S) = (^T: (new: ^S -> ^T) x)

type LimitedValue<'v, 'c>(value: 'v, validator: 'c -> 'v -> Option<'v>, config: 'c) = 
    member this.Value = value 
    member this.Validator = value 
    member this.Config = config
    with
        static member inline Create<'r, 'v, 'c when 'r :> LimitedValue<'v, 'c>>(x:'v) : Option<'r> =
            let value: 'r = newValue v
            let validate = value.Validator
            let config = value.Config
            let result = validate config v
            match result with
            | Some r -> Some value
            | _ -> None

But as soon as I do this I get this error

Util-LimitedValue.fs(44, 27): [FS1198] The generic member 'Create' has been used at a non-uniform instantiation prior to this program point. 
Consider reordering the members so this member occurs first. Alternatively, specify the full type of the member explicitly, 
including argument types, return type and any additional generic parameters and constraints.

Which I wont to for 2 reasons

  1. reordering members will usually break code somewhere else
  2. I can only specify full types explicitly in the inherited classes by overriding. This is somehow incopatible with the purpose of this library to reduce boilerplate

I must admit that I am not even sure that this is a "language suggestion" at all. It feels more like a bug report. However there might be type theoretic constraints that forbid that or maybe hard technical compiler limits of which I am not aware of.

The existing way of approaching this problem in F# is ... As far as I can say there is none!

Pros and Cons

The advantages of making this adjustment to F# are that this would cleanup the SRTPs a bit a a make it less surprising using them.

The disadvantages of making this adjustment to F# are: None

Extra information

Estimated cost (XS, S, M, L, XL, XXL): ?

Related suggestions: (put links to related suggestions here)

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test and pay for this this
@0x53A

This comment has been minimized.

Show comment
Hide comment
@0x53A

0x53A Dec 11, 2017

Contributor

However at the moment it is only allowed to define constructor constraints that take unit as a param

see Microsoft/visualfsharp#675

in the signature - disallowed
in the body - allowed

This works and constrains the ctor by an int parameter:

let inline f() = (^t : (new : int -> ^t) 5)

type MyType(i:int) =
    member x.I = i

let x = f<MyType> ()

And yes, it would be great if this was fixed and you could also specify it in the signature.

Contributor

0x53A commented Dec 11, 2017

However at the moment it is only allowed to define constructor constraints that take unit as a param

see Microsoft/visualfsharp#675

in the signature - disallowed
in the body - allowed

This works and constrains the ctor by an int parameter:

let inline f() = (^t : (new : int -> ^t) 5)

type MyType(i:int) =
    member x.I = i

let x = f<MyType> ()

And yes, it would be great if this was fixed and you could also specify it in the signature.

@robkuz

This comment has been minimized.

Show comment
Hide comment
@robkuz

robkuz Dec 11, 2017

@0x53A yeah, that is true. However your proposal only works in simpler circumstances.

For example this code (following your lead)

module Definitions2 =

    type Validator<'c, 'v> = 'c -> 'v -> Option<'v>

    type LimitedValue2<'v, 'c>(value: 'v, config: 'c, validator: Validator<'c, 'v>) =
        member this.Value = value
        member this.Config = config
        member this.Validator = validator

    let inline createInternal (x: ^V) : ^R = (^R: (new : ^V -> ^R) x)
    let inline validator (x: ^R) : Validator< ^V, ^C> = (^R: (member Validator: Validator< ^V, ^C>) x)
    let inline config (x: ^R) : ^C = (^R: (member Config: ^C) x)

    let inline tryCreate(x:'v) : Option<'r> =
        let res = createInternal x
        let valid = validator res 
        let conf = config res
        let vres = valid conf x
        match vres with
        | Some _ -> res |> Some
        | _ -> None

module Types2 =
    open Definitions2
    let private validate normalize fn v = if fn (normalize v) then Some (normalize v) else None
    let validateLen l s = validate trim (isLen l) s

    type String5(v) =   inherit LimitedValue2<string, int>(v, 5, validateLen)

Works perfectly well for the following example code

module Test =
    open Types2
    open Definitions2
    
    let x: Option<String5> = tryCreate "xxx"

However it fails with an error on the call site as soon the inferrence context becomes more complex like here

let description: Option<String50> = toObj "description" (getWrappedStringFromJson tryCreate) x
                                                                                    ^^^^^
[FS0332] Could not resolve the ambiguity inherent in the use of the operator '( .ctor )' 
at or near this program point. Consider using type annotations to resolve the ambiguity.

If however I use code like this

type LimitedValue<'Validator, 'Config, 'T when 'Validator :> Validator<'Config, 'T>
                                           and  'Validator : (new: unit -> 'Validator)> =
    DependentType of 'T 
    
    with
        member __.Value = 
            let (DependentType s) = __
            s
        static member Extract (x : LimitedValue<'Validator, 'Config, 'T> ) = 
            let (DependentType s) = x
            s
        static member TryCreate(x:'T) : Option<LimitedValue<'Validator, 'Config, 'T>> =
            (new 'Validator()).Validate x
            |> Option.map DependentType

From DependentType library

It seems that when encoding constraints on the type level the inferrer is able to things better then when simply constraining parameters for a function (for whatever reason).

So it is not only that it is inconsistent and therefore surprising but depending on (I would say) subtle differences on how you approach this you might get good inferrence results or not on the call site.

But again it's hard for me to tell if this is a "bug" of if there are inherent limitations (theoretical or technical) why this is not possible.

robkuz commented Dec 11, 2017

@0x53A yeah, that is true. However your proposal only works in simpler circumstances.

For example this code (following your lead)

module Definitions2 =

    type Validator<'c, 'v> = 'c -> 'v -> Option<'v>

    type LimitedValue2<'v, 'c>(value: 'v, config: 'c, validator: Validator<'c, 'v>) =
        member this.Value = value
        member this.Config = config
        member this.Validator = validator

    let inline createInternal (x: ^V) : ^R = (^R: (new : ^V -> ^R) x)
    let inline validator (x: ^R) : Validator< ^V, ^C> = (^R: (member Validator: Validator< ^V, ^C>) x)
    let inline config (x: ^R) : ^C = (^R: (member Config: ^C) x)

    let inline tryCreate(x:'v) : Option<'r> =
        let res = createInternal x
        let valid = validator res 
        let conf = config res
        let vres = valid conf x
        match vres with
        | Some _ -> res |> Some
        | _ -> None

module Types2 =
    open Definitions2
    let private validate normalize fn v = if fn (normalize v) then Some (normalize v) else None
    let validateLen l s = validate trim (isLen l) s

    type String5(v) =   inherit LimitedValue2<string, int>(v, 5, validateLen)

Works perfectly well for the following example code

module Test =
    open Types2
    open Definitions2
    
    let x: Option<String5> = tryCreate "xxx"

However it fails with an error on the call site as soon the inferrence context becomes more complex like here

let description: Option<String50> = toObj "description" (getWrappedStringFromJson tryCreate) x
                                                                                    ^^^^^
[FS0332] Could not resolve the ambiguity inherent in the use of the operator '( .ctor )' 
at or near this program point. Consider using type annotations to resolve the ambiguity.

If however I use code like this

type LimitedValue<'Validator, 'Config, 'T when 'Validator :> Validator<'Config, 'T>
                                           and  'Validator : (new: unit -> 'Validator)> =
    DependentType of 'T 
    
    with
        member __.Value = 
            let (DependentType s) = __
            s
        static member Extract (x : LimitedValue<'Validator, 'Config, 'T> ) = 
            let (DependentType s) = x
            s
        static member TryCreate(x:'T) : Option<LimitedValue<'Validator, 'Config, 'T>> =
            (new 'Validator()).Validate x
            |> Option.map DependentType

From DependentType library

It seems that when encoding constraints on the type level the inferrer is able to things better then when simply constraining parameters for a function (for whatever reason).

So it is not only that it is inconsistent and therefore surprising but depending on (I would say) subtle differences on how you approach this you might get good inferrence results or not on the call site.

But again it's hard for me to tell if this is a "bug" of if there are inherent limitations (theoretical or technical) why this is not possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment