fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
345 stars 21 forks source link

Constructor syntax for discriminated unions type defs #1376

Closed utenma closed 2 months ago

utenma commented 2 months ago

I propose to allow constructor syntax on DUs without dropping existing one

Currently F# DUs use Ocaml syntax exclusively

type Shape =
    | Rectangle of width : float * length : float
    | Circle of radius : float
    | Prism of width : float * float * height : float

That is inconsistent with usages elsewhere for instance both instantiation and pattern matching use constructor call syntax

let rect = Rectangle(length = 1.3, width = 10.0)
let circ = Circle (1.0)
let prism = Prism(5., 2.0, height = 3.0)

let getShapeWidth shape =
    match shape with
    | Rectangle(width = w) -> w
    | Circle(radius = r) -> 2. * r
    | Prism(width = w) -> w

Other ML languages already allow constructor syntax for union type definitions, for instance rescript, previously known as ReasonML

// Rescript lang snippet
type account =
  | None
  | Instagram(string)
  | Facebook(string, int)

Pros and Cons

The constructor syntax on DUs type definition is cleaner, consistent with F# instantiation and pattern matching

Suggestion is to allow constructor syntax as alternative syntax without dropping existing one

Affidavit (please submit!)

Please tick these items by placing a cross in the box:

Please tick all that apply:

For Readers

If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

kerams commented 2 months ago

I don't think this would fly even as a proposal for the original syntax for DU definitions. What you're suggesting is inconsistent with tuple constructors and method invocation syntax - the constituent elements of (x, y, z) are values, but with | Facebook (string, int) you're using a comma-separated list of types (or field name, type pairs) . Asterisks are the separator in tuple types, so I think at best one could argue for the removal of of, but even that would have issues (like when you'd want to use an actual tuple for a DU case field).

utenma commented 2 months ago

@kerams that was a snippet from Rescript lang, however it's not hard to imagine it for F#, even with tuples would be like a regular function signature

let getDistance ((x1,y1): float*float) ((x2,y2): float*float) =
    (x1*x2 - y1*y2) 

just replace the let with union bar | and remove the function body

brianrourkeboll commented 2 months ago

Is this not https://github.com/fsharp/fslang-suggestions/issues/1129, more or less?

utenma commented 2 months ago

@brianrourkeboll that one is specific to tuple syntax, this one would be to wrap in parenthesis instead of prefixing with of

brianrourkeboll commented 2 months ago

@brianrourkeboll that one is specific to tuple syntax, this one would be to wrap in parenthesis instead of prefixing with of

All right, thanks for the clarification. However:

  1. This proposal does share with #1129 the use of parentheses and , instead of * to specify a tuple-like type signature.
  2. I doubt this proposal would be implemented unless #1129 were also implemented.

If #1129 were implemented, syntax like

type U =
    | A of (int, string)

would presumably already be allowed.

So I guess this proposal could be construed as an add-on to #1129 in the sense that both are trying to bring the syntax of types (and type signatures) closer to the syntax of values (expressions/patterns).

utenma commented 2 months ago

The parenthesis/comma tuple proposal would fix the main pain point, as mentioned by @brianrourkeboll the proposed syntax on https://github.com/fsharp/fslang-suggestions/issues/1129 seems fairly decent

Tarmil commented 2 months ago

If #1129 were implemented, syntax like

type U =
    | A of (int, string)

would presumably already be allowed.

It would, but it wouldn't mean the same thing. Just like currently, the following two are different:

type U = | A of int * string
type U' = | A of (int * string)

The latter allocates a tuple, whereas the former stores the two values directly in the A.

That being said, I can't imagine comma syntax being implemented only for some uses of the asterisk type operator. That would be a crazy inconsistency. So if (int, string) ever makes it into the language, then surely so will some form of comma syntax for unions, whether it be A of int, string or A(int, string) as proposed here.

brianrourkeboll commented 2 months ago

@Tarmil Yeah, I understand that distinction in the current syntax, but I don't think it could be the same way in #1129, since the following are currently legal:

match 1, 2 with
| x : int, y -> x + y // y is a value of type int.
let f (x : int, y) = x + y // y is a value of type int.

I.e., at least one set of parentheses would always be required for backwards-compatibility, since in many scenarios symbols after , are parsed as values, not types.