Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.Sign up
Allow to define arbitrary constructors within type constraints #631
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.
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
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
Which I wont to for 2 reasons
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
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:
Please tick all that apply:
in the signature - disallowed
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.
@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
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.