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

Typed Union Cases #606

Open
Lavinski opened this Issue Sep 19, 2017 · 31 comments

Comments

Projects
None yet
8 participants
@Lavinski

Lavinski commented Sep 19, 2017

As it stands today, Union Cases have corresponding Types generated by the compiler if at lease one Case of the Union contains data, aka Case of obj, furthermore these types cannot be used at compile time.

I propose all Union Cases are backed by a Type and these types are accessible at compile time.

For example the following Union

type Union = 
| CaseA
| CaseB of int
| CaseC of int * int

Would compile to something like the following, with the types CaseA, CaseB and CaseC being accessible in the program. Currently, these types can only be used by reflection or from other languages such as C# when referencing the assembly containing the F# types.

[<AbstractClass>]
type Union() = class end

type CaseA() =
    inherit Union()
    
type CaseB(a: obj) =
    inherit Union()
    member this.Item = a
    
type CaseC(a: obj, b: int) =
    inherit Union()
    member this.Item1 = a
    member this.Item2 = b

The motivator for this change is to enable the usage of these types CaseA, CaseB and CaseC. This enables the Casting from the type Union Type, in this case Union to the Case Type such as CaseA. Currently this is impossible in FSharp and you would have to fallback to creating the type hierarchy manually.

Simple example

type Union = 
| CaseA
| CaseB of int
| CaseC of int * int

let testFunction (case: Union.CaseB) =
    case

let value = CaseB 1
match value with
| CaseA -> ()
| CaseB x -> testFunction (value :?> Union.CaseB)
| CaseC (x, y) -> ()

In this example, the signature of testFunction would be CaseB -> int as opposed to Union -> int.

This could also help interop with other .NET languages as well as serialization.

In my particular case, the serialization benefits are the main driver. When modelling events in a DDD system with Unions it's important that each Case has it's own type.

Pros and Cons

The advantages of making this adjustment to F# are:

  • Compiler generation of each case is uniform

Clarification:
The following snippet compiles to a single type Union.

type Union = 
| CaseA
| CaseB

However, if the Case contains data then the compiler generates three types Union, _CaseA and CaseB.

type Union = 
| CaseA
| CaseB of int

This change would mean that in both Cases three types would be created Union, CaseA and CaseB.

  • Each case can be used as an explicit type

The disadvantages of making this adjustment to F# are:

  • All cases would need a backing type

Extra information

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

No idea

Related suggestions: (put links to related suggestions here)

Utilise CLR union types for discriminated unions

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
@abelbraaksma

This comment has been minimized.

Show comment
Hide comment
@abelbraaksma

abelbraaksma Sep 19, 2017

I'm not quite sure what you are after or what use-case you are trying to solve. It seems to me that the syntax is already available to do what you want:

let testFunction (Union.CaseB case) =
    case

image

All cases would need a backing type

Not sure what that entails, Your example shows two cases with a type and one without. If you mean "without" is no longer going to be possible, I'm afraid that would be a blocking restriction. Even something fundamental like Option.None would then be broken.

Compiler generation of each case is uniform

Could you explain how it is currently not uniform? The whole idea behind DU's is uniformity and type-safety.

Each case can be used as an explicit type

This is already possible, either with pattern matching, or with lh-case syntax for function definitions (which are essentially patterns).

Note that your code, if it would compile, has two return paths of union and one of int. If I fix your code for that, isn't this what you were actually after (the casting is unnecessary)?

let value = CaseB 1
match value with
| CaseA -> 1
| CaseB x -> testFunction value   // succeeds and compiles with `testFunction` above
| CaseC (x, y) -> 2

Which has equal semantics to this:

let value = CaseB 1
match value with
| CaseA -> 1
| CaseB x -> x        // `x` is essentially the typed value of this case
| CaseC (x, y) -> 2

I may not understand your change request properly, could you elaborate, possibly with a code example that shows what is currently not possible and that would be possible with your proposal?

abelbraaksma commented Sep 19, 2017

I'm not quite sure what you are after or what use-case you are trying to solve. It seems to me that the syntax is already available to do what you want:

let testFunction (Union.CaseB case) =
    case

image

All cases would need a backing type

Not sure what that entails, Your example shows two cases with a type and one without. If you mean "without" is no longer going to be possible, I'm afraid that would be a blocking restriction. Even something fundamental like Option.None would then be broken.

Compiler generation of each case is uniform

Could you explain how it is currently not uniform? The whole idea behind DU's is uniformity and type-safety.

Each case can be used as an explicit type

This is already possible, either with pattern matching, or with lh-case syntax for function definitions (which are essentially patterns).

Note that your code, if it would compile, has two return paths of union and one of int. If I fix your code for that, isn't this what you were actually after (the casting is unnecessary)?

let value = CaseB 1
match value with
| CaseA -> 1
| CaseB x -> testFunction value   // succeeds and compiles with `testFunction` above
| CaseC (x, y) -> 2

Which has equal semantics to this:

let value = CaseB 1
match value with
| CaseA -> 1
| CaseB x -> x        // `x` is essentially the typed value of this case
| CaseC (x, y) -> 2

I may not understand your change request properly, could you elaborate, possibly with a code example that shows what is currently not possible and that would be possible with your proposal?

@0x53A

This comment has been minimized.

Show comment
Hide comment
@0x53A

0x53A Sep 19, 2017

Contributor

It seems to me that the syntax is already available to do what you want:

let testFunction (Union.CaseB case) =
    case

this pattern match allows you to directly use CaseB inside the function, but does NOT restrict the input parameters:

testFunction is still of type Union -> int

image

This is already possible, either with pattern matching, or with lh-case syntax for function definitions (which are essentially patterns).

It can be approximated, but it can't be used as a real type.

If it were a real type, I could assign it to a local, etc:

image


I have also wanted this feature a few times.

Sometimes you have a DU, and want to reuse one case in one place. For example:

open System

type Configuration =
| Local
| Network of host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool


let connectNetwork (configuration:Configuration.Network) =
    // do the stuff
    ()

let connect config =
    match config with
    | Local ->
        // do the stuff
        ()
    | Network _ as network ->
        connectNetwork network

Yes, the correct change would be to extract the configuration to a record and then pass that around. But that would mean I need to declare two types instead of one (ok, not that bad) AND if this is existing code, I need to refactor all places to instead use the record creation syntax instead of the tuple-like DU ctor, which is an invasive change.

Contributor

0x53A commented Sep 19, 2017

It seems to me that the syntax is already available to do what you want:

let testFunction (Union.CaseB case) =
    case

this pattern match allows you to directly use CaseB inside the function, but does NOT restrict the input parameters:

testFunction is still of type Union -> int

image

This is already possible, either with pattern matching, or with lh-case syntax for function definitions (which are essentially patterns).

It can be approximated, but it can't be used as a real type.

If it were a real type, I could assign it to a local, etc:

image


I have also wanted this feature a few times.

Sometimes you have a DU, and want to reuse one case in one place. For example:

open System

type Configuration =
| Local
| Network of host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool


let connectNetwork (configuration:Configuration.Network) =
    // do the stuff
    ()

let connect config =
    match config with
    | Local ->
        // do the stuff
        ()
    | Network _ as network ->
        connectNetwork network

Yes, the correct change would be to extract the configuration to a record and then pass that around. But that would mean I need to declare two types instead of one (ok, not that bad) AND if this is existing code, I need to refactor all places to instead use the record creation syntax instead of the tuple-like DU ctor, which is an invasive change.

@abelbraaksma

This comment has been minimized.

Show comment
Hide comment
@abelbraaksma

abelbraaksma Sep 19, 2017

@0x53A, for situations like these, I use type aliases. Not exactly the same as what is suggested here, but it will give some type safety. Unfortunately, the anonymous type parameters are not allowed, which makes this, at least for your example, a less-then perfect solution:

// host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool
type Network = string * uint32 * TimeSpan *  bool
type Configuration =
| Local
| Network of Network

let connectNetwork (configuration:Network) =
    // do the stuff
    ()

let connect config =
    match config with
    | Local ->
        // do the stuff
        ()
    | Network network ->
        connectNetwork network

Yes, the correct change would be to extract the configuration to a record and then pass that around.

That would be another alternative. The type alias doesn't mean a new type, but does help with such scenarios, I find myself often using it for clarity in coding. Using a record may be another step, but as you said, that means another level of indirection for people to understand.

Still, I don't much like the idea of passing the original DU around as if it is only of its cases. It breaks the rule of least surprise and can wreak havoc in terms of type safety (no way for a compiler to check you are passing Local where Network is expected). What I could imagine, though, is something that you might call an implicit type alias, which could then be referenced in the way you suggest. However, for type safety and definability, I believe this should apply to the encapsulated value of that case, not the whole class (DU) itself.

With your example, an implicit type alias is created implicitly, this means it wouldn't break existing code, and it must be written in DU_Identifier.Case_Identifier style. Since you cannot use that currently on places where a type identifier is expected, this wouldn't break existing code either. Your example would then become:

type Configuration =
| Local
// Configuration.Network is an implicit type alias for itself, similar to the `Network` alias before
| Network of host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool     

// this function can take any of:
// * string * uint32 * TimeSpan *  bool, 
// * host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool 
// * Configuration.Network
// (in that way it works essentially the same as a type alias, without the extra definition)
let connectNetwork (configuration:Configuration.Network) =
    // do the stuff
    ()

let connect config =
    match config with
    | Local ->
        // do the stuff
        ()
    | Network network ->              // currently disallowed, unless you use a type alias
            connectNetwork network

Though I am not sure if such a big change will be worth its while, as I'm sure that, once you try to work it out in an RFC, you may find a gazillion corner cases to deal with...

abelbraaksma commented Sep 19, 2017

@0x53A, for situations like these, I use type aliases. Not exactly the same as what is suggested here, but it will give some type safety. Unfortunately, the anonymous type parameters are not allowed, which makes this, at least for your example, a less-then perfect solution:

// host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool
type Network = string * uint32 * TimeSpan *  bool
type Configuration =
| Local
| Network of Network

let connectNetwork (configuration:Network) =
    // do the stuff
    ()

let connect config =
    match config with
    | Local ->
        // do the stuff
        ()
    | Network network ->
        connectNetwork network

Yes, the correct change would be to extract the configuration to a record and then pass that around.

That would be another alternative. The type alias doesn't mean a new type, but does help with such scenarios, I find myself often using it for clarity in coding. Using a record may be another step, but as you said, that means another level of indirection for people to understand.

Still, I don't much like the idea of passing the original DU around as if it is only of its cases. It breaks the rule of least surprise and can wreak havoc in terms of type safety (no way for a compiler to check you are passing Local where Network is expected). What I could imagine, though, is something that you might call an implicit type alias, which could then be referenced in the way you suggest. However, for type safety and definability, I believe this should apply to the encapsulated value of that case, not the whole class (DU) itself.

With your example, an implicit type alias is created implicitly, this means it wouldn't break existing code, and it must be written in DU_Identifier.Case_Identifier style. Since you cannot use that currently on places where a type identifier is expected, this wouldn't break existing code either. Your example would then become:

type Configuration =
| Local
// Configuration.Network is an implicit type alias for itself, similar to the `Network` alias before
| Network of host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool     

// this function can take any of:
// * string * uint32 * TimeSpan *  bool, 
// * host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool 
// * Configuration.Network
// (in that way it works essentially the same as a type alias, without the extra definition)
let connectNetwork (configuration:Configuration.Network) =
    // do the stuff
    ()

let connect config =
    match config with
    | Local ->
        // do the stuff
        ()
    | Network network ->              // currently disallowed, unless you use a type alias
            connectNetwork network

Though I am not sure if such a big change will be worth its while, as I'm sure that, once you try to work it out in an RFC, you may find a gazillion corner cases to deal with...

@Lavinski

This comment has been minimized.

Show comment
Hide comment
@Lavinski

Lavinski Sep 20, 2017

@abelbraaksma it looks like @0x53A has already clarified my proposal, I have also added to my original comment to clarify a few points.

@0x53A thanks for your response. It's encouraging to see I'm not the only one who's come up against this limitation.

The key technical point for this proposal is that the signature of testFunction would be Union.CaseB -> int as opposed to Union -> int. This means that the function is more restrictive than just Union which offers better type safety.

Having said that I'm not sure what the open source process is with regards to FSharp and I have no experience with modifying a compiler or language so any pointers would be greatly appreciated.

Lavinski commented Sep 20, 2017

@abelbraaksma it looks like @0x53A has already clarified my proposal, I have also added to my original comment to clarify a few points.

@0x53A thanks for your response. It's encouraging to see I'm not the only one who's come up against this limitation.

The key technical point for this proposal is that the signature of testFunction would be Union.CaseB -> int as opposed to Union -> int. This means that the function is more restrictive than just Union which offers better type safety.

Having said that I'm not sure what the open source process is with regards to FSharp and I have no experience with modifying a compiler or language so any pointers would be greatly appreciated.

@rmunn

This comment has been minimized.

Show comment
Hide comment
@rmunn

rmunn Sep 20, 2017

Currently, an F# discriminated union usually (see below) compiles to an abstract base class and a set of concrete implementation classes. Scott Wlaschin has a repo where he keeps a bunch of F# code samples and their decompiled C# equivalents. The one most relevant to the current discussion is this DU:

type CheckNumber = CheckNumber of int
type CardType = MasterCard | Visa
type CardNumber = CardNumber of string

[<NoComparisonAttribute>]
type PaymentMethod = 
    | Cash
    | Check of CheckNumber 
    | CreditCard of CardType * CardNumber 

The PaymentMethod class decompiles into a 200-line C# file, so I'll just link it here rather than copying it: https://github.com/swlaschin/fsharp-decompiled/blob/master/CsEquivalents/UnionTypeExamples/PaymentMethod.cs

Now, when I said that an F# DU "usually" compiles to a class hierarchy, there's one exception. Any DU that does not have any cases with extra data (e.g., the CardType example in Scott Wlaschin's sample code) does not get compiled into a class hierarchy. Instead, it gets turned into a single class CardType with a single int Tag property, and two singleton instances for MasterCard and Visa. See the full decompiled C# code for CardType here: https://github.com/swlaschin/fsharp-decompiled/blob/master/CsEquivalents/UnionTypeExamples/CardType.cs

@Lavinski, I don't think that what you're asking for is for DUs like CardType to have their cases turned into "real" types, because there's no use case for those. Correct me if I'm wrong, but I believe what you're asking for is to be able to type-safely write functions like:

let processCardPayment (card : PaymentMethod.CreditCard) =
    // Compiler won't let a Cash or Check value be passed to this function
    let cardType, cardNumber = card  // Destructuring assignment is valid here
    printfn "Charging card number %s of type %A" cardNumber cardType

Because the internal representation of DU cases is already a .NET class, I think it wouldn't be too difficult to implement this suggestion if it was approved. I'd estimate its cost at no more than L, maybe even M. If this suggestion is asking for "basic" DUs like CardType to be turned into real classes then my estimate of the cost would go up, but I don't think that's the request.

rmunn commented Sep 20, 2017

Currently, an F# discriminated union usually (see below) compiles to an abstract base class and a set of concrete implementation classes. Scott Wlaschin has a repo where he keeps a bunch of F# code samples and their decompiled C# equivalents. The one most relevant to the current discussion is this DU:

type CheckNumber = CheckNumber of int
type CardType = MasterCard | Visa
type CardNumber = CardNumber of string

[<NoComparisonAttribute>]
type PaymentMethod = 
    | Cash
    | Check of CheckNumber 
    | CreditCard of CardType * CardNumber 

The PaymentMethod class decompiles into a 200-line C# file, so I'll just link it here rather than copying it: https://github.com/swlaschin/fsharp-decompiled/blob/master/CsEquivalents/UnionTypeExamples/PaymentMethod.cs

Now, when I said that an F# DU "usually" compiles to a class hierarchy, there's one exception. Any DU that does not have any cases with extra data (e.g., the CardType example in Scott Wlaschin's sample code) does not get compiled into a class hierarchy. Instead, it gets turned into a single class CardType with a single int Tag property, and two singleton instances for MasterCard and Visa. See the full decompiled C# code for CardType here: https://github.com/swlaschin/fsharp-decompiled/blob/master/CsEquivalents/UnionTypeExamples/CardType.cs

@Lavinski, I don't think that what you're asking for is for DUs like CardType to have their cases turned into "real" types, because there's no use case for those. Correct me if I'm wrong, but I believe what you're asking for is to be able to type-safely write functions like:

let processCardPayment (card : PaymentMethod.CreditCard) =
    // Compiler won't let a Cash or Check value be passed to this function
    let cardType, cardNumber = card  // Destructuring assignment is valid here
    printfn "Charging card number %s of type %A" cardNumber cardType

Because the internal representation of DU cases is already a .NET class, I think it wouldn't be too difficult to implement this suggestion if it was approved. I'd estimate its cost at no more than L, maybe even M. If this suggestion is asking for "basic" DUs like CardType to be turned into real classes then my estimate of the cost would go up, but I don't think that's the request.

@Lavinski

This comment has been minimized.

Show comment
Hide comment
@Lavinski

Lavinski Sep 20, 2017

@rmunn yes, that matches what I was thinking

Lavinski commented Sep 20, 2017

@rmunn yes, that matches what I was thinking

@isaacabraham

This comment has been minimized.

Show comment
Hide comment
@isaacabraham

isaacabraham Sep 20, 2017

This would also cut down on boilerplate whereby you have to create a separate "inner" single-case DU to get around this e.g.

type BarData = BarData of int

type Possibilities =
| Foo
| Bar of BarData

let handleBar (BarData x) = ...

This could be simplified to just

type Possibilities =
| Foo
| Bar of int

let handleBar (Bar x) = ...

This would be a win for me - less boilerplate, less jumping around code etc.. But perhaps this should be an "opt-in" feature e.g.

[<AllowCaseAssignment>]
type Possibilities =
| Foo
| Bar of int

Or something like that.

isaacabraham commented Sep 20, 2017

This would also cut down on boilerplate whereby you have to create a separate "inner" single-case DU to get around this e.g.

type BarData = BarData of int

type Possibilities =
| Foo
| Bar of BarData

let handleBar (BarData x) = ...

This could be simplified to just

type Possibilities =
| Foo
| Bar of int

let handleBar (Bar x) = ...

This would be a win for me - less boilerplate, less jumping around code etc.. But perhaps this should be an "opt-in" feature e.g.

[<AllowCaseAssignment>]
type Possibilities =
| Foo
| Bar of int

Or something like that.

@abelbraaksma

This comment has been minimized.

Show comment
Hide comment
@abelbraaksma

abelbraaksma Sep 20, 2017

@isaacabraham: we have to be careful here to talk about the correct possible syntax, your middle example shows:

let handleBar (Bar x) = ...

This is already a legal pattern. It will show a warning that you didn't match on Foo, but it is legal can can be used, and expects a Possibilities type.

I think you meant:

let handleBar (x: Bar) = ...

But there too we have to be careful, as if I update your first example to look like the code below, what do you expect the compiler to choose here? The implicit union case type (to use a variant of my previous term) or the actual type?

type Bar = Bar of int

type Possibilities =
| Foo
| Bar of Bar

let handleBar (x: Bar) = ...   // ????

Hence my suggestion in my previous comment to require fully qualified DU names, or conflicts will arise with existing code and it would make this a breaking change. So the last line should look like this:

// x is of type Bar, which contains an int. It is not of type Possibilities
let handleBar (x: Posibilities.Bar) = ...

And even then, a change here would break existing code if the qualified name is used in current code that matches a module and/or namespace. This is not that uncommon, as so many F# users create a type and then a module with the same name (using ModuleSuffix). Any change here, if @dsyme would consider to approve this in principle, would have to take many corner cases into account to prevent this from becoming a breaking change.

On @Lavinski's and @rmunn's notes on not allowing this for DU's that have no wrapped types, I would prefer a solution that works without surprises. If F# is going to support DU-cases-as-implicit-type-unwrappers-disguised-as-types, it should be done in a design that works orthogonal for all DU types (or even expanded to tuples with named values, and record types). Otherwise we break the principle of least surprise for a new language feature.

On top of these musings, a language feature that extends the type system should be able to be properly inferred. How would that work here if the type annotation is not given in the signature?

My take on that is that it should work similar to using type aliases, to keep it simple. I.e. where a function that takes that type alias, will also take the pure type it has aliased. So in the case of @rmunn's example:

let processCardPayment (card : PaymentMethod.CreditCard) =

Here CreditCard is defined as CardType * CardNumber, so I'm suggesting a call like the following should succeed:

let x = processCardPayment (Visa , CardNumber "12345")   // works

I think that would make implementing this easier, as the compiler (and the very complex type inference mechanism) does not have to be expanded with a new set of possible types. The downside of such approach is lesser type safety, which is at the heart of your request. But as mentioned before, that can be easily tackled by existing means.

@Lavinski, you asked about the process for new features. The normal procedure is usually to wait for someone on the MS F# Team to look into the suggestion and give it his or her blessing, which will typically be accompanied with a request to create an RFC (which I believe stands for Request For Change). That RFC should contain the formal syntax and semantics, plus any expected impact and/or backwards incompatibility issues, or open questions that remain to be answered. If the RFC is approved, a PR (or WIP) can be made by any volunteer, which can thenceforth be reviewed by the community. Once all bugs are resolved and the PR is accepted, it can be merged into a selected branch (i.e., become part of F# 4.2).

At least that is how I understand the process, I do not claim to know it in detail as I haven't been through to the (whole) process myself yet.

abelbraaksma commented Sep 20, 2017

@isaacabraham: we have to be careful here to talk about the correct possible syntax, your middle example shows:

let handleBar (Bar x) = ...

This is already a legal pattern. It will show a warning that you didn't match on Foo, but it is legal can can be used, and expects a Possibilities type.

I think you meant:

let handleBar (x: Bar) = ...

But there too we have to be careful, as if I update your first example to look like the code below, what do you expect the compiler to choose here? The implicit union case type (to use a variant of my previous term) or the actual type?

type Bar = Bar of int

type Possibilities =
| Foo
| Bar of Bar

let handleBar (x: Bar) = ...   // ????

Hence my suggestion in my previous comment to require fully qualified DU names, or conflicts will arise with existing code and it would make this a breaking change. So the last line should look like this:

// x is of type Bar, which contains an int. It is not of type Possibilities
let handleBar (x: Posibilities.Bar) = ...

And even then, a change here would break existing code if the qualified name is used in current code that matches a module and/or namespace. This is not that uncommon, as so many F# users create a type and then a module with the same name (using ModuleSuffix). Any change here, if @dsyme would consider to approve this in principle, would have to take many corner cases into account to prevent this from becoming a breaking change.

On @Lavinski's and @rmunn's notes on not allowing this for DU's that have no wrapped types, I would prefer a solution that works without surprises. If F# is going to support DU-cases-as-implicit-type-unwrappers-disguised-as-types, it should be done in a design that works orthogonal for all DU types (or even expanded to tuples with named values, and record types). Otherwise we break the principle of least surprise for a new language feature.

On top of these musings, a language feature that extends the type system should be able to be properly inferred. How would that work here if the type annotation is not given in the signature?

My take on that is that it should work similar to using type aliases, to keep it simple. I.e. where a function that takes that type alias, will also take the pure type it has aliased. So in the case of @rmunn's example:

let processCardPayment (card : PaymentMethod.CreditCard) =

Here CreditCard is defined as CardType * CardNumber, so I'm suggesting a call like the following should succeed:

let x = processCardPayment (Visa , CardNumber "12345")   // works

I think that would make implementing this easier, as the compiler (and the very complex type inference mechanism) does not have to be expanded with a new set of possible types. The downside of such approach is lesser type safety, which is at the heart of your request. But as mentioned before, that can be easily tackled by existing means.

@Lavinski, you asked about the process for new features. The normal procedure is usually to wait for someone on the MS F# Team to look into the suggestion and give it his or her blessing, which will typically be accompanied with a request to create an RFC (which I believe stands for Request For Change). That RFC should contain the formal syntax and semantics, plus any expected impact and/or backwards incompatibility issues, or open questions that remain to be answered. If the RFC is approved, a PR (or WIP) can be made by any volunteer, which can thenceforth be reviewed by the community. Once all bugs are resolved and the PR is accepted, it can be merged into a selected branch (i.e., become part of F# 4.2).

At least that is how I understand the process, I do not claim to know it in detail as I haven't been through to the (whole) process myself yet.

@rmunn

This comment has been minimized.

Show comment
Hide comment
@rmunn

rmunn Sep 21, 2017

On @abelbraaksma's let x = processCardPayment (Visa , CardNumber "12345") suggestion, I disagree. That would break my expectations of the type system and cause significant surprise. If the function is defined as:

let processCardPayment (card : PaymentMethod.CreditCard) =

then I would expect the call to look like this:

let x = processCardPayment (CreditCard (Visa , CardNumber "12345"))
// Or if a fully-qualified name is necessary:
let x = processCardPayment (PaymentMethod.CreditCard (Visa , CardNumber "12345"))

That would, I think, be closer to how it currently works, where a function defined as let f (Foo x) = x can be called with a Foo 5 case (assuming Foo is Foo of int), but cannot be called directly as f 5.

rmunn commented Sep 21, 2017

On @abelbraaksma's let x = processCardPayment (Visa , CardNumber "12345") suggestion, I disagree. That would break my expectations of the type system and cause significant surprise. If the function is defined as:

let processCardPayment (card : PaymentMethod.CreditCard) =

then I would expect the call to look like this:

let x = processCardPayment (CreditCard (Visa , CardNumber "12345"))
// Or if a fully-qualified name is necessary:
let x = processCardPayment (PaymentMethod.CreditCard (Visa , CardNumber "12345"))

That would, I think, be closer to how it currently works, where a function defined as let f (Foo x) = x can be called with a Foo 5 case (assuming Foo is Foo of int), but cannot be called directly as f 5.

@abelbraaksma

This comment has been minimized.

Show comment
Hide comment
@abelbraaksma

abelbraaksma Sep 21, 2017

then I would expect the call to look like this:

@rmunn, unfortunately, your suggestion to call it like processCardPayment (CreditCard (Visa , CardNumber "12345")) cannot possibly work, as that will set the type to PaymentMethod using current semantics, and we cannot change that behavior.

For that scenario, it is already possible to create a filter function (with or without the warning).

The new scenario is suggested to work as explained in your own post above, where you showed this:

let processCardPayment (card : PaymentMethod.CreditCard) =
    // Compiler won't let a Cash or Check value be passed to this function
    let cardType, cardNumber = card  // Destructuring assignment is valid here
    printfn "Charging card number %s of type %A" cardNumber cardType

Since you say that desctructuring is valid here, the rh-side must be a tuple. That means that your code (at least the way I understand it) can be called like this:

match x with
| Cash -> ...
| Check _ -> ...
| CreditCard typeAndNumber ->  // currently, matching a tuple this way is not allowed
    processCardPayment typeAndNumber   // typeAndNumber is CardType * CardNumber

To summarize, given this approach, I can distill four suggestions from rereading the whole thread:

  1. Allow for implicit discriminated union case types, where the typename is equal to the case name
  2. Allow usage of such types similar to type abbreviations (my proposal, not the original, which suggests a more strongly typed approach, though this is the same behavior you would get if you create type abbreviations for each case)
  3. Stop requiring tupled types to have to be matched using parenthesized comma-syntax (this follows from (1), because if you currently create an explicit case-type with a type abbreviation, this is the behavior you get)
  4. Either require FQNs always for the types, or only when there is an ambiguity (i.e., a type with that name already exists, in which case the type gets precedence, otherwise existing code will stop compiling)

That is my view on how this could work and be relatively painless to introduce, without backwards compatibility issues.

abelbraaksma commented Sep 21, 2017

then I would expect the call to look like this:

@rmunn, unfortunately, your suggestion to call it like processCardPayment (CreditCard (Visa , CardNumber "12345")) cannot possibly work, as that will set the type to PaymentMethod using current semantics, and we cannot change that behavior.

For that scenario, it is already possible to create a filter function (with or without the warning).

The new scenario is suggested to work as explained in your own post above, where you showed this:

let processCardPayment (card : PaymentMethod.CreditCard) =
    // Compiler won't let a Cash or Check value be passed to this function
    let cardType, cardNumber = card  // Destructuring assignment is valid here
    printfn "Charging card number %s of type %A" cardNumber cardType

Since you say that desctructuring is valid here, the rh-side must be a tuple. That means that your code (at least the way I understand it) can be called like this:

match x with
| Cash -> ...
| Check _ -> ...
| CreditCard typeAndNumber ->  // currently, matching a tuple this way is not allowed
    processCardPayment typeAndNumber   // typeAndNumber is CardType * CardNumber

To summarize, given this approach, I can distill four suggestions from rereading the whole thread:

  1. Allow for implicit discriminated union case types, where the typename is equal to the case name
  2. Allow usage of such types similar to type abbreviations (my proposal, not the original, which suggests a more strongly typed approach, though this is the same behavior you would get if you create type abbreviations for each case)
  3. Stop requiring tupled types to have to be matched using parenthesized comma-syntax (this follows from (1), because if you currently create an explicit case-type with a type abbreviation, this is the behavior you get)
  4. Either require FQNs always for the types, or only when there is an ambiguity (i.e., a type with that name already exists, in which case the type gets precedence, otherwise existing code will stop compiling)

That is my view on how this could work and be relatively painless to introduce, without backwards compatibility issues.

@isaacabraham

This comment has been minimized.

Show comment
Hide comment
@isaacabraham

isaacabraham Sep 21, 2017

@abelbraaksma

This is already a legal pattern. It will show a warning that you didn't match on Foo, but it is legal can can be used, and expects a Possibilities type.

That's exactly the point. You don't want a warning - you want the compiler to guarantee that you can't call this code with anything apart from a Bar. What I typed is what I meant to type (as far as I'm aware). I don't want to have to create an "inner" DU just to give me compiler safety that I'm guaranteed to only call a function with that case.

isaacabraham commented Sep 21, 2017

@abelbraaksma

This is already a legal pattern. It will show a warning that you didn't match on Foo, but it is legal can can be used, and expects a Possibilities type.

That's exactly the point. You don't want a warning - you want the compiler to guarantee that you can't call this code with anything apart from a Bar. What I typed is what I meant to type (as far as I'm aware). I don't want to have to create an "inner" DU just to give me compiler safety that I'm guaranteed to only call a function with that case.

@abelbraaksma

This comment has been minimized.

Show comment
Hide comment
@abelbraaksma

abelbraaksma Sep 21, 2017

@isaacabraham, then how is your approach possible? I mean, the compiler needs to statically, deterministically, be able to assess the actual type. But the type of the variable you pass in in your example is of the DU itself. Yet you expect the function to only accept a (now hidden) case-type.

Let's say you would write this:

match payment with
| Cash -> processCardPayment payment
| Check _ -> processCardPayment payment
| CreditCard _ ->  processCardPayment payment

My understanding of the OP is that this should throw a compile time error for wrong type on the first two calls. But this is impossible to determine statically, so unless we use the case-value (which has a different type), I don't see how this can be done.

Note that currently, the syntax of your earlier post shows already existing syntax, which would indeed expect type Payment (after adopting your Bar example), and would throw a dynamic error, plus a warning at compile time. I.e., the following is already possible, but does not satisfy the requirements of the OP:

let processCardPayment (Payment.CreditCard cc) = ... 

What am I missing?

abelbraaksma commented Sep 21, 2017

@isaacabraham, then how is your approach possible? I mean, the compiler needs to statically, deterministically, be able to assess the actual type. But the type of the variable you pass in in your example is of the DU itself. Yet you expect the function to only accept a (now hidden) case-type.

Let's say you would write this:

match payment with
| Cash -> processCardPayment payment
| Check _ -> processCardPayment payment
| CreditCard _ ->  processCardPayment payment

My understanding of the OP is that this should throw a compile time error for wrong type on the first two calls. But this is impossible to determine statically, so unless we use the case-value (which has a different type), I don't see how this can be done.

Note that currently, the syntax of your earlier post shows already existing syntax, which would indeed expect type Payment (after adopting your Bar example), and would throw a dynamic error, plus a warning at compile time. I.e., the following is already possible, but does not satisfy the requirements of the OP:

let processCardPayment (Payment.CreditCard cc) = ... 

What am I missing?

@isaacabraham

This comment has been minimized.

Show comment
Hide comment
@isaacabraham

isaacabraham Sep 21, 2017

@abelbraaksma To be honest, I'm talking now from the users perspective of F#. With this hat on, I'm not interested in talking about how it would be implemented. Just what the use-case / requirement is - that from time to time it is useful to specify a specific case and to statically determine this.

You've mentioned for the third time now your point regarding the warning issue - once again I'll say - that what I would like is to have this guaranteed and without a warning otherwise what's the point. In the same way that you can exhaustively pattern match over cases, I would like to be able to specific a single case from a DU as an input, without the hassle of creating a nested wrapper type.

isaacabraham commented Sep 21, 2017

@abelbraaksma To be honest, I'm talking now from the users perspective of F#. With this hat on, I'm not interested in talking about how it would be implemented. Just what the use-case / requirement is - that from time to time it is useful to specify a specific case and to statically determine this.

You've mentioned for the third time now your point regarding the warning issue - once again I'll say - that what I would like is to have this guaranteed and without a warning otherwise what's the point. In the same way that you can exhaustively pattern match over cases, I would like to be able to specific a single case from a DU as an input, without the hassle of creating a nested wrapper type.

@abelbraaksma

This comment has been minimized.

Show comment
Hide comment
@abelbraaksma

abelbraaksma Sep 21, 2017

@isaacabraham, thanks for making clear that you understand what I was trying to explain. Sorry if I was repetitive, didn't mean to, it is not always clear what part of a message is understood.

To be honest, I'm talking now from the users perspective of F#.

Yes, me too. And I'm trying to get that perspective clear and perhaps I'm doing a lousy job, but without getting it clear, there's no chance it is implementable. I am not talking about how to implement it, but about proposing something users can use and implementers can implement. The single most important thing for both end-users and implementers alike is determinism. If it isn't there, it cannot be implemented.

So, I'm just trying one more time (sorry about that), could you come up with an example how your proposal could work, deterministically, from a user's perspective, or how you think it is dramatically different from my proposal (which I believe is to some extent deterministic), and why (if?) my proposal is not suitable (which, as I have tried to point out, forces type-safety on a single case, without the hassle of writing wrapper types or abbreviations, though obviously, you must pass in that DU-case, and not the wrapper DU value, as then you'll have all cases again and you are back to where you started).

abelbraaksma commented Sep 21, 2017

@isaacabraham, thanks for making clear that you understand what I was trying to explain. Sorry if I was repetitive, didn't mean to, it is not always clear what part of a message is understood.

To be honest, I'm talking now from the users perspective of F#.

Yes, me too. And I'm trying to get that perspective clear and perhaps I'm doing a lousy job, but without getting it clear, there's no chance it is implementable. I am not talking about how to implement it, but about proposing something users can use and implementers can implement. The single most important thing for both end-users and implementers alike is determinism. If it isn't there, it cannot be implemented.

So, I'm just trying one more time (sorry about that), could you come up with an example how your proposal could work, deterministically, from a user's perspective, or how you think it is dramatically different from my proposal (which I believe is to some extent deterministic), and why (if?) my proposal is not suitable (which, as I have tried to point out, forces type-safety on a single case, without the hassle of writing wrapper types or abbreviations, though obviously, you must pass in that DU-case, and not the wrapper DU value, as then you'll have all cases again and you are back to where you started).

@realvictorprm

This comment has been minimized.

Show comment
Hide comment
@realvictorprm

realvictorprm Sep 21, 2017

Member

@isaacabraham I think I understand what you mean but I can't describe it in code because I have no idea how it could be done too.

Member

realvictorprm commented Sep 21, 2017

@isaacabraham I think I understand what you mean but I can't describe it in code because I have no idea how it could be done too.

@robkuz

This comment has been minimized.

Show comment
Hide comment
@robkuz

robkuz Sep 21, 2017

I am a bit lost to imagine how this feature would ever work without some kind of flow analysis

robkuz commented Sep 21, 2017

I am a bit lost to imagine how this feature would ever work without some kind of flow analysis

@isaacabraham

This comment has been minimized.

Show comment
Hide comment
@isaacabraham

isaacabraham Sep 21, 2017

@abelbraaksma I gave an example proposal - admittedly one that was barely thought through - that might have sufficed. Here it is again.

[<AllowCaseAssignment>]
type Possibilities =
| Foo
| Bar of int

In this situation, I'm telling the compiler explicitly that I want specific checking on that type and to allow me to supply it to another function etc., and for the compiler to restrict it. I have no real problem with full-qualified names being the guide to the compiler instead of an attibute or whatever - both have pros and cons. The main point is that there should be some way to do this. I know it has a crazy type system but Scala manages to do this - I'm sure theres a way that we can propose something that's workable.

isaacabraham commented Sep 21, 2017

@abelbraaksma I gave an example proposal - admittedly one that was barely thought through - that might have sufficed. Here it is again.

[<AllowCaseAssignment>]
type Possibilities =
| Foo
| Bar of int

In this situation, I'm telling the compiler explicitly that I want specific checking on that type and to allow me to supply it to another function etc., and for the compiler to restrict it. I have no real problem with full-qualified names being the guide to the compiler instead of an attibute or whatever - both have pros and cons. The main point is that there should be some way to do this. I know it has a crazy type system but Scala manages to do this - I'm sure theres a way that we can propose something that's workable.

@realvictorprm

This comment has been minimized.

Show comment
Hide comment
@realvictorprm

realvictorprm Sep 21, 2017

Member

@isaacabraham there's always a way I agree.

I think I've an idea how it can work.

Member

realvictorprm commented Sep 21, 2017

@isaacabraham there's always a way I agree.

I think I've an idea how it can work.

@realvictorprm

This comment has been minimized.

Show comment
Hide comment
@realvictorprm

realvictorprm Sep 21, 2017

Member

Ok let's pick up @isaacabraham example.

Imagine following, the definition would compile roughly down to this:

[<Sealed>]
type Possibilities() =
  class
  end

type Foo () =
   inherit Possibilities()
   class
   end

type Bar(i) =
  inherit Posibilities()
  let var1 : int = i

(ignore that it's not possible this way yet.)

Member

realvictorprm commented Sep 21, 2017

Ok let's pick up @isaacabraham example.

Imagine following, the definition would compile roughly down to this:

[<Sealed>]
type Possibilities() =
  class
  end

type Foo () =
   inherit Possibilities()
   class
   end

type Bar(i) =
  inherit Posibilities()
  let var1 : int = i

(ignore that it's not possible this way yet.)

@isaacabraham

This comment has been minimized.

Show comment
Hide comment
@isaacabraham

isaacabraham Sep 21, 2017

Isn't that more or less what it compiles down to now anyway i.e. a base class with two subclasses (ignore all the Tag stuff around it)?

isaacabraham commented Sep 21, 2017

Isn't that more or less what it compiles down to now anyway i.e. a base class with two subclasses (ignore all the Tag stuff around it)?

@realvictorprm

This comment has been minimized.

Show comment
Hide comment
@realvictorprm

realvictorprm Sep 21, 2017

Member

I think so, the point is just that we cannot access the inner types like you would like.
However I'm not an expert here yet, someone else might know this even better.

Member

realvictorprm commented Sep 21, 2017

I think so, the point is just that we cannot access the inner types like you would like.
However I'm not an expert here yet, someone else might know this even better.

@abelbraaksma

This comment has been minimized.

Show comment
Hide comment
@abelbraaksma

abelbraaksma Sep 21, 2017

we cannot access the inner types like you would like.

@realvictorprm, we have to be careful here, as it is primarily the reason that you shouldn't need access to the inner types. If you do, you should probably use record types (that's not to say that I do see use-cases for this).

I gave an example proposal - admittedly one that was barely thought through - that might have sufficed. Here it is again.

@isaacabraham, I am really trying to follow your logic, and I thought I responded in depth to your earlier proposal, and I agreed to your proposal and tried to show a way how it would work. What you copies above is a DU with an attribute annotation. I still don't see how the compiler can now in case you pass in an instance of the DU that you are guaranteed to only and ever only pass in that case of the DU.

And that's the whole problem here. I am sympathetic to the idea, but the whole discussion evolves around strong(er) type safety than is currently the case with loose matching (I mean, incomplete lh-side matching expressions).

The only way to pass a subgregated (as opposed to aggregated) type instance, you can only ever unwrap the type. Again, that is the current idea of DU's to begin with, you are supposed to write functions that unwrap the inner types.

Let's try to see what this issue is solving. Let's have a look at an omnipresent function:

let f someOption = 
    someOption
    |> Option.map (fun i -> i + i)   // type-safe access to inner type of DU Option

Since Option.map only ever needs to do something when the input contains Some value, we could consider this would look as follows with the new syntax:

// assuming what I think @isaacabraham meant (someOption is of type Option):
let map f (someOption: Option.Some) = f someOption.Value |> Some

But this begs the question, what happens if the input is None? Will it be ignored? How, what is the effect? Hence, currently, the compiler forces you to consider all cases and you must write this instead (common knowledge, I know, but let's repeat it for the example):

// a default implementation
let map f = function Some x -> f x |> Some | None -> None

(a bit to my own surprise, using current syntax uses less characters, but that's besides the point, as the OP showed, this gets more interesting if you have many cases).

Because of what seems to be the current train of thought (pass in an instance of a DU, but only act on one specific case of that DU, compiler must enforce this), we have to consider either how this can be achieved, or if there is an alternative way.

My alternative way as explained before should not be taken as a change to the proposal, but as a way of moving forward on the proposal, as it solves the question "what if the programmer uses this feature wrongly".

My approach cannot be used for scenarios like map, and deliberately so. However, it can be used to do something specific on a specific case-type:

let mapSome f (someSome: Option.Some) = f someSome |> Some
// usage:
let x = 
   match Some 15 with
   | None -> ...
   | Some x -> mapSome (fun x -> x + x)   // the type mapSome takes is guaranteed to take Option.Some, here int

I am still unsure if I translate your (@isaacabraham's) proposal correctly. If you know the answer to the question ("what if"), then please explain where my line of thought goes wrong. I hope the example above is a better explanation, based on a real-world example. Otherwise, apologies for repeating the obvious, I'm just not sure what the misunderstanding is, I have the strong feeling we are talking about the same thing with only minor differences in the details. But often, details matter, I'm afraid.

PS:

but Scala manages to do this - I'm sure theres a way that we can propose something that's workable.

What exactly does Scala manage and how? Does it give access to the inner types, or values, and what does it do when the DU does not hold that value (i.e., is of another case altogether)? Is it compile-time enforcement or runtime dynamic exception (as with Some.Value, for instance, which accesses the inner value as well, and it raises a runtime exception).

abelbraaksma commented Sep 21, 2017

we cannot access the inner types like you would like.

@realvictorprm, we have to be careful here, as it is primarily the reason that you shouldn't need access to the inner types. If you do, you should probably use record types (that's not to say that I do see use-cases for this).

I gave an example proposal - admittedly one that was barely thought through - that might have sufficed. Here it is again.

@isaacabraham, I am really trying to follow your logic, and I thought I responded in depth to your earlier proposal, and I agreed to your proposal and tried to show a way how it would work. What you copies above is a DU with an attribute annotation. I still don't see how the compiler can now in case you pass in an instance of the DU that you are guaranteed to only and ever only pass in that case of the DU.

And that's the whole problem here. I am sympathetic to the idea, but the whole discussion evolves around strong(er) type safety than is currently the case with loose matching (I mean, incomplete lh-side matching expressions).

The only way to pass a subgregated (as opposed to aggregated) type instance, you can only ever unwrap the type. Again, that is the current idea of DU's to begin with, you are supposed to write functions that unwrap the inner types.

Let's try to see what this issue is solving. Let's have a look at an omnipresent function:

let f someOption = 
    someOption
    |> Option.map (fun i -> i + i)   // type-safe access to inner type of DU Option

Since Option.map only ever needs to do something when the input contains Some value, we could consider this would look as follows with the new syntax:

// assuming what I think @isaacabraham meant (someOption is of type Option):
let map f (someOption: Option.Some) = f someOption.Value |> Some

But this begs the question, what happens if the input is None? Will it be ignored? How, what is the effect? Hence, currently, the compiler forces you to consider all cases and you must write this instead (common knowledge, I know, but let's repeat it for the example):

// a default implementation
let map f = function Some x -> f x |> Some | None -> None

(a bit to my own surprise, using current syntax uses less characters, but that's besides the point, as the OP showed, this gets more interesting if you have many cases).

Because of what seems to be the current train of thought (pass in an instance of a DU, but only act on one specific case of that DU, compiler must enforce this), we have to consider either how this can be achieved, or if there is an alternative way.

My alternative way as explained before should not be taken as a change to the proposal, but as a way of moving forward on the proposal, as it solves the question "what if the programmer uses this feature wrongly".

My approach cannot be used for scenarios like map, and deliberately so. However, it can be used to do something specific on a specific case-type:

let mapSome f (someSome: Option.Some) = f someSome |> Some
// usage:
let x = 
   match Some 15 with
   | None -> ...
   | Some x -> mapSome (fun x -> x + x)   // the type mapSome takes is guaranteed to take Option.Some, here int

I am still unsure if I translate your (@isaacabraham's) proposal correctly. If you know the answer to the question ("what if"), then please explain where my line of thought goes wrong. I hope the example above is a better explanation, based on a real-world example. Otherwise, apologies for repeating the obvious, I'm just not sure what the misunderstanding is, I have the strong feeling we are talking about the same thing with only minor differences in the details. But often, details matter, I'm afraid.

PS:

but Scala manages to do this - I'm sure theres a way that we can propose something that's workable.

What exactly does Scala manage and how? Does it give access to the inner types, or values, and what does it do when the DU does not hold that value (i.e., is of another case altogether)? Is it compile-time enforcement or runtime dynamic exception (as with Some.Value, for instance, which accesses the inner value as well, and it raises a runtime exception).

@isaacabraham

This comment has been minimized.

Show comment
Hide comment
@isaacabraham

isaacabraham Sep 21, 2017

OK, I'm jumping out of this. Just going round in circles here. Sorry - I couldn't explain it any better.

isaacabraham commented Sep 21, 2017

OK, I'm jumping out of this. Just going round in circles here. Sorry - I couldn't explain it any better.

@abelbraaksma

This comment has been minimized.

Show comment
Hide comment
@abelbraaksma

abelbraaksma Sep 21, 2017

@isaacabraham, that's a pity, as I would like to invite you to continue your efforts to explain what you mean, even to people like me who may not be apt enough to grasp it so easily.

At least tell me this. In my post above I ask whether I understand your solution correctly, and try to give an example of that. Is that correct? How would you use your solution with those examples? And do you think my approach is fundamentally different or equal to your approach?

My goal is just to know if your approach is deterministic or not, and if you believe it is, let us try to understand each other so we can work towards a more formal description of this feature. Without a deterministic proposal it will not be possible to implement in any language, and @KevinRansom or @dsyme won't be able to "approve in principle".

I'm sorry if I confuse you and/or don't understand you, but I would like to get somewhere and if we don't continue the discussion, we won't get any further for sure.

You said Scala "does this", so why not start there and point us to how Scala solves this problem? Then perhaps we can translate that back to F#.

abelbraaksma commented Sep 21, 2017

@isaacabraham, that's a pity, as I would like to invite you to continue your efforts to explain what you mean, even to people like me who may not be apt enough to grasp it so easily.

At least tell me this. In my post above I ask whether I understand your solution correctly, and try to give an example of that. Is that correct? How would you use your solution with those examples? And do you think my approach is fundamentally different or equal to your approach?

My goal is just to know if your approach is deterministic or not, and if you believe it is, let us try to understand each other so we can work towards a more formal description of this feature. Without a deterministic proposal it will not be possible to implement in any language, and @KevinRansom or @dsyme won't be able to "approve in principle".

I'm sorry if I confuse you and/or don't understand you, but I would like to get somewhere and if we don't continue the discussion, we won't get any further for sure.

You said Scala "does this", so why not start there and point us to how Scala solves this problem? Then perhaps we can translate that back to F#.

@realvictorprm

This comment has been minimized.

Show comment
Hide comment
@realvictorprm

realvictorprm Sep 21, 2017

Member

I agree, please show us. I'm looking forward to every suggestion for F# (and hope to find time implementing some).

Member

realvictorprm commented Sep 21, 2017

I agree, please show us. I'm looking forward to every suggestion for F# (and hope to find time implementing some).

@Lavinski

This comment has been minimized.

Show comment
Hide comment
@Lavinski

Lavinski Sep 22, 2017

Just noting, I'm still here. Just in a different timezone.

I'll attempt to clarify some things, firstly @abelbraaksma far as I can tell it seems like you might still be missing something here. I'll try and go through my use case. I also want to assure you that I believe my original suggestion can be implemented such that it is deterministic.

Secondly, thank you @isaacabraham I greatly appreciate your comments and suggestions. The attribute is a nice way of avoiding any breaking changes that could come from this feature.

Ok so I'll start with some terms to clarity

Case Label:
The case label is what we have in F# today, examples for type Result = | Success s | Failure f the Labels are Success and Failure.

Case Type:
The case type is not available at compile time today type Result = | Success s | Failure f the Case Types could be Result.CaseTypes.Success and Result.CaseTypes.Failure. (Note: I've added in the extra CaseTypes identifier to avoid confusion with the existing Result.Success which would be of type 't -> Result in F# today. There is probably be a better place to put the types, I'm not sure yet)

Now, an example of how I'd want to use this:

[<AllowCaseAssignment>]
type Events =
| StockAdded of int
| StockRemoved of int

Here I have a function that is strongly typed for one exact case. The signature would be Events.CaseTypes.StockAdded -> unit and trying to call the function with the Union Events could be an error.

let stockAddedHandler (stockAdded:  Events.CaseTypes.StockAdded) =
    let itemsAdded = (StockAdded n) // You would not get a warning here because there's only one case it could possibly be
    ()

Following that, you would need to downcast the Union to the Case Type in order to call such a function.

match events with
| StockAdded x -> stockAddedHandler (events :> Events.CaseTypes.StockAdded)
| StockRemoved x -> stockRemovedHandler  (events :> Events.CaseTypes.StockRemoved)

Note: You could also do this with reflection, imagine a dynamic list of message handlers. This is also useful for using DU to define messages and send them over the wire messages with regards to serialisation. The type int, in this case, or a tuple int * string is not the same as StockAdded.

Again I'm not sure Union.CaseTypes.CaseType is the solution but maybe something like that approach could work?

Lavinski commented Sep 22, 2017

Just noting, I'm still here. Just in a different timezone.

I'll attempt to clarify some things, firstly @abelbraaksma far as I can tell it seems like you might still be missing something here. I'll try and go through my use case. I also want to assure you that I believe my original suggestion can be implemented such that it is deterministic.

Secondly, thank you @isaacabraham I greatly appreciate your comments and suggestions. The attribute is a nice way of avoiding any breaking changes that could come from this feature.

Ok so I'll start with some terms to clarity

Case Label:
The case label is what we have in F# today, examples for type Result = | Success s | Failure f the Labels are Success and Failure.

Case Type:
The case type is not available at compile time today type Result = | Success s | Failure f the Case Types could be Result.CaseTypes.Success and Result.CaseTypes.Failure. (Note: I've added in the extra CaseTypes identifier to avoid confusion with the existing Result.Success which would be of type 't -> Result in F# today. There is probably be a better place to put the types, I'm not sure yet)

Now, an example of how I'd want to use this:

[<AllowCaseAssignment>]
type Events =
| StockAdded of int
| StockRemoved of int

Here I have a function that is strongly typed for one exact case. The signature would be Events.CaseTypes.StockAdded -> unit and trying to call the function with the Union Events could be an error.

let stockAddedHandler (stockAdded:  Events.CaseTypes.StockAdded) =
    let itemsAdded = (StockAdded n) // You would not get a warning here because there's only one case it could possibly be
    ()

Following that, you would need to downcast the Union to the Case Type in order to call such a function.

match events with
| StockAdded x -> stockAddedHandler (events :> Events.CaseTypes.StockAdded)
| StockRemoved x -> stockRemovedHandler  (events :> Events.CaseTypes.StockRemoved)

Note: You could also do this with reflection, imagine a dynamic list of message handlers. This is also useful for using DU to define messages and send them over the wire messages with regards to serialisation. The type int, in this case, or a tuple int * string is not the same as StockAdded.

Again I'm not sure Union.CaseTypes.CaseType is the solution but maybe something like that approach could work?

@realvictorprm

This comment has been minimized.

Show comment
Hide comment
@realvictorprm

realvictorprm Sep 22, 2017

Member

Before reading the comment above I want to propose what I think might work (next try 😄). Afterwards I'll talk about your last comment @Lavinski.

I think if we look into what @0x53A showed we can produce a valid syntax which is also fine in my opinion.

From his example:

type Configuration =
| Local
// Configuration.Network is an implicit type alias for itself, similar to the `Network` alias before
| Network of host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool     

// this function can take any of:
// * string * uint32 * TimeSpan *  bool, 
// * host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool 
// * Configuration.Network
// (in that way it works essentially the same as a type alias, without the extra definition)
let connectNetwork (configuration:Configuration.Network) =
    // do the stuff
    ()

let connect config =
    match config with
    | Local ->
        // do the stuff
        ()
    | Network network ->              // currently disallowed, unless you use a type alias
            connectNetwork network

I would just change a bit to clarify what's changing.

type Configuration =
| Local
// Configuration.Network is an implicit type alias for itself, similar to the `Network` alias before
| Network of host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool     

// this function can take any of:
// * string * uint32 * TimeSpan *  bool, 
// * host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool 
// * Configuration.Network
// (in that way it works essentially the same as a type alias, without the extra definition)
let connectNetwork (configuration:Configuration.Network) =
    // do the stuff
    ()

let connect config =
    match config with
    | Local : Configuration.Local-> // Unwrapping an "abstract class Configuration" to Local
        // do the stuff
        ()
    | Network network : Configuration.Network ->              // Unwrapping an "abstract class Configuration" to Network
            connectNetwork network

So the proposal would be:

Allow that Union cases are real accessible types in terms of polymorphism. Technical seen the union types would just declare an abstract class which only x types inherited(so its sealed afterwards).

Also, I think it's pretty fine that we want to be able to split the functions. @abelbraaksma do you think you understand now what is meant? @isaacabraham is my understanding here correct?


Questions are left:

  1. Maybe this could also introduce some speed-up for single case union types?
  2. A new syntax for such union types would be required, how should it look like?
    -> See the example above, it would break existing code which accepts only the abstract class.
Member

realvictorprm commented Sep 22, 2017

Before reading the comment above I want to propose what I think might work (next try 😄). Afterwards I'll talk about your last comment @Lavinski.

I think if we look into what @0x53A showed we can produce a valid syntax which is also fine in my opinion.

From his example:

type Configuration =
| Local
// Configuration.Network is an implicit type alias for itself, similar to the `Network` alias before
| Network of host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool     

// this function can take any of:
// * string * uint32 * TimeSpan *  bool, 
// * host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool 
// * Configuration.Network
// (in that way it works essentially the same as a type alias, without the extra definition)
let connectNetwork (configuration:Configuration.Network) =
    // do the stuff
    ()

let connect config =
    match config with
    | Local ->
        // do the stuff
        ()
    | Network network ->              // currently disallowed, unless you use a type alias
            connectNetwork network

I would just change a bit to clarify what's changing.

type Configuration =
| Local
// Configuration.Network is an implicit type alias for itself, similar to the `Network` alias before
| Network of host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool     

// this function can take any of:
// * string * uint32 * TimeSpan *  bool, 
// * host:string * port:uint32 * timeOut : TimeSpan * someOtherFlags : bool 
// * Configuration.Network
// (in that way it works essentially the same as a type alias, without the extra definition)
let connectNetwork (configuration:Configuration.Network) =
    // do the stuff
    ()

let connect config =
    match config with
    | Local : Configuration.Local-> // Unwrapping an "abstract class Configuration" to Local
        // do the stuff
        ()
    | Network network : Configuration.Network ->              // Unwrapping an "abstract class Configuration" to Network
            connectNetwork network

So the proposal would be:

Allow that Union cases are real accessible types in terms of polymorphism. Technical seen the union types would just declare an abstract class which only x types inherited(so its sealed afterwards).

Also, I think it's pretty fine that we want to be able to split the functions. @abelbraaksma do you think you understand now what is meant? @isaacabraham is my understanding here correct?


Questions are left:

  1. Maybe this could also introduce some speed-up for single case union types?
  2. A new syntax for such union types would be required, how should it look like?
    -> See the example above, it would break existing code which accepts only the abstract class.
@ghost

This comment has been minimized.

Show comment
Hide comment
@ghost

ghost Sep 25, 2017

I think I sympathize with both sides in this back and forth. On the one hand, I have situations where I get the compiler warning for pattern matching to a single DU case when I have already effectively exhausted the cases earlier or in other ways. So far I've lived with the warnings. Thanks to @isaacabraham for the idea of nesting more types. And I see the appeal in treating a single case like a type.

But I think I now understand @abelbraaksma 's formulation with the Option type. This just doesn't work, right?:

let f x : Option<float> = ... ; let g (x : Option.Some<float>) = ... ; let z = f x |> g;

Can't be statically checked. There's no typing ability to assure that f is not going to attempt to divide by 0 in F#. If you prefer:

let getPaymentMethod () : PaymentMethod = ... ; let processCreditCard (cc : PaymentMethod.CreditCard) = ... ; let f = getPaymentMethod () |> processCreditCard;

So you'd have to have functions with return types either of a DU type or of a single case of DU "type". Does that work in type theory?

(Usually I only partially follow these discussions at best, between use case, implementation, and type theory. Maybe I'm getting it here, maybe not.)

ghost commented Sep 25, 2017

I think I sympathize with both sides in this back and forth. On the one hand, I have situations where I get the compiler warning for pattern matching to a single DU case when I have already effectively exhausted the cases earlier or in other ways. So far I've lived with the warnings. Thanks to @isaacabraham for the idea of nesting more types. And I see the appeal in treating a single case like a type.

But I think I now understand @abelbraaksma 's formulation with the Option type. This just doesn't work, right?:

let f x : Option<float> = ... ; let g (x : Option.Some<float>) = ... ; let z = f x |> g;

Can't be statically checked. There's no typing ability to assure that f is not going to attempt to divide by 0 in F#. If you prefer:

let getPaymentMethod () : PaymentMethod = ... ; let processCreditCard (cc : PaymentMethod.CreditCard) = ... ; let f = getPaymentMethod () |> processCreditCard;

So you'd have to have functions with return types either of a DU type or of a single case of DU "type". Does that work in type theory?

(Usually I only partially follow these discussions at best, between use case, implementation, and type theory. Maybe I'm getting it here, maybe not.)

@abelbraaksma

This comment has been minimized.

Show comment
Hide comment
@abelbraaksma

abelbraaksma Sep 30, 2017

I think if we look into what @0x53A showed we can produce a valid syntax which is also fine in my opinion.
From his example:

Ahum, I think you (@realvictorprm) accidentally copied my example there :P.

So you'd have to have functions with return types either of a DU type or of a single case of DU "type". Does that work in type theory?

Thanks @enkerom, indeed, that was my point on static checking.

The answer is, given the present situation: no, it is not possible, because if you create a single-case DU, you create an instance of that DU-type, not of the case-type. So, Some x returns an Option<_>, not a type like Option.Some<_> or so. They don't exist.

However, with this proposal, they would come to exist, so there should be a way to deal with them (if at all possible).

Maybe this could also introduce some speed-up for single case union types?

@realvictorprm, no, it doesn't, as these are already optimized away.

A new syntax for such union types would be required, how should it look like?
-> See the example above, it would break existing code which accepts only the abstract class.

@realvictorprm, we will need something better, then.

Any solution (if we want this to get accepted) cannot change existing behavior. And if we introduce new syntax, we must be very careful that it is unambiguous and deterministic and is currently not valid syntax (i.e., there won't be a backward compatibility issue if we make syntax legal that is currently not legal). Expanding the (very complex) type inference system is in itself not impossible and should not deter us from wanting to improve it.

I'll attempt to clarify some things, firstly @abelbraaksma far as I can tell it seems like you might still be missing something here.
<snip />
you would need to downcast the Union to the Case Type in order to call such a function.

@Lavinski, yes, that is certainly possible. Thanks for taking the time to explain it in more detail.

One of my points I tried to address was precisely this one, and I don't think that I made that very clear ;). This (from your last post) will never work, there is no parent-child relationship.

 (events :> Events.CaseTypes.StockAdded)

Also, this syntax suggests, I think, the following reasoning form the programmer's perspective, talking to the compiler:

  • Compiler, here I have an object events that I know if of type Events
  • And, compiler, I have just assessed that I know what is inside that object, it is Events.StockAdded
  • So, please convert this to its inner type for me, Events.CaseTypes.StockAdded

Now, this is perfectly sensible reasoning, it is roughly the same as box x :?> TypeY. However, as with :?> you do not gain type safety, you lose it, as there is no way the compiler can tell that you, the programmer, are right.

And from your very own example, it goes wrong there, because you had (comments mine):

match events with
| StockAdded x -> 
    stockAddedHandler (events :> Events.CaseTypes.StockAdded)   // OK
| StockRemoved x -> 
    stockAddedHandler  (events :> Events.CaseTypes.StockRemoved)  // oops! Runtime error?

Hence my lengthy explanations: I like this whole idea (the original idea, I mean, as in easier and type safe access to individual cases), but only if it adds to type safety, not if we lose it. This could be achieved by new syntax, here's one other suggestion taking your last example:

match events with
| of StockAdded as x -> 
    stockAddedHandler x   // OK, function stockAddedHandler can handle this case-type

| of StockRemoved as x -> 
    stockAddedHandler x  // compile-time error, type mismatch

In this, deliberately verbose syntax (and not intended as a real proposal), I take the idea of the current way you can use patterns for dynamically matching against a runtime type. I.e., this whole idea has most merit if it could somehow have the same morphological principles as this:

match x with :? Foo as foo -> ... | :? Bar as bar -> ...

So, in the syntaxified example above, I do of Case-Type-Here as variable-Here -> .... Then, variable=-Here, or x above, has the static type of CaseTypes.StockAdded or CaseTypes.StockRemoved.

And now, at least, the compiler knows what to do and you gain type safety.

abelbraaksma commented Sep 30, 2017

I think if we look into what @0x53A showed we can produce a valid syntax which is also fine in my opinion.
From his example:

Ahum, I think you (@realvictorprm) accidentally copied my example there :P.

So you'd have to have functions with return types either of a DU type or of a single case of DU "type". Does that work in type theory?

Thanks @enkerom, indeed, that was my point on static checking.

The answer is, given the present situation: no, it is not possible, because if you create a single-case DU, you create an instance of that DU-type, not of the case-type. So, Some x returns an Option<_>, not a type like Option.Some<_> or so. They don't exist.

However, with this proposal, they would come to exist, so there should be a way to deal with them (if at all possible).

Maybe this could also introduce some speed-up for single case union types?

@realvictorprm, no, it doesn't, as these are already optimized away.

A new syntax for such union types would be required, how should it look like?
-> See the example above, it would break existing code which accepts only the abstract class.

@realvictorprm, we will need something better, then.

Any solution (if we want this to get accepted) cannot change existing behavior. And if we introduce new syntax, we must be very careful that it is unambiguous and deterministic and is currently not valid syntax (i.e., there won't be a backward compatibility issue if we make syntax legal that is currently not legal). Expanding the (very complex) type inference system is in itself not impossible and should not deter us from wanting to improve it.

I'll attempt to clarify some things, firstly @abelbraaksma far as I can tell it seems like you might still be missing something here.
<snip />
you would need to downcast the Union to the Case Type in order to call such a function.

@Lavinski, yes, that is certainly possible. Thanks for taking the time to explain it in more detail.

One of my points I tried to address was precisely this one, and I don't think that I made that very clear ;). This (from your last post) will never work, there is no parent-child relationship.

 (events :> Events.CaseTypes.StockAdded)

Also, this syntax suggests, I think, the following reasoning form the programmer's perspective, talking to the compiler:

  • Compiler, here I have an object events that I know if of type Events
  • And, compiler, I have just assessed that I know what is inside that object, it is Events.StockAdded
  • So, please convert this to its inner type for me, Events.CaseTypes.StockAdded

Now, this is perfectly sensible reasoning, it is roughly the same as box x :?> TypeY. However, as with :?> you do not gain type safety, you lose it, as there is no way the compiler can tell that you, the programmer, are right.

And from your very own example, it goes wrong there, because you had (comments mine):

match events with
| StockAdded x -> 
    stockAddedHandler (events :> Events.CaseTypes.StockAdded)   // OK
| StockRemoved x -> 
    stockAddedHandler  (events :> Events.CaseTypes.StockRemoved)  // oops! Runtime error?

Hence my lengthy explanations: I like this whole idea (the original idea, I mean, as in easier and type safe access to individual cases), but only if it adds to type safety, not if we lose it. This could be achieved by new syntax, here's one other suggestion taking your last example:

match events with
| of StockAdded as x -> 
    stockAddedHandler x   // OK, function stockAddedHandler can handle this case-type

| of StockRemoved as x -> 
    stockAddedHandler x  // compile-time error, type mismatch

In this, deliberately verbose syntax (and not intended as a real proposal), I take the idea of the current way you can use patterns for dynamically matching against a runtime type. I.e., this whole idea has most merit if it could somehow have the same morphological principles as this:

match x with :? Foo as foo -> ... | :? Bar as bar -> ...

So, in the syntaxified example above, I do of Case-Type-Here as variable-Here -> .... Then, variable=-Here, or x above, has the static type of CaseTypes.StockAdded or CaseTypes.StockRemoved.

And now, at least, the compiler knows what to do and you gain type safety.

@ghost

This comment has been minimized.

Show comment
Hide comment
@ghost

ghost Sep 30, 2017

I'm not knowledgeable enough to answer this but it kind of feels like that having compilation constraints on sum type cases would amount to having dependent types-? (And which f#'s present type system can't do.)

ghost commented Sep 30, 2017

I'm not knowledgeable enough to answer this but it kind of feels like that having compilation constraints on sum type cases would amount to having dependent types-? (And which f#'s present type system can't do.)

@Lavinski

This comment has been minimized.

Show comment
Hide comment
@Lavinski

Lavinski Oct 1, 2017

@abelbraaksma thank you for your response. I see that my mistake with stockAddedHandler highlights how this leads to less type safety in this case. When I wrote the original proposal my focus was on using the CaseTypes without considering matching in a typesafe manner. Your example which I've copied below would indeed allow us to ensure type-safety when matching, and imho looks like a nice approach to the issue (but if there is a way to write it which is less verbose that'd be great as well).

match events with
| of StockAdded as x -> 
    stockAddedHandler x 

I originally found the desire to use CaseTypes while using reflection which "bypasses" the issue (because reflection is never type-safe) of matching the cases (something that I would still like to consider in the design of this feature). This is what I was referring to in my last reply when I mentioned serialisation and message handlers.

Lavinski commented Oct 1, 2017

@abelbraaksma thank you for your response. I see that my mistake with stockAddedHandler highlights how this leads to less type safety in this case. When I wrote the original proposal my focus was on using the CaseTypes without considering matching in a typesafe manner. Your example which I've copied below would indeed allow us to ensure type-safety when matching, and imho looks like a nice approach to the issue (but if there is a way to write it which is less verbose that'd be great as well).

match events with
| of StockAdded as x -> 
    stockAddedHandler x 

I originally found the desire to use CaseTypes while using reflection which "bypasses" the issue (because reflection is never type-safe) of matching the cases (something that I would still like to consider in the design of this feature). This is what I was referring to in my last reply when I mentioned serialisation and message handlers.

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