Skip to content
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 as type parameters #608

Closed
5 of 6 tasks
robkuz opened this issue Sep 25, 2017 · 13 comments
Closed
5 of 6 tasks

Allow Literals as type parameters #608

robkuz opened this issue Sep 25, 2017 · 13 comments

Comments

@robkuz
Copy link

robkuz commented Sep 25, 2017

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
Copy link

@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
Copy link
Author

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
Copy link
Author

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
Copy link
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
Copy link
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 as completed Nov 16, 2017
@jackfoxy
Copy link

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
Copy link
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
Copy link
Author

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

@cartermp
Copy link
Member

Re-opening this, as Anonymous Records covers some of this, but does not address the full suggestion such as this:

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">

@cartermp cartermp reopened this Dec 20, 2018
@Swoorup
Copy link

Swoorup commented Dec 2, 2020

A question I have is what should happen here?

type Person: { mutable name: "John"}

Can you change the contents? But nothing fits that type other than itself?

@Swoorup
Copy link

Swoorup commented Dec 2, 2020

Wondering if this can instead be erased as well?
This would fit naturally working with anonymous type tagged union.

type Author = ("John" | "Arthur")
type Teacher = ("Brian" | "Arthur")
type Person = (Author | Teacher) // same as ("John" | "Brian" | "Arthur")

// future F# version 20 :P
type TeacherPersonCombo = $"{Author}-{Teacher}" // same as ("John-Brian" | "Arthur-Brian" | "John-Arthur")

typeof<Person>.Name = "string"
let p: Person = "John"
let _: string = p

match p with 
| :? "John" -> ()
| :? ("Biran | "Arthur") -> ()

@rmunn
Copy link

rmunn commented Dec 2, 2020

Seems to me that a type defined as { mutable name: "John"} could assign values to name, but the only legal value to assign would be "John". This is obviously a silly case, but { mutable method: ("GET" | "PUT") } would not be silly, and is essentially the same as the "John" example from the typechecker's point of view.

@robkuz robkuz closed this as completed Jun 15, 2021
@cartermp cartermp reopened this Jun 15, 2021
@dsyme dsyme changed the title Allow Literals in (Generic) Type Definitions Allow Literals as type parameters Oct 28, 2022
@dsyme
Copy link
Collaborator

dsyme commented Jan 10, 2023

I'm closing in favour of #1195 which covers the more general typescript-like design suggestion, where literals as type parameters would also be included

@dsyme dsyme closed this as completed Jan 10, 2023
@njlr njlr mentioned this issue May 27, 2023
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants