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 Literals in (Generic) Type Definitions #608

Closed
robkuz opened this Issue Sep 25, 2017 · 8 comments

Comments

Projects
None yet
3 participants
@robkuz

robkuz commented Sep 25, 2017

Allow Literals in (Generic) Type Definitions

I often encounter the situation where I have a data structure which in is a singleton.
So when definining that data structure (and its types) I can not only define the data types of the properties but I can also narrow down the actual values/instances for these properties.

Assume a data structure for holding the VATs for some European countries

type VatRule = 
    {
        normal: float
        reduced: float
    }

type VatRules = {
    DE: VatRule 
    AT: VatRule 
    CH: VatRule 
}

let VATRULES = {
    DE =    { normal = 19.0; reduced =  7.0}
    AT =    { normal = 20.0; reduced =  10.0}
    CH =    { normal = 8.0;  reduced = 2.5}
}

Now this looks easy enough but is a unneeded and still error prone if for example somebody would define a new VATRULES instance and use that.
What I'd like to do is something like this

type VatRule = 
    {
        normal: float
        reduced: float
    }

[<Singleton>]
type VatRules = {
    DE :    { normal = 19.0; reduced =  7.0}  
    AT :    { normal = 20.0; reduced =  10.0}
    CH :    { normal = 8.0;  reduced = 2.5}
}

let VATRULES : VatRules = Unchecked.defaultof<_> // or any other attempt to create this yields always this singleton with all properties instantiated

Possible variations could be

[<Singleton>]
type VatRules = {
    DE :    ({ normal = 19.0; reduced =  7.0}  : VatRule)
    AT :    ({ normal = 20.0; reduced =  10.0}  : VatRule)
    CH :    ({ normal = 8.0;  reduced = 2.5}  : VatRule)
}

// or

type VatRule<'a, 'b when 'a:> float and 'b: float> = 
    {
        normal: 'a
        reduced: 'b
    }

[<Singleton>]
type VatRules = {
    DE :    VatRule<19.0, 7.0>
    AT :    VatRule<29.0, 10.0>
    CH :    VatRule<8.0, 2.5>
}

Where could this be used? For example having a fixed and statically typed dispatch table in an backend app servicing web requests

type Method =
    | GET
    | POST

type Route<'method, 'port, 'path when 'a :> Method and 'b :> int and 'path :> string> =
{
    method: 'method
    port: 'port
    path: 'path
}

type Routes =
    | Foo of Route<GET, 8081, "/foo">
    | Bar of Route<POST, 8080, "/bar">
    | FooBar of Route<POST, 8082, "/foobar">

let processRoute (r:Route) =
    match r with
    | Foo v
    | Bar v
    | FooBar v -> processRouteInternal v

let res = processRoute <| Foo ()

Somehow it would be necessary to allow to call any of this value constructors without a parameter (maybe unit?) and yield the propert Route instance on destructing.

One last example: assume a workflow system where within each workflow step the same record is processed. However each workflow step might access/edit only certain
parts of that record and other must not be touchd

type AccessLevel =
    | EmptyAccess
    | EditAccess
    | LockedAccess

type Access<'a> =
    | Empty
    | Edit of 'a
    | Locked of 'a

type AbstractRecord<'foo, 'bar, 'baz> = 
{
    foo: 'foo
    bar: 'bar
    baz: 'baz
}

type WorkRecord = AbstractRecord<   Access<int>,
                                    Access<float>,
                                    Access<string>>

type AccessDef<'foo, 'bar, 'baz> = AbstractRecord<'foo, 'bar, 'baz> = 

type Workflow = 
    | StepA of WorkRecord * AccessDef<EmptyAccess, EditAccess, EmptyAccess>
    | StepB of WorkRecord * AccessDef<EmptyAccess, EditAccess, EditAccess>
    | StepC of WorkRecord * AccessDef<EditAccess, EditAccess, LockedAccess>
    | StepD of WorkRecord * AccessDef<EditAccess, LockedAccess, LockedAccess>

let isEmptyAccess a =
    match a with
    | EmptyAccess -> true
    | _ -> false

let isEditAccess a =
    match a with
    | EditAccess -> true
    | _ -> false

let isLockedAccess a =
    match a with
    | isLockedAccess -> true
    | _ -> false

let checkAccessForProperty (propertyAccess:Access<'a>) (accessLevel: AccessLevel) =
    match propertyAccess with
    | Empty when isEmptyAccess accessLevel -> true
    | Edit _ when isEditAccess accessLevel -> true 
    | Locked _ when isLockedAccess accessLevel -> true

let checkForAccessibility r a =
    checkAccessForProperty r.foo a.foo and
    checkAccessForProperty r.bar a.bar and
    checkAccessForProperty r.baz a.baz and

let updateRecordInStep (w:Workflow) =
    match w with
    | StepA r a
    | StepB r a
    | StepC r a
    | StepD r a ->
        if checkForAccessibility r a then
            // do whatever
        else
            Error ["Not possible"]

let r = updateRecordInStep (StepB someRecordInstance ())

Pros and Cons

The advantages of making this adjustment to F# are free singletons! (Other langs have free Monads we could have free singletons ;-) ) Also no option to misconfigure anything at runtime anymore (the option to misconfigure durcing compile time remains however)

The disadvantages of making this adjustment to F# are depending on the syntax/semantics this could be a breaking change. However I think this could be implemented in a non-breaking fashion

Extra information

Estimated cost (XS, S, M, L, XL, XXL): L (hard to say)

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 this
  • I or my company would be willing to pay for this this
@jackfoxy

This comment has been minimized.

Show comment
Hide comment
@jackfoxy

jackfoxy Sep 26, 2017

@robkuz I think you meant

type VatRules = {
    DE: VatRule
    AT: VatRule
    CH: VatRule
}

in your first code snippet.

I would like to see literals in Class Type parameters, but I do not think it is possible in .NET. (I would like to be wrong.) This would open the door to a kind of dependent type.

jackfoxy commented Sep 26, 2017

@robkuz I think you meant

type VatRules = {
    DE: VatRule
    AT: VatRule
    CH: VatRule
}

in your first code snippet.

I would like to see literals in Class Type parameters, but I do not think it is possible in .NET. (I would like to be wrong.) This would open the door to a kind of dependent type.

@robkuz

This comment has been minimized.

Show comment
Hide comment
@robkuz

robkuz Oct 17, 2017

I have found a partial solution to this.
I will write up a bit more on how this could be merged into a more concise approach for this lang suggestion

robkuz commented Oct 17, 2017

I have found a partial solution to this.
I will write up a bit more on how this could be merged into a more concise approach for this lang suggestion

@robkuz

This comment has been minimized.

Show comment
Hide comment
@robkuz

robkuz Nov 14, 2017

I am just thinking about how this could be implemented in a backwards and .NET compatible way.

Let' say we have this types

type SomeGeneric<'a,'b> = {a: 'a; b: 'b}
type FixedValues = SomeGeneric<100, "foo">

The easiest way is to wrap each (literal) value in a wrapper class

type ConstValue<'a>(v:'a) = 
    member this.Value = v

and then to inherit from that class and provide a static Zero method

type Value_100() = inherit ConstValue(100) with
    static member Zero = Value_100() 
type Value_foo() = inherit ConstValue("foo") with
    static member Zero = Value_foo()

The initial definition would then be rewritten as

type FixedValues = SomeGeneric<Value_100, Value_foo>

then the creation of a given type could be as easy as

let zero = LanguagePrimitives.GenericZero

let a: FixedValues = {a = zero; b = zero}

robkuz commented Nov 14, 2017

I am just thinking about how this could be implemented in a backwards and .NET compatible way.

Let' say we have this types

type SomeGeneric<'a,'b> = {a: 'a; b: 'b}
type FixedValues = SomeGeneric<100, "foo">

The easiest way is to wrap each (literal) value in a wrapper class

type ConstValue<'a>(v:'a) = 
    member this.Value = v

and then to inherit from that class and provide a static Zero method

type Value_100() = inherit ConstValue(100) with
    static member Zero = Value_100() 
type Value_foo() = inherit ConstValue("foo") with
    static member Zero = Value_foo()

The initial definition would then be rewritten as

type FixedValues = SomeGeneric<Value_100, Value_foo>

then the creation of a given type could be as easy as

let zero = LanguagePrimitives.GenericZero

let a: FixedValues = {a = zero; b = zero}
@dsyme

This comment has been minimized.

Show comment
Hide comment
@dsyme

dsyme Nov 16, 2017

Collaborator

@robkuz Does #207 help here? e.g.

let VatRules = {<
    DE :    {< normal = 19.0; reduced =  7.0 >}  
    AT :    {< normal = 20.0; reduced =  10.0 >}
    CH :    {< normal = 8.0;  reduced = 2.5 >}
>}
Collaborator

dsyme commented Nov 16, 2017

@robkuz Does #207 help here? e.g.

let VatRules = {<
    DE :    {< normal = 19.0; reduced =  7.0 >}  
    AT :    {< normal = 20.0; reduced =  10.0 >}
    CH :    {< normal = 8.0;  reduced = 2.5 >}
>}
@dsyme

This comment has been minimized.

Show comment
Hide comment
@dsyme

dsyme Nov 16, 2017

Collaborator

I'll close as https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1030-anonymous-records.md seems to cover it - please reopen if not

Collaborator

dsyme commented Nov 16, 2017

I'll close as https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1030-anonymous-records.md seems to cover it - please reopen if not

@dsyme dsyme closed this Nov 16, 2017

@jackfoxy

This comment has been minimized.

Show comment
Hide comment
@jackfoxy

jackfoxy Nov 16, 2017

I'm literally interested in literals as type parameters.

type MyType<1, "foo"> = ...

That may not have been the primary focus of this issue. I will open a new language suggestion at another time when I have this worked out.

jackfoxy commented Nov 16, 2017

I'm literally interested in literals as type parameters.

type MyType<1, "foo"> = ...

That may not have been the primary focus of this issue. I will open a new language suggestion at another time when I have this worked out.

@dsyme

This comment has been minimized.

Show comment
Hide comment
@dsyme

dsyme Nov 16, 2017

Collaborator

I'm literally interested in literals as type parameters.

@jackfoxy The first technical question (besides syntax) is if there is any algebraic way to combine literals, either in non-generic code (e.g. type MyType<1+1, "foo">) or, more importantly, in generic code

let f (x1:MyType<'n`>) (x2:MyType<'n2>) = MyType<'n1 + 'n2>(x1 + x2)

You might get a fair way in a useful prototype if such combination is only allowed in inline code using the existing range of operations for building constant expressions (for which there are already evaluators in the compiler)

Collaborator

dsyme commented Nov 16, 2017

I'm literally interested in literals as type parameters.

@jackfoxy The first technical question (besides syntax) is if there is any algebraic way to combine literals, either in non-generic code (e.g. type MyType<1+1, "foo">) or, more importantly, in generic code

let f (x1:MyType<'n`>) (x2:MyType<'n2>) = MyType<'n1 + 'n2>(x1 + x2)

You might get a fair way in a useful prototype if such combination is only allowed in inline code using the existing range of operations for building constant expressions (for which there are already evaluators in the compiler)

@robkuz

This comment has been minimized.

Show comment
Hide comment
@robkuz

robkuz Dec 7, 2017

@dsyme #207 helps a bit but only for those cases where records are already involved. It doesn't help for cases with DUs. In order to get those running we would need to be able to do this somehow.

type Method =
    | GET
    | POST

type Routes =
    | Foo of {|method = GET; port = 8081; path = "/foo"|}
    | Bar of  {|method = POST; port = 8081; path = "/bar"|} 
    | FooBar of {|method = POST; port = 8081; path = "/foobar"|} 

But I don't see how this would be possible as anonymous records seem to be a concept purely on the value level and not on the type level.

The only way I foresee in the furture is with type providers for types and an extended concept for literal values

[<Literal>]
let FooRoute = {method = GET; port = 8081; path = "/foo"}
[<Literal>]
let BarRoute = {method = POST; port = 8081; path = "/bar"} 
[<Literal>]
let FooBarRoute = {method = POST; port = 8081; path = "/foobar"} 

type Routes<'a, 'b, 'c> =
    | Foo of 'a
    | Bar of  'b
    | FooBar of 'c

type ActualRoutes = SomeTP<Routes<_, _,_>, FooRoute, BarRoute, FoobarRoute>

But appart from this and the fact that syntax & semantics are not clear and that literals would have to be extended for that to work it looses much of its appeal because there is a TP involved (which seems to be total overkill) and the closeness of the information being presented is lost. They are all scattered around.

inline type expansion

What about this idea instead.
This would work only for class based approaches

type Route(method:Method; port: int, path : string) =
    member this.Method = method
    member this.Port = port
    member this.Path = path

type Routes =
    | Foo of inherit Route(GET, 8081, "/foo")
    | Bar of inherit Route(POST, 8080, "/bar")
    | FooBar of inherit Route(POST, 8082, "/foobar")

basically everywhere where a type is supplied (like here)

type FooRoute = inherit Route(GET, 8081, "/foo")

one would be able to drop in an inherit SomeType type level expression.

@dsyme would that be something worthwhile examining? I don't want to reopen this issue and would then create a new issue

robkuz commented Dec 7, 2017

@dsyme #207 helps a bit but only for those cases where records are already involved. It doesn't help for cases with DUs. In order to get those running we would need to be able to do this somehow.

type Method =
    | GET
    | POST

type Routes =
    | Foo of {|method = GET; port = 8081; path = "/foo"|}
    | Bar of  {|method = POST; port = 8081; path = "/bar"|} 
    | FooBar of {|method = POST; port = 8081; path = "/foobar"|} 

But I don't see how this would be possible as anonymous records seem to be a concept purely on the value level and not on the type level.

The only way I foresee in the furture is with type providers for types and an extended concept for literal values

[<Literal>]
let FooRoute = {method = GET; port = 8081; path = "/foo"}
[<Literal>]
let BarRoute = {method = POST; port = 8081; path = "/bar"} 
[<Literal>]
let FooBarRoute = {method = POST; port = 8081; path = "/foobar"} 

type Routes<'a, 'b, 'c> =
    | Foo of 'a
    | Bar of  'b
    | FooBar of 'c

type ActualRoutes = SomeTP<Routes<_, _,_>, FooRoute, BarRoute, FoobarRoute>

But appart from this and the fact that syntax & semantics are not clear and that literals would have to be extended for that to work it looses much of its appeal because there is a TP involved (which seems to be total overkill) and the closeness of the information being presented is lost. They are all scattered around.

inline type expansion

What about this idea instead.
This would work only for class based approaches

type Route(method:Method; port: int, path : string) =
    member this.Method = method
    member this.Port = port
    member this.Path = path

type Routes =
    | Foo of inherit Route(GET, 8081, "/foo")
    | Bar of inherit Route(POST, 8080, "/bar")
    | FooBar of inherit Route(POST, 8082, "/foobar")

basically everywhere where a type is supplied (like here)

type FooRoute = inherit Route(GET, 8081, "/foo")

one would be able to drop in an inherit SomeType type level expression.

@dsyme would that be something worthwhile examining? I don't want to reopen this issue and would then create a new issue

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