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

Implicit mapping between Normalized and Anonymous Record types #1266

Closed
4 of 5 tasks
Eliemer opened this issue Mar 30, 2023 · 6 comments
Closed
4 of 5 tasks

Implicit mapping between Normalized and Anonymous Record types #1266

Eliemer opened this issue Mar 30, 2023 · 6 comments

Comments

@Eliemer
Copy link

Eliemer commented Mar 30, 2023

I propose we allow type directed, implicit mapping (a la automapper) for functions that receive or output anonymous records, without actually changing how anonymous records behave, are created, and are represented in compiled code. I'm envisioning more of a macro rather than a wholly different approach to types.

The existing way of approaching anonymous records in F# is explicitly and manually mapping each property in the construction of the resulting record.

// Some large normalized type
type CoolRecordType =
    { A: int
      B: bool
      C: string
      D: float }

let myCoolRecord =
    { A = 0
      B = false
      C = "Hello"
      D = 1.04 }

// A small normalized type
type TinyRecord = { B: bool }

// Some anonymous record as input type
let someInputFun (x: {| A: int; B: bool |}) = x.A

// Anonymous record as output type
let someOutputfun (x: bool) = {| A = 1; B = x |}

// == Explicit Mapping ==
let inputPolymorphism =
    // normalized type is supertype of anon record
    let input =
        {| A = myCoolRecord.A
           B = myCoolRecord.B |}

    someInputFun input

let outputPolymorphism: TinyRecord =
    // normalized type is subtype of anon record
    let output = someOutputfun false

    { B = output.B }

My proposition is to use the available type information to make implicit mappings if and only if the anonymous and normalized record types are strictly subtypes one of the other. E.g., Large type gets mapped into a smaller type. This should only work if every mapped field is of the exact same name and type. E.g., { A : int } = {| A : int |}, { A : int } <> { A : float }. If properties dont match, this should result in a compile-time type mismatch error, or an FS0764: No assignment given for field X of type Y.

Using the types above, the proposal would look like

// == Implicit Mapping ==
let inputPolymorphism:  CoolRecordType = 
    // anonymous record is implicitly extracted from `myCoolRecord`
    someInputFun myCoolRecord

let outputPolymorphism: TinyRecord = 
    // `TinyRecord` is implicitly constructed from anonymous record
    someOutputfun false

It's also possible to achieve this pseudo structural typing using reflection, but we lose compile-time errors for type mismatches.

Pros and Cons

The advantages of making this adjustment to F# are ...
Enables something that resembles row polymorphism, without altering IL representations, ideally. It also reduces lines of code in such cases where mapping would result in boilerplate and encourages the usage of anonymous records.

The disadvantages of making this adjustment to F# are ...
Enables row polymorphism, contrary to anon records design principle, as stated in the original RFC for anonymous records here, but I would like to argue that this was chosen to simplify IL representation and maintain an achievable goal for that proposal. Now that anon records were successfully introduced into the language, this change seems like a good QoL addition that shouldn't disrupt the existing implementation.

Extra information

Estimated cost (XS, S, M, L, XL, XXL): Naively, I wanna say that this is a small feature.

Related Suggestions:
#1251
#1253 (sorta, implies implicit mappings as well)

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

For Readers

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

@Lanayx
Copy link

Lanayx commented Apr 1, 2023

If #1253 is implemented you'll be able to write

let inputPolymorphism () = 
    // anonymous record is implicitly extracted from `myCoolRecord`
    someInputFun {| ...myCoolRecord |}
    
let outputPolymorphism: TinyRecord = 
    // `TinyRecord` is implicitly constructed from anonymous record
    { ...(someOutputfun false) }    

@dsyme
Copy link
Collaborator

dsyme commented Apr 13, 2023

Yes I agree this is really a use case for #1253

@dsyme dsyme closed this as completed Apr 13, 2023
@Happypig375
Copy link
Contributor

@Lanayx Would the spread operator really allow this syntax without the surrounding brackets?

@Lanayx
Copy link

Lanayx commented Apr 15, 2023

@Happypig375 I don't see why not :) You can write do that today with other operators, for example string -2, or take the popular UMX library where you write string %x
If you are talking about the second example - I don't see how it may work without brackets

@Eliemer
Copy link
Author

Eliemer commented Apr 16, 2023

I think @Happypig375 is talking about this

let x = {| ... y |}
// vs above
let x = ... y

@Lanayx
Copy link

Lanayx commented Apr 17, 2023

You are absolutely right, the curly braces have to be there, I've updated my example

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants