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

Literals as types #656

Closed
5 tasks done
jbeeko opened this issue Apr 5, 2018 · 10 comments
Closed
5 tasks done

Literals as types #656

jbeeko opened this issue Apr 5, 2018 · 10 comments

Comments

@jbeeko
Copy link

jbeeko commented Apr 5, 2018

Add literals as types, als Typescript See also literal types in type script, https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types

Also relates to erased type-tagged unions #538


This language suggestion proposes adding Enumerate Unions. These are union types were rather than having different cases each of which defines any number of items the union is an enumeration of the same data for each case.

One example domain currencies. There is a fixed list of these and a handful data elements are associated with each currenty:

  • name
  • number
  • minor units

Syntax

A proposed syntax for EnumerateUnions is:

type Currency of Name: string; Number: int =
| CAD Name = "CAD"; Number = 123
| USD Name = "USD"; Number = 223
| GBP Name = "GBP"; Number = 444

printfn "The number of currency: '%s' is: %i" CAD.Name CAD.Number

This is very succinct (4 lines vs about 16 using the current languate), easy to understand, models all the data and prevents the construction of invalid currencies.

Here is a more complex example modeling ISO currency https://en.wikipedia.org/wiki/ISO_4217 and country codes https://en.wikipedia.org/wiki/ISO_3166-1.

type Country of Name: string; Code: string; LongCode: string; IsoNumber: int; Independant: bool; Currencies : Currency list =
| CA Name = "Canada"; Code = "CA"; LongCode = "CAN"; Number = 124; Independant = true; Currencies = [CAD]
| GB Name = "United Kingdom of Great Britain and Northern Ireland"; Code = "GB"; LongCode = "GBR"; Number = 826; Independant = true; Currencies = [CAD]
| US Name = "United States of America"; Code = "US"; LongCode = "USA"; Number = 840; Independant = true; Currencies = [USD]
| VG Name = "British Virgin Islands"; Code = "VG"; LongCode = "VGB"; Number = 92; Independant = false; Currencies = [USD]
| IO Name = "British Indian Ocean Teritory"; Code = "IO"; LongCode = "IOT"; Number = 86; Independant = false; Currencies = [USD; GBP]

and Currency of Name: string; Number: int; Digits: int UsedIn: Country list =
| CAD Code = "CAD" Name = "Canadian Dollar"; Number = 124; Digits = 2; UsedIn = [CA]
| USD Code = "USD" Name = "United States Dollar"; Number = 840; Digits = 2; UsedIn = [US; VG; IO]
| GBP Code = "GBP" Name = "Pound sterling"; Number = 826; Digits = 2; UsedIn = [GB; IO]

printfn "The currency: '%s' is used in: %A" USD.Name USD.UsedIn
printfn "The Country: '%s' uses the currencies: %A" IO.Name IO.Currencies

Again a lot of information is represented very clearly and compactly in a typesafe way.

Uses

Where would this be useful? All cases where there domains contain an infrequently changing enumeration of items with richer data than just strings or integers. These may be globally defined lists (currencies) or specific to the application. Examples are:

  • Chemical elements (possibly including isotopes or isomers)
  • Countries
  • Other geographic elements such as mountains, states or cities
  • Planets
  • Currencies
  • BloodTypes
  • Languages
  • Slow chaning lists of application elements such as suppliers or parts
  • Positions in organizations
  • As an alternative to databases where the data set is small (1000 records) and changes infrequently, or on a controlled release schedule.
  • Defining menu items or actions

Related Proposals

Open Proposal #564

Is a proposal to ease access to data elements common to all cases. Those elements are then accessible as properties using dot notation.

type ShoppingCart = 
| EmptyCart of AppliedDiscountCode: string
| CartWithItems of AppliedDiscountCode: string * Items: string list
| CompletedCart of AppliedDiscountCode: string * Items: string list * CalculatedTotal: decimal

v.AppliedDiscount //where v is of type ShoppingCart

This comment #564 (comment) suggests that commen field be defined explicitly like this:

type ShoppingCart =
    val DiscountCode: string
    | EmptyCart
    | CartWithItems of Items: string list
    | CompletedCart of Items: string list * CalculatedTotal: decimal

with constructor like this: EmptyCart(DiscountCode=code).

The two proposals could perhaps be merged like this:

type ShoppingCart =
    val DiscountCode: string
    | EmptyCart of DiscountCode = "Free"
    | CartWithItems of DiscountCode = "Moderate"; Items: string list
    | CompletedCart of DiscountCode = "Full"; Items: string list * CalculatedTotal: decimal

In this case if the value is defined in the defintion that value is fixed for all "instantiations" of the case. One issue with this (other than being more complex) is that it blurs the line between "constant values" and dynamic values.

In this syntax the simple currency example is as follows:

type Currency =
val Name: string
val Number: int 
| CAD Name = "CAD"; Number = 123
| USD Name = "USD"; Number = 223
| GBP Name = "GBP"; Number = 444

printfn "The number of currency: '%s' is: %i" CAD.Name CAD.Number

Closed Proposal #558

Suggests being able to define constant data for DU like so:

type Planets =
| MERCURY of {Mass=3.303e+23; Radius=2.4397e6)
| VENUS of {Mass=4.869e+24; Radius=6.0518e6),
| EARTH of {Mass=5.976e+24; Radius=6.37814e6),
| MARS of {Mass=6.421e+23; Radius=3.3972e6),
| JUPITER of {Mass=1.9e+27; Radius=7.1492e7),
| SATURN of {Mass=5.688e+26; Radius=6.0268e7),
| URANUS of {Mass=8.686e+25; Radius=2.5559e7),
| NEPTUNE of {Mass=1.024e+26; Radius=2.4746e7);

However it does not suggest a way to make it easier to access the common set of data.

Existing approaches to this

Unions without Data

type Currency = 
| CAD
| USD
| GBP

This means the requirement that all currencies are enumerated but the data associated with Currencies is not modeled. The code can be obtained as a string using reflection but that is all.

Enums

type Currency = 
| CAD=123
| USD=223
| GBP=444

In this case the integer value is available as the data representing the Currency but again not all the needed data.

Link unions cases with records via let

type Details = {
    Code : Currency
    Name : string
    Number: int
  } 
type Currency =
    Curency of Details

let CAD = Currency {Name = "CAD"; Number = 123}
let USD = Currency {Name = "USD"; Number = 223}
let GBP = Currency {Name = "GBP"; Number = 444}

printfn "The number of currency: '%s' is: %i" CAD.Name CAD.Number

This supports all the data but is verbose. And to hide the Currency constructor to prevent the creation of invalid currencies will make it even more verbose.

Link unions cases with records via static members

type Currency =
| CAD 
| USD
| GBP
with 
static private member AllDetails =
  [
  {Code = CAD; Name = "CAD"; Number = 123}
  {Code = USD; Name = "USD"; Number = 223}]
  {Code = GBP; Name = "GBP"; Number = 444}]
member this.Details = 
    Currency.AllDetails |> List.find (fun e -> e.Code = this)
member this.Name = 
   this.Details.Name
member this.Number = 
   this.Details.Number

and Details = {
Code : Currency
Name : string
Number: int
} 

printfn "The number of currency: '%s' is: %i" CAD.Name CAD.Number

Again a bit verbose and tedious. It also suffers from the disadvantage of needing to keep the Union of currencies in line with the list of details.

This could also be done using a module to hold a function with AllDetails.

Pros and Cons

The advantages of making this adjustment to F# are that modeling complex static data in a typesafe way becomes very straight forward. See the example modeling Countries and Currencies above. Also see the examples of modeling the same domain with the current language definition.

The disadvantages of making this adjustment to F# are ...

Extra information

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

Related suggestions: #564, #558

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 (a least not as far as I can tell, which is not far)
  • I or my company would be willing to help implement and/or test this (though my skills are limited)
@jbeeko jbeeko closed this as completed Apr 5, 2018
@jbeeko jbeeko reopened this Apr 5, 2018
@eugbaranov
Copy link

Alternative approach could be:

type CurrencyCode = CAD | USD | GBP
type CurrencyDetails = { Name: string; Number: int; Digits: int }
module Currency =
    let details = function
    | CAD -> { Name = "Canadian Dollar"; Number = 124; Digits = 2 }
    | USD -> { Name = "United States Dollar"; Number = 840; Digits = 2 }
    | GBP -> { Name = "Pound sterling"; Number = 826; Digits = 2 }
let { Name = name; Number = number } = CAD |> Currency.details
printfn "The number of currency: '%s' is: %i" name number

type CountryCode = CA | GB | US | VG | IO
type CountryDetails = { Name: string; Code: string; LongCode: string; Number: int; Independant: bool; Currencies : CurrencyCode list }
module Country =
    let details = function
    | CA -> { Name = "Canada"; Code = "CA"; LongCode = "CAN"; Number = 124; Independant = true; Currencies = [CAD] }
    | GB -> { Name = "United Kingdom of Great Britain and Northern Ireland"; Code = "GB"; LongCode = "GBR"; Number = 826; Independant = true; Currencies = [CAD] }
    | US -> { Name = "United States of America"; Code = "US"; LongCode = "USA"; Number = 840; Independant = true; Currencies = [USD] }
    | VG -> { Name = "British Virgin Islands"; Code = "VG"; LongCode = "VGB"; Number = 92; Independant = false; Currencies = [USD] }
    | IO -> { Name = "British Indian Ocean Teritory"; Code = "IO"; LongCode = "IOT"; Number = 86; Independant = false; Currencies = [USD; GBP] }

let { Name = countryName; Currencies = currencies } = IO |> Country.details
printfn "The '%s' country uses the currencies: %A" countryName currencies

@charlesroddie
Copy link

This proposal isn't needed.

type Currency =
    CAD | USD | GBP
    member t.Name   = match t with CAD -> "CAD" | USD -> "USD" | GBP -> "GBP"
    member t.Number = match t with CAD -> 123   | USD -> 223   | GBP -> 444

@jwosty
Copy link
Contributor

jwosty commented Apr 10, 2018

@charlesroddie that's true, but there could be value in providing the suggested (or similar) syntactic sugar. If it would get used enough, I think it could be a valuable shorthand. Though I suggest we also add the with and and keywords and make it a block, which would let it be a bit more syntactically flexible. For example, you'd be able to unambiguously break it out on separate lines like so:

type Currency =
    | CAD
        with Name = "CAD"
        and Number = 123
    | ...

It reads kinda nicely, too. My question then would be, would we also want to be able to have the associated value depend on values contained by the DU case? For example, I'd find the following valuable:

type Shape =
    | Rectangle of width:int * height: int
        with Area = width * height
    | RightTriangle of base:int * height:int
        with Area = width * height / 2

This would imply that this feature is actually a shorthand for properties (or maybe methods?).

@jwosty
Copy link
Contributor

jwosty commented Apr 10, 2018

Continuing on my last comment, these shorthand properties could instead take the associated arguments as parameters, to handle things like nested tuples nicely, at the expense of slightly cluttering simpler examples (like Shape):

type Geometry =
| Line of vec1:(float * float) * vec2:(float * float)
    with Length((x1,y1),(x2,y2)) = sqrt (((x1 - x2) ** 2.) + ((y1 - y2) ** 2.))

That much said, I think this is interesting to consider.

@Luiz-Monad
Copy link

Luiz-Monad commented Aug 13, 2019

I propose using anonymous records now that we have them.
Something like that.

type Planets =
| MERCURY of {|Mass=3.303e+23; Radius=2.4397e6|}
| VENUS of {|Mass=4.869e+24; Radius=6.0518e6|}
| EARTH of {|Mass=5.976e+24; Radius=6.37814e6|}
| MARS of {|Mass=6.421e+23; Radius=3.3972e6|}
| JUPITER of {|Mass=1.9e+27; Radius=7.1492e7|}
| SATURN of {|Mass=5.688e+26; Radius=6.0268e7|}
| URANUS of {|Mass=8.686e+25; Radius=2.5559e7|}
| NEPTUNE of {|Mass=1.024e+26; Radius=2.4746e7|}

So, in this way no new syntax is needed, we can use what we already have.

@dsyme
Copy link
Collaborator

dsyme commented Jun 16, 2022

Mroe realistic would be a typescript-like combination of literals-as-types plus adhoc unions, e.g. "CDN" | "USD" | int

@dsyme dsyme changed the title Unions as Better Enumerations Literals as types Jun 16, 2022
@3xau1o
Copy link

3xau1o commented Jul 22, 2022

Rescript used a special syntax for this purpose, the hashtag intentionally tries to distinguish syntax of literal types from literal values, still their semantics remain as in typescript.

type color = [#red | #green | #blue]

@3xau1o
Copy link

3xau1o commented Jul 22, 2022

Mroe realistic would be a typescript-like combination of literals-as-types plus adhoc unions, e.g. "CDN" | "USD" | int

that's the way Scala embraced

// the following constant can only store ints from 1 to 3
val three: 1 | 2 | 3 = 3

val one: 1 = 1                     // val declaration
def foo(x: 1): Option[1] = Some(x) // param type, type arg
def bar[T <: 1](t: T): T = t       // type parameter bound
foo(1: 1)                          // type ascription

@kspeakman
Copy link

Adding an alternative. We'll use static data, then it typically gets moved to a DB after a few updates. I use this approach which works for both.

type Item = { ... }
let items : Item list = ... // loaded or statically listed
let codeDict = items |> List.map (fun x -> x.Code, x) |> dict

// once you know the lookup can't fail
let item = codeDict.[code]

If it's static data, Code could be a DU or module constants, so the indexer above is safe to use. But this doesn't work if data is moved to a DB -- data changes could cause crashes. For me, something like Code typically comes from outside my program (e.g. front end) and is validated by checking if it's a dictionary key. So using the indexer is not a problem.

Contextually, this approach is for small amounts of frequently-used or expensive-to-compute data. Otherwise it's easier to load a specific item from db when needed.

@kerams kerams mentioned this issue Oct 21, 2022
5 tasks
@dsyme
Copy link
Collaborator

dsyme commented Oct 26, 2022

Closing preferring #1195 for now

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

8 participants