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

signature improvement for generics #674

Open
sgtz opened this Issue Jun 8, 2018 · 9 comments

Comments

Projects
None yet
4 participants
@sgtz

sgtz commented Jun 8, 2018

Signature Improvement for generics

the generic portion of a method or function gets ignored when fsi produces the signature description. In the case below, we've got nothing to show for x prime "<'x>". Can we improve the situation?

open System
type Abc() =
  static member A() = "abc",[1]
  static member B(n) =
    "blah",[for i = 0 to n-1 do yield i]

  static member B<'x>(n) =
    typeof<'x>.Name,[for i = 0 to n-1 do yield i]

  static member B(ty:Type,n) =
    ty.Name,[for i = 0 to n-1 do yield i]

SIGNATURE

type Abc =
  class
    new : unit -> Abc
    static member A : unit -> string * int list
    static member B : n:int -> string * int list
    static member B : n:int -> string * int list
    static member B : ty:Type * n:int -> string * int list
  end

Issue -- there are two entries for B in the below. Only when a type is a standard argument, does the signature come close to what I was expecting. Shouldn't the generic version produce a different signature?

static member B : unit -> string * int list  

Proposed syntax

static member C : 'type -> string

The existing way of approaching this problem in F# is to make a mental note of what is actually going on underneath.

Pros and Cons

The advantages of making this adjustment to F# are -> Some("type signatures are also accurate + explicit for generic cases")

The disadvantages of making this adjustment to F# are -> None.

Extra information

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

Related suggestions: n/a

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • [ X ] This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • [ X ] I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • [ X ] 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:

  • [ X ] This is not a breaking change to the F# language design
  • [ X ] I or my company would be willing to help implement and/or test this
@abelbraaksma

This comment has been minimized.

abelbraaksma commented Jun 9, 2018

I'm not sure I understand the issue. The second method C above takes a unit and returns a type. That seems accurate to me. I think you're suggesting it should instead say that it takes a value of type 'type and returns a string, but that appears to be wrong.

Just trying to understand what you're suggesting ;)

@sgtz

This comment has been minimized.

sgtz commented Jun 9, 2018

is this clearer? Please see the update. Essentially, 'x goes off into the ether. The AST is correct based on expected behaviour working correctly, so it probably just an issue with pretty printing.

@abelbraaksma

This comment has been minimized.

abelbraaksma commented Jun 10, 2018

Yes, it's clearer now. I think the main issue with your code is that you are using generics that doesn't appear anywhere in the signature, hence you don't see it back in the signature F# gives you. I.e. suppose you have:

  static member B<'x>(n, a:'x) =
    typeof<'x>.Name,[for i = 0 to n-1 do yield i]

This would give you the signature like it's expected (note the appearance of 'x)

static member B : n:int * a:'x -> string * int list

I don't think changing the inferred signature to something else than it actually is is the right way forward, but perhaps inference could detect that it is a generic signature where the generic type parameter is a requirement (as opposed to being implicit, which is usual in F#, as in my example above).

For instance, instead of static member B : n:int -> string * int list, type inference could show it as static member B<'a> : n:int -> string * int list.

That way it wouldn't change the actual signature and the shown signature is still legal syntax and actually maps the original definition. An added benefit would be that in the output of your type it becomes immediately obvious what the difference are:

type Abc =
  class
    new : unit -> Abc
    static member A : unit -> string * int list
    static member B : n:int -> string * int list
    static member B<'a> : n:int -> string * int list           // now this is distinguishable
    static member B : ty:Type * n:int -> string * int list
  end

Whether this is easy or hard to achieve, I don't know. Type inference is a tricky thing in the compiler.

@sgtz

This comment has been minimized.

sgtz commented Jun 10, 2018

interesting... it's as you said... thanks for mentioning the not used + inference idea...

static member B<'x>(n) =
  typeof<'x>.Name,[for i = 0 to n-1 do yield i]
  Unchecked.defaultof<'x>

... signature is...

static member B : n:int -> 'x

I'm not sure if this is predictable enough though... going back a step to the weird behaviour:

static member BB<'x>(n:int) =
  match typeof<'x> with 
  | ty when ty = typeof<int> -> "int!!"
  | _ -> "something else"

signature

    static member BB : n:int -> string

and yet, inference or not, 'x has influenced behaviour.

It might not affect most people in practice, yet, maybe it's a rough enough edge to be worthy of sanding one day. The context is "reasoning with types + signatures" + type providers + it came up in actual work that I'll be using for teaching purposes. I'll skip over this so that I don't confuse anyone. I'll definitely avoid the generic notation, which would have been more compact, yet probably not have added much in terms of readability. I do like the way <'x> works generally -- just need to avoid it in this context. Perhaps, hand cranking the type signature is acceptable. Thanks.

@abelbraaksma

This comment has been minimized.

abelbraaksma commented Jun 10, 2018

I do like the way <'x> works generally

Most of the time you won't need it, F#'s auto-generics take care of that.

and yet, inference or not, 'x has influenced behaviour.

It influenced behavior, yes, but 'x did not appear in your signature, so it won't appear in the inferenced types of your signature, simply because it isn't there.

Though I do see your point: if the generic parameter is specified, it is probably for a good reason (i.e., in your case you use it with typeof<>), and it will therefor influence the outcome. It is probably best to avoid having two members that share the same name but are only different in one "with type param" and another one "without type param", unless it can be inferred from the signature (which isn't true for your cases).

@sgtz

This comment has been minimized.

sgtz commented Jun 10, 2018

there are implications beyond F# only scenarios, like perhaps at that point, you are injecting IL into a byte array, or translating to something further afield. The finer point is, the programmer was explicit, so we should not override this.

Also, it's inconsistent with a simpler case:

let f<'z> (x:int) (y:int) =
  x + y + 1

signature:

val f<'z> : x:int -> y:int -> int

The language needs to be predictable so that you can read the code, and not be forced to run it to understand what is going on.

NB. we still don't know if this is just pretty printing.

@cartermp

This comment has been minimized.

Member

cartermp commented Jun 11, 2018

What I'm reading from this suggestion is to pretty-print generic type arguments for functions and members. For example, in FSI:

Class and constructor show generic type argument:

> type C<'T>() = class end;;
type C<'T> =
  class
    new : unit -> C<'T>
  end

Member does not:

> type C2<'T>() =
-     member __.M<'T>() = ();;
type C2<'T> =
  class
    new : unit -> C2<'T>
    member M : unit -> unit
  end

Neither does a function:

> let f<'T>() = ();;
val f<'T> : unit -> unit

I think that's reasonable and probably doesn't require a language change. Assuming it does not, then this seems like a fine feature request in the Visual F# repository.

@realvictorprm

This comment has been minimized.

Member

realvictorprm commented Jun 11, 2018

@cartermp this shouldn't be too difficult. I can take a look as soon as we decide on a style.

@sgtz

This comment has been minimized.

sgtz commented Jun 11, 2018

@cartermp: I've been trying to catch glitches and say something about them without overthinking it. I was unsure of scope at the beginning of this.

@realvictorprm: re: style -- probably the way a let does it currently is the right way to go. Although I'd prefer to have fewer angle brackets, this is the idiomatic way in F# / .Net.

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