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 types on record elements #747

Closed
5 tasks done
ShalokShalom opened this issue Jun 10, 2019 · 16 comments
Closed
5 tasks done

Implicit types on record elements #747

ShalokShalom opened this issue Jun 10, 2019 · 16 comments

Comments

@ShalokShalom
Copy link

ShalokShalom commented Jun 10, 2019

Implicit types of record elements

I suggest inferring types in record types as such:

type Details =
    { Name
      Description }

In this example, Name has the type Name and Description the type Description on the surface.

The existing way is obviously so:

type Details =
    { Name : Name
      Description : Description }

Pros and Cons

This obviously increases the type safety, reduces the clutter and makes our code distinctive.
Using primitive types on the user level contradicts the sense of type checking.

I can see the chance to increase clarity in our compiler messages.

It is also very declarative and clean to read in our type signatures.
I currently see zero serious issues here, besides of adoption and the actual integration itself.

Optional optimisation:

In order to keep up with the performance, we can use inferred primitive types in the actual compilation step and show the inferred custom type just to the user and the type checker.

So you can illusionary cover such primitive types with the here mentioned custom type.

It is possible, that the current compiler simply does struggle to infer the type of elements in record types, so this could be the actual work to do.

Extra information

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

S - L

Affidavit

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like the 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 am willing to advertise and test this
@Happypig375
Copy link
Contributor

How would you create an instance of such a record?

@ShalokShalom
Copy link
Author

Such as described in the first code example? You just leave it free, if you like to infer it.
Otherwise, you just describe it as you are used to.

@Happypig375
Copy link
Contributor

No, the first code example is defining the type. I'm asking for the equivalent code for

{ Name = "John Doe"; Description = "An ordinary man" }

. Will this be written the same way?

@theprash
Copy link

I think you mean the existing way is to do this is:

type Details =
    { Name : Name
      Description : Description }

You might want to edit that to avoid confusion.

I don't think this is a bad idea. But I don't think it's worth introducing to F# yet another instance of multiple ways to do the same thing, and to be slightly less explicit, just to save a few characters. It's another thing for people new to the language to learn that they won't see very often because it's only in code using the latest versions of F#.


Note:

One way of getting something similar but not quite the same with existing features is to use a single-case DU and tuple. It's not as convenient to access the component parts as it is with records.

type Name = Name of string
type Description = Description of string

type Details = Details of Name * Description

@ShalokShalom
Copy link
Author

ShalokShalom commented Jun 10, 2019

@Happypig375 Yes, correct.
@theprash Thanks. I see your point. New users should not take too many looks into existing code at all.

At least this is my impression. It scares the hell out of me in general, while less so in F#.

This whole topic goes beyond this suggestion and I think it's important since people get used to custom types. Primitive types could be a compilation detail, nobody needs to deal with them in the code. YMMV

@Happypig375
Copy link
Contributor

So you have

type Details =
    { Name
      Description }
let a : string = "John Doe"
let b : Details = { Name = a; Description = "An ordinary man." }
let c : Name (* or Details.Name? *) = b.Name

For c,
Will string members be exposed? (c.Length)
Can it be passed around as a string? (String.length c)
Can it be equated with other strings? (c = "John Doe")

@charlesroddie
Copy link

I had to upgrade my internal compiler to understand this thread.

The title and first paragraph mention a way to conflate record member names and type names. So "on the surface" this request has type 'Request<RecordTypeConflation>.

However subsequent discussion implies that it is actually has type Request<PrimitiveTypeAvoidance>. What has happened is that the undefined type RecordTypeConflation has been inferred from subsequent usage to have primitive type PrimitiveTypeAvoidance.

@Happypig375
Copy link
Contributor

To be fair I also thought this was just to save a few keystrokes until I read the Optional Optimization part.

@cartermp
Copy link
Member

cartermp commented Jun 10, 2019

@ShalokShalom Please don't modify the affidavit section that previously said this:

I or my company would be willing to help implement and/or test this

To be clear, would you or your company be willing to implement this?


This suggestion is currently two suggestions:

  • Allow record type declarations to have inferred types for their labels
  • Infer record label types based on their names

The first suggestion, based on subsequent discussion, is more in line with existing F# semantics, like this:

let add x y = x + y // 'x' and 'y' are inferred to be 'int', as is the result

Such behavior has a precedent with existing F# code and its purpose is primarily to save keystrokes. However, it is the body of the function that helps the typechecker determine a type to infer. The only meaningful types that could be inferred for records without any members that perform some kind of algebraic operations on them is a generic type.

type R = { A; B }

// record R
//   val A: 'a
//   val B: 'b

This might be useful for defining generic structural groups of data, but unlike functions, do not allow for using type inference + inline to apply static constraints unless done so on a member. And most record types don't have members.

The second suggestion does not have any existing precedent. The closest I can think of is variable name suggestion via Humanizer in the C# tools for Visual Studio.

I don't think the section about optimization is relevant here. This wouldn't improve CPU time for compilation in any way; it would probably make it marginally worse.

I don't think I'm in favor of this, but I'm certainly open to examples where this would be useful. What uses would there be to declaring generic, nominal groups of named data?

@voronoipotato
Copy link

I'm not so sure about this myself. Seems like it adds another way of doing things that potentially leads to some very bad habits. A question I'd be asking is we use records far more than we define them, so what kind of use case is defining so many record types that they need a shorthand. If there is one perhaps we should be focusing on adding records to type providers or some other code gen story.

Here's an example of a dangerous habit this proposal affords.

type R = {Int; Float; Bool}

@ShalokShalom
Copy link
Author

Hi there all 🤗

Thanks for your participation. Reading through these comments, I think I made a not so good job describing what I mean:

  1. Correct me if I am wrong: Build in (primitive) types perform faster as custom types, such as shown in my example. This is at least how it's in a lot of other languages and how I assumed it is here as well.

  2. Custom types are more distinguishable as they describe precisely what they are, what they do and why they are there.

@voronoipotato Correct. I suggest this now as an addition to record types, since record types are one of the things a newbie sees first and we can well apply this to other constructs as well.

Why this all? The general idea is just, that we use this concept as the default way in order to increase the usage of custom types, compared to primitives.

In order to sustain the assumed performance benefit of build in types, I suggested the 2 layer principle.

In general is the fundamental idea the same as that for type inference, significant whitespace and so on.

Why typing something that is obvious?

Don't repeat yourself. 🤗

@voronoipotato
Copy link

I better understand what you are trying to accomplish. I think it makes sense why you would want it. I wonder if a tuple type alias or single case DU will accomplish your goal. We try to reduce the number of ways of doing the same thing if possible.

type Details = Name * Description

If you want to exclude Name * Description that were not tagged as a Details? A single case DU also accomplishes this.

type Details = Details of Name * Description

In my current understanding the record exists because you intend to give it a special name that represents the how the domain talks about the different components, in a way that the order does not matter. So if all your record titles are existing type names, a single case DU or a tuple is fine. There is already a issue in suggestions for reducing duplicate naming for single case DU. Which if memory serves is still being deliberated.

//single case DU redundancy example
type DU = DU of int

@ShalokShalom
Copy link
Author

Oh, that looks nice. Do you see any substantial differences here? I mean, why are record types used at all instead of saying maps, unions and tuple types?

@Happypig375
Copy link
Contributor

Because they have element names, yes, names that are known at compile time!
Maps: Unknown number of possible elements (Known at run time)
Unions: Known number of possible values (only one element)
Tuples: Known number of elements like records, but are unnamed
Records: Known number of elements like records, but are named

@7sharp9
Copy link
Member

7sharp9 commented Jun 11, 2019

I don't like the fact there are no top level annotations, its always recommended to add annotation to top level function to make the code clear, this should apply to top level types defined like records etc. Having everything inferred could be quite troublesome.

@ShalokShalom
Copy link
Author

ShalokShalom commented Jun 11, 2019

@Happypig375 I see.

@7sharp9 It is different here, in my opinion. Top level functions can have all kinds of types. This proposal here makes always clear what type it is.

I could add that the sense of this idea is to develop helpful habits and to introduce new users of the language to a certain mindset. That is the meaning and intention here, as opposed to saving a couple of keystrokes. It is about sane defaults.

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

7 participants