fslang-suggestions icon indicating copy to clipboard operation
fslang-suggestions copied to clipboard

Add support for separated type annotations

Open baronfel opened this issue 8 years ago • 32 comments

Submitted by amazingant on 10/8/2014 12:00:00 AM
58 votes on UserVoice prior to migration

Although it's typically preferred to let the compiler handle types, it is often necessary to provide type annotation. Sometimes this is to help the compiler understand the types a function can take, often it is done to help other developers understand your code, and in my personal use, I frequently add type annotations to ensure the compiler will catch typos I make which change a function's return type.

Although not as verbose as other .NET languages, the F# type annotations do add verbosity to a function definition, look cluttered compared to Haskell's syntax (personal opinion), and do not match the type format provided by the compiler or interactive environment.

I propose adding syntax to allow defining a function's type with a syntax closer to Haskell's. Since the :: operator is already in use, my proposal is to create a new operator such as => for these purposes; a new keyword could suffice as well, or simply new use of the val keyword currently used in signature files. My initial suggestion is as follows:

let f => int -> int -> int
let f x y = x * y

While the inclusion of variable names would reduce the terseness of the syntax and would provide little benefit to readability or comprehension, such a change would provide consistency with the format output by fsi.exe and seen in Visual Studio with IntelliSense. An more complex type signature with such a format:

let f => a:(int -> string -> int) -> x:string list -> y:int -> int list
let f a x y = List.map (a y) x

In these examples, neither of the code snippets would typically need type annotations, and the annotations here are much larger than the implementation itself. However, the former syntax (my preference) would greatly reduce the length of function declarations in more complex functions. The latter option, while I would prefer it over the current syntax, would introduce an additional location where variable names would have to be changed during refactoring.

It is my opinion that were such a syntax to be provided, use of both the new and current syntaxes together should provide a compiler warning to the effect of "Multiple type annotations provided for function {0}. Consider removing extra type annotations to simplify refactoring of your code."

Original UserVoice Submission Archived Uservoice Comments

baronfel avatar Oct 20 '16 02:10 baronfel

IMO we can achieve an equivalent level of de-cluttering, and keep the 'spirit' of ML, by making the type inference engine use types from signature files as input the same way it does annotations in the implementation files. If I'm not mistaken, OCaml does this as well, right? Since we can ascribe a more restricted type to a value in the interface file than in the implementation.

yawaramin avatar Oct 30 '16 00:10 yawaramin

After looking into Idris I've also become a proponent explicit types, and I've been adding explicit types to all my F# functions (and most of my Python functions as well) ever since. I feel explicit types add a lot of documentation value to the code. Its like adding comments to your code, -except these 'comments' are much better because they are type checked :).

I've considered making a plugin for emacs or Visual Studio to automatically add infered type annotations on top of functions in the same style as C# adornments in visual studio. However, I have no knowledge about either emacs or visual studio plugins, so its quite a big project for me, which I haven't had time for yet.

michelrandahl avatar Oct 30 '16 10:10 michelrandahl

@michelrandahl hi, I feel signature files (or interface files) are a good place to put all the explicit type annotations, because they already exist and are used, and wouldn't require any surface changes to the language. The only problem with them is as I mentioned above.

About the automatic annotation adornments, check out Visual Studio Code or Atom with the Ionide plugin. It does that. The feature is called CodeLens. Actually I found it rather annoying, so I turned it off. I can get type info any time by hovering over or clicking in any symbol.

yawaramin avatar Oct 30 '16 14:10 yawaramin

@yawaramin I guess it all comes down to personal preference, where my preference is that of the style used in Idris/Haskell eg. type signatures together with the code. If it was ever implemented in F# I would prefer it to be optional of course. Anyhow, I think you might be right that it would be a too radical change/addition to the language.

Thank you so much for pointing out that the adornments already exists with Ionide, I've been missing that feature so bad. It's looks exactly how I wanted. Now I just hope someone will implement it for emacs as well, or I'll just have to finally get around learning how to make emacs plugins ^^.

michelrandahl avatar Oct 30 '16 15:10 michelrandahl

let f : int -> int -> int =
    fun x y -> x * y

Is currently valid syntax and gets you most of the way there and iirc the IL generated after compilation is exactly the same as let f x y = x * y

cloudRoutine avatar Oct 30 '16 15:10 cloudRoutine

@cloudRoutine quite neat!

This method could be handy to write some script/prototype/exploratory code, but the downsides are that it is not very common to write functions this way and it would be advisable to adapt the function declaration if it is used in a production application. I would love to have optional type annotations like used in Idris/Haskell.

toburger avatar Nov 09 '16 07:11 toburger

@toburger top-level type annotations are mandatory in Idris.

yawaramin avatar Nov 10 '16 04:11 yawaramin

I've used both ways now in Elm (annotations above) and F# (annotations inline). At first, I loved Elm annotations and wished F# had them. But as time went on the Elm annotations became a bit of a chore. Even if you could define them as:

myFunc : a -> MyRecordType -> b
// or even
myFunc : _ -> MyRecordType -> _

It's a chore to declare every parameter's type when you only really need it for MyRecordType.

Now, F#'s inline annotations are certainly not perfect. In particular, having to wrap characters around a parameter with "( ... : MyRecordType )" is also a chore, and the parameter looks "ugly". But I prefer being able to annotate only the parameters that require it instead of having to declare (at least some placeholder) types for every parameter. I usually wait until the compiler complains before adding an annotation. Otherwise, I let the code be as generic as possible.

kspeakman avatar Mar 06 '17 15:03 kspeakman

I think there are three purposes for type annotations:

  • Making the compiler happy: inline extensions are the way to go. Oftentimes you only have to annotate a single argument so a separate type annotation is too verbose with all the wildcard types.
  • Writing (all) types for documentation purpose: separate type annotations are way more readable. For large scale projects a separate signature file can be handy (and writing the doc comments together with the type definitions). One limitations exists though: signature files are a good mechanism to describe the public API, I don't think it is a good fit for private functions, they are oftentimes the moving parts of the application and maintaining two files is overhead.
  • Writing types before writing code: in functional programming oftentimes you know the function signature before you know how to implement the function. Separate type annotations are way more elegant to work with.

toburger avatar Mar 07 '17 07:03 toburger

Function signatures as they are feel borderline vestigial. What's the work it would take to add this so that the signature actually counts as annotation, and I can do this inline? Having it as an entire separate file is a whole lot of work replicating my entire program structure, and I can't use it in FSI. I do just want this to be optional so that I can use it where it makes sense to use it. I'm somewhat new to compiler discussion and frankly F#, https://fsharpforfunandprofit.com/posts/function-signatures/ brought me here. If it is something that a small group of users are passionate about maybe we could be guided into doing the work, and improve the number of project contributors.

Example

val foo int -> int
let foo x = x

voronoipotato avatar May 02 '17 15:05 voronoipotato

What bothers me the most is the fact the style isn't consistent. Let's say I choose to let the compiler handle types but then sometimes I might have to help it out which makes me end up with code like this:

let f x = ...
let g x (y:MyType) = ... 

Or maybe I feel like annotating the types:

let f : 'a List -> 'a option = function
  | []     -> None
  | x :: _ -> Some x

let g (x:int) (y:int) : int = x + y`

In order to make the code look consistent I pretty much have to resort to writing the function g above like this:

let g : int -> int -> int =
  fun x y -> x + y

I personally find f: int -> int -> int more readable than g (x:int) (y:int) : int. Not only that but the former is also the way types are annotated in intellisense, fsi, signature files and so on.

@baronfel I propose adding syntax to allow defining a function's type with a syntax closer to Haskell's. Since the :: operator is already in use, my proposal is to create a new operator such as => for these purposes; a new keyword could suffice as well, or simply new use of the val keyword currently used in signature files.

I'm not sure about using the => operator as it could potentially be used in the future for (much needed) improvement to type constraint syntax? ;)

ghost avatar Nov 12 '18 15:11 ghost

I don't think using lambdas is a solution. Perhaps something like this would be nice. This would allow you to pass your function into a neatly named slot. Right now of course this gives an error.

//:( error
//This would be great if it worked and it totally makes sense why it should work
//I feel like this could just be sugar for the next example.
type CoolFunction = int -> int -> int
let (kickflip:CoolFunction) x y = x * y

and this currently works

// :)
type CoolFunction = int -> int -> int
let (kickflip: CoolFunction) = fun x y -> x * y

Using a type for this is nice because it gives you a practical name for the signature beyond what it consumes and beyond the specific implementation details aka the function name. In this sense the type represents a kind of "functional interface" which can be consumed by other functions.

This works as well

// :|
type CoolFunction = int -> int -> int 
let kickflip = (fun x y -> x * y) :> CoolFunction

This is how you would do it if you wanted to avoid fun, which yeah it doesn't look fun I guess.

// :/
type CoolFunction = int -> int -> int 
let kickflip = 
    let f x y = x * y 
    f :> CoolFunction

voronoipotato avatar Nov 15 '18 21:11 voronoipotato

Anyone have a feel for whether on not this would ever be approved? /cc @dsyme / @cartermp

7sharp9 avatar Jul 05 '19 11:07 7sharp9

I'm really undecided on this one. After programming a lot of haskell recently, I get the appeal. But something about it just doesn't feel right for an ML.

cartermp avatar Jul 09 '19 16:07 cartermp

VS2019 comes with (experimental) CodeLens feature, which will display an annotation above (or beside) the function. With this, most of the reason I liked type signatures from Elm are satisfied: I get to see the separate type signature. But now I get that without having to type it out manually, nor having to keep it updated.

It wouldn't hurt my feelings if separate type signatures were implemented, but our team would almost never use it. We really like the almost-dynamic feel of not having to specify types most of the time. Plus (from Elm experience) maintaining a separate type signature eventually feels like a chore -- you have to update 2 places if you add/remove (or sometimes change) a parameter. There have been many times where we updated one and not the other and were briefly confused when it wouldn't compile. We probably wouldn't do type signatures in Elm anymore, except the compiler issues a warning if you don't. I'd much rather see effort toward type signatures first go into getting CodeLens out of experimental status. Then see what's what after that.

kspeakman avatar Jul 09 '19 21:07 kspeakman

I wish F# emitted a warning for a top level function without an annotation.

7sharp9 avatar Jul 09 '19 22:07 7sharp9

I wish F# emitted a warning for a top level function without an annotation.

If you really wish it, make an issue suggesting the feature (maybe in the compiler repo?). It could be behind a compiler flag, for example. That would make it controllable from build scripts or IDE configurations.

kspeakman avatar Jul 09 '19 23:07 kspeakman

I'm really undecided on this one. After programming a lot of haskell recently, I get the appeal. But something about it just doesn't feel right for an ML.

Can you explain what about separated type annotations doesn't feel right for an ML?

toburger avatar Jul 10 '19 11:07 toburger

I like the concept of signature files for enforcing types on top-level functions in a library, but their use is a bit clunky and requires doubling up the number of files in a project. Also, being able to do these inline would provide a convenient way around the old "value restriction" issue.

I'm really undecided on this one. After programming a lot of haskell recently, I get the appeal. But something about it just doesn't feel right for an ML.

To be clear, the feature requested in this issue is nothing new for F#. The ability to add separated function signatures already exists with signature files. The desire to be able to write the signature closer to the function is more desirable than having a separate file for a few reasons.

Having a separate file to manage on top of the implementation feels very C/C++ header-y, and just creates additional burden. It would be great If we were able to define signatures as we do now in a signature file, but inline with the source:

 // no non-intuitive value restriction error
val someValue : int32
let someValue = someGenericFunction ()

// the names of type parameters should come from the annotation as well; no more 'a0, 'a1, etc
val bind : Option<'a> -> ('a -> Option<'b>) -> Option<'b>
let bind option binder = ...

A few common complaints I've heard when showing people F#, and trying to spread it's use is that

  • The type annotations for functions look clunky.
  • The type inference system allows different signatures to be inferred for a public function, breaking the API.
  • Signature files feel burdensome, especially in light of F# requiring users to manage the order of files in the *.fsproj file.
  • Signature files move the signature further away from the functions, making them harder to review or see.

Having inline (but separate) type annotations would address all of these concerns. I would add to this that the compiler should allow generating a warning for public functions without an associated signature as well.

To address arguments against this feature in the vein of "x tool provides a tooltip showing a signature"; this does no good when reading the code in an editor that doesn't provide such a feature.

adam-becker avatar Jan 26 '20 20:01 adam-becker

@voronoipotato the issue with the examples provided in your post is the loss of parameter names (and their documentation?)

I actually like the approach of unifying the signature file syntax that @adam-becker proposes, although I don't find the current type annotation syntax to be clunky, it is the same as in most languages, baring C/C++/java and C# among the popular ones; the syntax is in fact more consistent than C type languages having their casting syntax and type comes before symbol name dichotomy.

smoothdeveloper avatar Jan 26 '20 21:01 smoothdeveloper

@smoothdeveloper I personally dislike having to wrap parameters in () and put a colon after the name just to introduce a type annotation. In this way, they are heavier than most languages.

But the only reason I have to do that is because the alternative (making a new file and having the annotation far away) is lacking.

I think if this were to be adopted, there should be a rule to enforce that a type annotation is always directly above the thing being annotated.

// this should not be allowed because it defeats the purpose of merging the *.fsi file
val add : int32 -> int32 -> int32
let multiply a b = a * b
let add a b = a + b

adam-becker avatar Jan 29 '20 20:01 adam-becker

there should be a rule to enforce that a type annotation is always directly above the thing being annotated.

@adam-becker Though a strong argument for this feature is the opposite: there are plenty scenarios where you know the type of a function, but want to defer the implementation. The current way to solve that is with interfaces, but they cannot be applied to functions in modules and require type instantiations.

An alternative could be (thinking out of the box) to allow interface-style declarations for modules and/or static classes. Some languages, like Java since v8, support this, and iirc, it was part of Eiffel too. Though static interfaces are a notion in OO, the concept could easily be adopted to the functional paradigm.

A module that inherits a static interface (or say, a function group definition), must implement the functions, just as if it's a normal interface, but without the class instantiation.

And just like normal interfaces, you can consume methods from the static interface as parameters or define variables of such deferred functions, but to call them, they must be given a concrete implementation.

This would by no means be a small language change, with undoubtedly a lot of questions to be answered, but it gives a lot of freedom on top of what's suggested in the original lang suggestion here.

abelbraaksma avatar Jan 31 '20 14:01 abelbraaksma

@abelbraaksma There's already a way to keep signatures and implementations separate, .fsi files! :smile:

Joking aside, having chosen F# for its functional aspects mostly puts me off of the idea of solving problems in the language by introducing OO concepts, like interfaces and inheritance. But if you really believe in this idea, it's distinct enough to be its own suggestion and you should definitely open an issue to argue in favor of it.

Back to the proposal at hand, I don't believe it's a very big lift, as all of the pieces needed to implement already exist. The pieces just need to be brought together and it may take some work, but I would love to have this as an option for defining type signatures.

adam-becker avatar Feb 18 '20 23:02 adam-becker

Oh no, please surgically extract .fsi files. Its like going back to header files. Moving comments and signature to a sig files is just awful.

7sharp9 avatar Feb 19 '20 11:02 7sharp9

I don't think this suggestion adds anything to F#, I do like the idea of making there compiler output a warning if you don't have a full type signature on a top level public type though.

7sharp9 avatar Feb 19 '20 11:02 7sharp9

There's already a way to keep signatures and implementations separate, .fsi files! smile

@adam-becker, that's not what I meant. FSI doesn't allow writing the signatures before usage, let alone before definition.

Another way to look at this is (in compile order):

  • define full signature (and documentation)
  • use it as if it exists
  • define implementation

abelbraaksma avatar Feb 19 '20 12:02 abelbraaksma

image

Linter is mad at me :(

Edit: I disabled FSharpLinter integration so now it can't remind me of my crimes.

2jacobtan avatar May 24 '20 11:05 2jacobtan

@2jacobtan, I fail to see the relevance to this thread? Maybe you intended to post it in the FSharpLint github repro instead? (btw, Lint is correct with its suggestion, you can replace the whole lambda with just ( * )).

abelbraaksma avatar May 24 '20 18:05 abelbraaksma

@2jacobtan, I fail to see the relevance to this thread? Maybe you intended to post it in the FSharpLint github repro instead? (btw, Lint is correct with its suggestion, you can replace the whole lambda with just ( * )).

Apologies for the irrelevance.

But thank you for the tip!! I finally understand the lint message. (I now realise it's completely due to my ignorance.)

2jacobtan avatar May 24 '20 20:05 2jacobtan

@abelbraaksma The relevance is that the linter was telling @2jacobtan that the suggested workaround posed in this thread isn't right. Sure, it could be refactored but these things can also be at odds with each other, so the community needs to a path forward that is consistent.

adam-becker avatar May 26 '20 16:05 adam-becker