fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
342 stars 20 forks source link

Prohibit duplicate identifiers at the same level #1299

Open pbachmann opened 11 months ago

pbachmann commented 11 months ago

I propose we... Issue a warning when an identifier is declared twice in this same indent level with a view to making it an error in later versions of the language.

The existing way of approaching this problem in F# is... To either issue errors or do nothing, depending on whether the duplicates are at the module level or not. Eg. at the module level:

let invoice = 5
let invoice = GetInvoiceFromDatabase invoice

.. emits error FS0037: Duplicate definition of value 'invoice'

On the other hand, in every other circumstance, F# allows the creation of duplicate identifiers:

let LoadInvoiceAndPrintCustomer () =
    let invoice = 3
    let invoice = GetInvoiceFromDatabase invoice
    printfn "cust %s" invoice.CustomerName

Pros and Cons

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

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

(in one sense, this is not a disadvantage because in fact what is being delivered to developers here is little more than good advice).

Extra information

Estimated cost (XS, S, M, L, XL, XXL):
I am in no position to estimate the cost.

Related suggestions: I have scanned/searched the 1200 or so issues and can’t find anything related.

There is a stackoverflow complaint - Error FS0037 sometimes, very confusing to which @tpetricek responds:

This is a bit confusing... It makes sense when you know how things are compiled though.

I think @tpetricek mistyped when he said 'a bit' - really he meant 'utterly'. As indicated elsewhere, I suggest F# developers should not be thinking about "how things are compiled" but instead be thinking about great names for their identifiers.

Another person wrote RTFM:

At any level of scope other than module scope, it is not an error to reuse a value or function name. If you reuse a name, the name declared later shadows the name declared earlier. However, at the top level scope in a module, names must be unique.

Which I think just begs the question.

Affidavit (please submit!)

For Readers

Please click the :+1: emoji on this issue, even if you don’t like the idea or see the need for it. These counts are used to generally order the suggestions by engagement.

An artificially inflated count might be necessary to counter the average developer’s preference for new toys over solid code. See Don Syme 12:43 – 13:29.

Origins of this idea

This idea arose from an attempt I made to explain why F# is a better programming language than others. Unfortunately, this article was based on the misplaced assumption (based on testing at the module level), that F# did not permit duplicate identifiers at any indent level.

vzarytovskii commented 11 months ago

Shadowing is a valid F# feature and it will be a breaking change. However, it's a valid use for the F# analyzer, and should be tracked in F# repo.

pbachmann commented 11 months ago

Can you show an example of where this "valid F# feature" is useful?

kerams commented 11 months ago

A classic example that comes to mind.

let x item =
    match item with
    | Some item -> ()
    | _ -> ()

But I get how shadowing values of the same type can be dangerous.

pbachmann commented 11 months ago
let x item =
    match item with
    | Some item -> ()
    | _ -> ()

... seem to be two different things,

They can have the same name. Everyone knows that in different contexts, something with the same name can have different meanings. Fred from Accounts is not the same as Fred from Marketing.

I thought I made it clear in my proposal that I was commenting on two identifiers at the same indent level eg.

let x item =
    let myName = "Sam"
    let myName = "No longer Sam"
kerams commented 11 months ago

I missed 'same indent level' in your original comment, but truth be told, I find it less of an issue than shadowing across scopes. You can filter some data in an inner scope and shadow a value, and then try to refer to it later in an outer scope, thinking you're accessing filtered data. Or you might be using the filtered data in the inner scope, but remove the filtering at some point in the future.

pbachmann commented 11 months ago

I find it less of an issue than shadowing across scopes. You can filter some data in an inner scope and shadow a value, and then try to refer to it later in an outer scope, thinking you're accessing filtered data. Or you might be using the filtered data in the inner scope, but remove the filtering at some point in the future.

I think I agree with you (in one sense), but would like to be clear about what you mean. There was a proposal for C# called static local functions, which they had a meeting about, though I don't know whether it ever made it into the language. Is that an attempt to solve the problem you're talking about? By making the inner stuff not capture the outer stuff? If so, I sympathise but tend to solve that problem by saying to myself, "Well if the inner code is genuinely independent of the outer context, stick the code into its own context (a separate function) so there's no need to ask for a static keyword."

Returning to my proposal, it might seem like a "lesser issue" to you, but I wonder what programmers from other languages think? Is there a Java programmer who doesn't think it is perfectly reasonable for the following code...

class HelloWorld {
    public static void main(String[] args) {
        final var args  = "one";
        System.out.println("Hello, World!");
    }
}

to issue an error:

HelloWorld.java:6: error: variable args is already defined in method main(String[])
final var args  = "one";
          ^

?

vzarytovskii commented 11 months ago

Can you show an example of where this "valid F# feature" is useful?

let results = getResults ()

Logger.log results

let results = results |> List.map ((*) 2)

...

We use it all the time in all the places.

Happypig375 commented 11 months ago

@pbachmann Any good IDE would highlight where the same variable is used. All variables are still typed strongly, the point of "Consolidate F#'s credentials as a strongly-typed language." is invalid. image

Also, indent levels become confusing when you consider that

let x item =
    let myName = "Sam"
    let myName = "No longer Sam"
    myName

can be written as

let x item =
    let myName = "Sam" in
        let myName = "No longer Sam"
        myName

if these two are considered differently then you have a new inconsistency.

pbachmann commented 11 months ago

@vzarytovskii If I understand the purpose of the logging example, you are saying "I've got some results, I want to log the results, then I want to manipulate the results (and maybe log them again) and I can't be bothered thinking of new names all the time - 'results' is good enough for me."

I apologise if I've misunderstood.

If "not having to think of new names all the time" is the use of shadowing, I understand but I just happen to have a view, expressed in my proposal, that developers should think of new names all the time or use a series of pipes. (The goal being to to create more readable code). This was explained in my essay to non-programmers.

Of course, maybe an experienced programmer like you may not have much interest in what non-programmers think.

pbachmann commented 11 months ago

@vzarytovskii

Maybe there is a new philosophy in the F# camp? I happen to be in full agreement with Don Syme when he railed against the use of point free code in this video. On the other hand, you think

let results = results |> List.map ((*) 2)

is a perfectly reasonable "simple example" to provide to a possible newcomer to the language?

vzarytovskii commented 11 months ago

@vzarytovskii

Maybe there is a new philosophy in the F# camp? I happen to be in full agreement with Don Syme when he railed against the use of point free code in this video. On the other hand, you think

let results = results |> List.map ((*) 2)

is a perfectly reasonable "simple example" to provide to a possible newcomer to the language?

It's was just the easiest example, which captures the intent, to type from my phone. It can be any lambda there. We do data transformation like this all the time, and shadowing is usually a way to go, to not pollute local scope with bindings which are essentially discarded.

pbachmann commented 11 months ago

@Happypig375 Yes I agree that an IDE that highlights where an identifier is used and where it originates from mitigates the problem. I might sadden the original designers of the language to think that a good IDE is required to alleviate some of the problems in the language's design.

All variables are still typed strongly, the point of "Consolidate F#'s credentials as a strongly-typed language." is invalid.

Yes F# variables are strongly typed. I was making fun of the fact that in F# you can write code like:

let myName = "Sam"
let myName = 2 + 2

which in most languages would seem ridiculous.

Regarding:

let x item =
    let myName = "Sam" in
        let myName = "No longer Sam"
        myName

I am not familiar with "let ... in". Is this verbose syntax? Do I need to learn verbose syntax to understand your point?

pbachmann commented 11 months ago

@vzarytovskii

OK, so it seems I have misunderstood you. When you say:

We do data transformation like this all the time, and shadowing is usually a way to go, to not pollute local scope with bindings which are essentially discarded.

.. you are not avoiding the generating a lot of names because of the difficulty of thinking of all those names, but because you want to assist the person trying to read your code by not giving him a lot of meaningless names to wade through?

Happypig375 commented 11 months ago

@pbachmann

I might sadden the original designers of the language to think that a good IDE is required to alleviate some of the problems in the language's design.

F# with all its type inference is already an IDE-dependent language. This is simply how the language is used.

Name shadowing is less ridiculous if you consider

let myName = "Sam"
match String50.validate myName with
| Some (myName: String50) -> // Wow! Different type!
    addMyNameToDatabase myName
| None -> Error $"My Name {myName} is longer than 50 characters."

Any other name would fall into the trap of adding unnecessary type information to the variable name.

I am not familiar with "let ... in". Is this verbose syntax? Do I need to learn verbose syntax to understand your point?

Yes, but it is not hard to understand. Any

let x = y
f1 x
f2 x

in local scope (NOT type/module scope) is equivalent to

let x = y in f1 x; f2 x

and it's similar to how MIT App Inventor (easy language for beginners) thinks about local variables. image

vzarytovskii commented 11 months ago

@vzarytovskii

OK, so it seems I have misunderstood you. When you say:

We do data transformation like this all the time, and shadowing is usually a way to go, to not pollute local scope with bindings which are essentially discarded.

.. you are not avoiding the generating a lot of names because of the difficulty of thinking of all those names, but because you want to assist the person trying to read your code by not giving him a lot of meaningless names to wade through?

Yeah, pretty much. But I'm biased, since I'm used to this style of code, and I account for possible shadowing, and use it a lot myself.

In my opinon, lots of different bindings can be harmful in different ways, can be unwilingly captured into closures, confusing for devs, regarding which one to use in the end (e.g. which one was last filtered, or mapped, or processed in a different way).

We cannot make this a warning which is emitted by default, since it will break a lot of existing code (people tend to make warnings threated as errors by defualt).

Two this can be considered are: a. Make it an informational warning, or off-by-default opt-in warning. b. Make it an another use-case for Analyzsers SDK which we're planning to work on around F# 9 / .NET 9.

P.S. one example from our VS tooling:

 let! declarationSpans =
    async {
        match declarationRange with
        | Some range -> return! rangeToDocumentSpans (document.Project.Solution, range)
        | None -> return! async.Return []
    }
    |> liftAsync

let declarationSpans =
    declarationSpans
    |> List.distinctBy (fun x -> x.Document.FilePath, x.Document.Project.FilePath)

It kinda makes it easier to read distinc logical blocks: first we fetch the data from the solution, an dthen we distinct it by the paths, without the need for separate binding.

pbachmann commented 11 months ago

@Happypig375 Re: String50 validation, I have already made it clear that my proposal applies to variables at the same indent level (see comments above)

Some (myName: String50) -> // Wow! Different type!

is a different type and should be a different type, we are all in agreement on that. btw I would have thought that String50.validate would supply the type and there's no need for :String50 annotation (though you might have provided that for my benefit).

Re: understanding verbose syntax, not for me tonight. I'm too tired :)

Happypig375 commented 11 months ago

@pbachmann Consider computation expressions

let result = option {
    let myName = "Sam"
    let! myName = String50.validate myName
    addMyNameToDatabase myName
}
realparadyne commented 11 months ago
let result = option {
    let myName = "Sam"
    let! myName = String50.validate myName
    addMyNameToDatabase myName
}

I've done a lot of this in actor based code, where there might be multiple asynchronous steps in handling a message to the actor and each one transforms a state record. I've tried switching it to unique names for example:

let! state' = performStep1 state
...
let! state'2 = performStep2 state'
...
return Some (NextActorState, state'2)

But this just creates new problems, I could easily have used the wrong state[', '2] in the return. Or I could easily forget to update one of the steps when inserting new ones (this of course did happen).

By reusing the same name 'state' at each step I'm actually making it more like other languages where 'state' would be mutable and you'd just keep updating it. Except with F# I get the benefit that it's not a single mutating value and the tooling will nicely show me where a name is bound and used.

Another place I'd frequently use it is transforming function arguments on function entry e.g

let DoSomething (arg1: string) (arg2: string option) =
    let arg2 = defaultArg arg2 (arg1 + "magic")
    $"first was {arg1} and second was {arg2}"
realparadyne commented 11 months ago

@pbachmann

Re: understanding verbose syntax, not for me tonight. I'm too tired :)

It's worth looking into this tomorrow. I feel it's something that should be highlighted more because it's key to really understanding that these types of languages truly are expression based. They are not a sequence of statements but a function is just one big expression. the let .. in .. form of binding reveals how that really works. The non-verbose form layered on top is generally simpler to read but it's also giving you an illusion of a sequence of statements/expressions by hiding the truth. That's why you can't consider language changes without considering both forms. Also the verbose form comes in very handy sometimes.

I really struggled to get to grips with functional and expression based programming for a long time (and still do). Especially with only written descriptions of it that didn't mesh together and didn't give me the visual mental model I need to understand things. I keep thinking if only I could make drawing and animations that explain it that it could be so much clearer!

robitar commented 11 months ago

I think a factor here, touched on by @realparadyne is that F# (and immutable/transformation style languages) places less responsibility on bindings than you would expect in something like Java.

There, you have lots of mutable state managed by nominal classes, and identifiers have a sort of ‘authority’ over them, just mutating them into something totally different would indeed be peculiar and confusing.

But here, bindings don’t really own the data they refer to, they are just transitory tags into the transformational state as it moves along.

Also, I actually disagree that a developer should be spending time coming up with highly specific names for everything. Apart from it being time consuming, it actually decreases readability because you just have 10 things to track instead of 5 etc, and when you get used to reading this style, its not a problem at all to update the bindings in your head as you go.

cartermp commented 11 months ago

FWIW I wouldn't really support this feature. As an off by default warning maybe, but I don't know if it's worth the maintenance cost to keep it going long term.

davidglassborow commented 11 months ago

We use shadowing all over the place to redefine a value so the previous one can’t be used by mistake, a feature I love in F #

pbachmann commented 11 months ago

Thanks @davidglassborow, others have made similar statements and I am still formulating a proper reply. The delay is because I feel I need not only to reconcile my original view with the views of those who have commented, but to equally factor in the views of those who are not in a position to comment (eg. high school students who are keen to learn coding).

davidglassborow commented 11 months ago

Yeah understand, I know we want to support new users, but it would be a shame to dumb the language down too much, if we did that I would definitely leave the community

BentTranberg commented 11 months ago

Me too - I'd be out of here in a flash. Seriously, this is something which has obviously "already been decided" in previous versions of F#. We don't warn about using a highly useful feature that is described in the language spec, and lots of us use all the time in order to gain readability and safety. There is nothing to warn about! Put it in an analyzer if you want, but not in the language, not even as a warning. It would greatly add to a situation where we'd have even more variations of the F# language. It's bad enough with the possibilities we already have to sort of redefine the language by specifying which warning to turn on or off or into an error.

pbachmann commented 11 months ago

@BentTranberg I am aware that it is not permitted to suggest something which has obviously "already been decided", but can't find any declined suggestions searching for "shadow", or by scanning the first 200 or so declined suggestions. I was labouring under the apprehension that shadow labelling just kind of drifted into the language from OCaml and perhaps (just saying perhaps), now might be the time to revisit it. When fully articulated, I imagine this suggestion would fall under option 4 of the readme being, "things we never thought of before".

I acknowledge there was a suggestion that was declined which might be related inasmuch as it might have provided a fix for the problem noted by @realparadyne in his comment above (regarding actor-based code): https://github.com/fsharp/fslang-suggestions/issues/446 .. but it is not clear to me why it was declined.

Shadowing is in the spec? Well, presumably most features being reviewed would be in the spec.

Happypig375 commented 11 months ago

@pbachmann The relevant discussion of #446 is here https://github.com/fsharp/fslang-suggestions/blob/d48c35ce216e2bff148937ec028ad61e5c273fdf/archive/suggestion-8151819-add-support-for-forward-piping-the-result-of-an-a.md

I received this suggested operator... let (|>>) xA x2y = async.Bind (xA, x2y >> async.Return) ...which works very nicely.... async { return! downloadData() |>> Seq.where isUseful |>> Seq.sortBy sortSelector |>> Seq.toList } Based on this, I'm considering deleting this suggestion. Thoughts?

Happypig375 commented 11 months ago

But still, piping necessarily assumes the argument to be piped is at the end, but we might use the intermediate result elsewhere or even multiple times. The operator is at most a workaround for a specific case.

pbachmann commented 11 months ago

@Happypig375

Your explanation of why it was "declined" was very helpful, thank you.

Re:

But still, piping necessarily assumes the argument to be piped is at the end, but we might use the intermediate result elsewhere or even multiple times. The operator is at most a workaround for a specific case.

I thought this was already covered in the apple pie story which I know you've read and commented on. (In the F# section, Grandma and the kid negotiate matters to come up with a combined pipe/intermediate result solution that they are both happy with).

BentTranberg commented 11 months ago

Ops, sorry, shadowing is perhaps not in the spec. I was so sure I saw it appear in a Google hit for a spec paper, but it looks like what I saw was this, from the language reference:

https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/functions/#scope

According to ChatGPT this feature also exists in OCaml and ML, and other functional languages. It pairs perfectly with the immutable-first nature of these languages.

In imperative languages, the situation is the opposite, and the reuse of variable names easily causes havoc. I know that very well because I used to fix other people's mistakes. In my experience, problems with shadowing in F# usually arise when it's mixed with mutability, which is precisely the situation you have in imperative languages.

pbachmann commented 11 months ago

@BentTranberg, thanks for the clarification re spec.

Re:

In imperative languages, the situation is the opposite, and the reuse of variable names easily causes havoc. I know that very well because I used to fix other people's mistakes. In my experience, problems with shadowing in F# usually arise when it's mixed with mutability, which is precisely the situation you have in imperative languages. In my experience, problems with shadowing in F# usually arise when it's mixed with mutability, which is precisely the situation you have in imperative languages.

It would be helpful for me to see simple examples of problems created by "shadowing mixed with mutability" and how they are fixed by changing to shadowed immutable data.

BentTranberg commented 11 months ago

I didn't say it was fixed by changing to shadowed immutable data. Why change logic because of that? It's the clashing of names that causes problems in these situations, so I make sure names don't clash when I have a local mutable. It's usually one mutable that clashes with something else, and more rarely it's two mutables that clash.

let blah () =
    let mutable i = 1
    while i < 10 do
        let mutable i = 2
        printfn $"i is {i}"
        i <- i + 1

I just don't write functions so large that I can't easily see this kind of situation, but old C# developers love to make this kind of mistake. Along with gotos and breaks and early returns and continue and whatnots in methods with 10.000 lines.

Functional code is simply a different beast than imperative code, and shadowing becomes a feature as a result of how expressions work. Once you mix in imperative stuff, the rules are changed for the worse, and it affects shadowing negatively too.

pbachmann commented 11 months ago

@BentTranberg,

I hope you're not yet "out of here".

Sorry for not responding sooner, your example of let blah () = etc. is so far removed from what I thought we were talking about, that it left me in despair, thinking you merry band of men and I will never be able to understand one another.

But I'll persist: Yes, I agree those "old C# developers" need to be coaxed away from the code you described. How to do that is, I think, a important question. Perhaps removing from them the temptations of shadow bindings might be part of the answer.

realparadyne commented 11 months ago

Perhaps removing from them the temptations of shadow bindings might be part of the answer.

Hello @pbachmann , So; so far not a single person (AFAIK) has thought this change will be a good one, in fact just the opposite. But it's good to keep an open mind and you want to persist so perhaps you could provide a couple of examples of real life code like we all write that makes use of shadowing to reduce the proliferation of identifiers and the significant problems that leads to in practise; and then examples of how it can be well done if shadowing were to be removed?

Happypig375 commented 11 months ago

@pbachmann I can see your perspective from newcomers to programming. But since shadowing is useful as demonstrated in other comments, shouldn't this be part of their learning instead of trying to avoid it?

pbachmann commented 11 months ago

@realparadyne

So; so far not a single person (AFAIK) has thought this change will be a good one, in fact just the opposite.

You're quite right. So far 18 downvotes on the proposal itself, with no upvotes.

One or two responses are off topic (though not unduly so) and a couple of responses misunderstand the proposal, but overwhelmingly comments are on topic, criticising the proposal with no attempt made to see why it might be a good idea.

Additionally, every comment speaking against the proposal is liked, often several times, and my comments never are.

It surprises me people haven't paused to consider the implications of that.

But it's good to keep an open mind

I agree.

Perhaps you could provide a couple of examples of real life code like we all write that makes use of shadowing to reduce the proliferation of identifiers and the significant problems that leads to in practise; and then examples of how it can be well done if shadowing were to be removed?

Off the top, here are 6 reasons why I have chosen not to. If you don't like them, I have others:

  1. Because if I provide examples of "real life code", I fear they will be interpreted as "unrepresentative samples". Then I will have to say, "Well, if you don't like my examples, please provide some better ones" which, presumably, will be forthcoming. Then we may have a continuation of this rather unproductive tug-of-war extending beyond "Whose point of view is correct?" into the realms of "Whose examples are the right ones?"

  2. Because if, instead of me providing example code, we agree on what good examples look like, it might start to foster a collaborative spirit that seems missing from this discussion.

  3. Because, as indicated in my most recent response to @Happypig375, there is already an excellent suite of source code examples available at Microsoft Learn, which any one could have scanned for suitable examples.

  4. Because, if I offered Microsoft Learn as a source of real life code examples, I fear it would be dismissed as "code for newbies" and that we need to see examples from professional code bases.

  5. Because I really admire the code and explanations from Microsoft Learn, do not think they are unreprepresentative of real world code, and would feel annoyed at hearing them belittled.

  6. Because I have downloaded the PDF of Microsoft Learn F# and searched the code looking for examples of shadowing and could only find one - so this excellent source or source code does not provide the examples of shadowing we require. (Admittedly, I only searched the first 270 pages before I got bored.) The one example I found has already been pointed to by @BentTranberg above, but its sole purpose is to illustrate that shadowing can be done - not to illustrate any use for it.

pbachmann commented 11 months ago

@Happypig375

I can see your perspective from newcomers to programming. But since shadowing is useful as demonstrated in other comments, shouldn't this be part of their learning instead of trying to avoid it?

I agree that, if the use of shadowing outweighs the costs, that these benefits should be introduced to newcomers as early as possible to avoid them going down the wrong path.

I completely support your implied suggestion that we should ensure that the code and explanations at Microsoft Learn should reflect whatever best practice is. As it stands, we appear to disagree on what that is, but perhaps if we discuss how to make the appropriate adjustments in Microsoft Learn, we will reach a shared understanding.

Now, there appears to be a problem. As indicated in my most recent answer to @realparadyne, none of the many code examples at Microsoft Learn F# use shadowing. Sometimes this can be understood because these code snippets are small, but even the big ones don't use it eg:

Computation expression example

I don't think it's enough to provide a new topic for Microsoft Learn along the lines of, "Shadowing - how it works and why you need it". I think it is necessary to rifle through their other example code and rework many of them to include shadowing. To do otherwise would be to subtly reinforce, in the mind of the F# newbies, that shadowing is neither necessary nor desirable. (Perhaps not even possible!)

I should point out that I really like the examples from Microsoft Learn, and don't have a problem with any of them even when they say things like:

let a1 = "this"
let a2 = "that"

The use of names like a1 and a2 make perfect sense to me and would not want to change them, but I acknowledge that I am alone in that and that I might benefit from some education in the matter. So let's hop right in and start imagining how we should change the doco.

Of course, there might be questions from the people who wrote Microsoft Learn in the way they did, along the lines of, "Why do you want to make these changes?"

I fear that not only will they want reasons for making changes, they will have had their own reasons for writing things in the way they did and will demand that we fully appreciate those reasons before proposing alternatives.

davidglassborow commented 11 months ago

Given this will never change in F# (it would break everyone including the compiler), I suggest you build an optional analyser to warn about it for people who don't like it.

For learners, it's probably worth teaching them the understanding that some languages support shadowing (another example is rust, another popular language lots of people are learning) and some don't.

smoothdeveloper commented 11 months ago

@pbachmann, When I picked up F#, all the way shadowing works (with order of open, etc.) made much more sense than the languages that preclude things because they can't manage the scope of things well, forcing all kinds of strategies that end up being more complicated (Objective-C and smalltalk for example).

Also consider how it can be used to turn a mutable binding into an immutable, or making sure a symbol you don't want to use after a certain point, won't be used (by assigning some value that you can't pass as parameter anywhere), there all kinds of valid uses, that would require contorsions in languages that don't support shadowing, or that lack some expressive power that F# has, in terms of scope of things.

Of course, maybe an experienced programmer like you may not have much interest in what non-programmers think.

I don't think this is good proposition, it comes from emotional standpoint and we are just discussing technical things :)

In programs, identifiers have a scope, different languages have different scoping rules, F# just allows you to end the scope for an identifier to reuse the name because the context has changed.

Outside of programming, you also reuse the same name reused in different contexts, and with a bit of practice and learning about the contexts, it starts to make sense and actually make communication more fluent (than having to use super long labels always giving all the context, all the time, unless your native tongue is German :)).

The term codegen means different things if you speak in a roslyn meeting than in a F# compiler, at a certain point of the discussion; your first name also mean different people depending the context.

It doesn't preclude anyone to setup more descriptive names, and get them adopted, but it is not always the best thing to do (other things are being optimised constantly).

F# enables both, and people interested in reading or writing code need to get accustomed to the idioms of how a particular piece of code is authored, as there is no silver bullet, but to be always more tolerant, patient, accepting that there can't be a single way to name or implement all the things, that will work for all the people at all time, unless we grow more tolerance and patience, to not get emotional when we face a piece of work, that should only inspire gratitude or at worst case, neutral feeling.

I downvoted the suggestion, but like others, I'm also receptive to the idea of having analyzer that perform what you are after, and a nice way to define where such analyzer should apply, if it suits the taste of authors / maintainers of F# code.

jwosty commented 11 months ago

Yes F# variables are strongly typed. I was making fun of the fact that in F# you can write code like:

let myName = "Sam"
let myName = 2 + 2

which in most languages would seem ridiculous.

This is actually an interesting point. I also use shadowing all the time, but it's (almost?) always based on the previous value of the variable. I struggle to think of a scenario where I shadow a variable and completely throw away the old value -- I'd probably agree that that's much more likely to be a mistake. I'd support it a warning for this case in particular. It could say something like: "Warning: a value named 'myName' is shadowing another value without using it. Consider naming the values differently, or using the old value to compute the new value."

I'd be willing to argue that this lines up with the philosophy of FS0020 ("The result of this expression ... is explicitly ignored"), which is to avoid throwing away information.

I'd be fine with this being in either the compiler or analyzer.

pbachmann commented 11 months ago

@jwosty Thanks for your insights, they open up new lines of inquiry. Your suggestion that discouraging the throwing away of shadowed values would be consistent with F# prohibiting developers from ignoring values in other contexts seems to have merit.

I am reminded of an interview between Scott Hansleman and Don Syme @ 7:58, in which Mr Syme spends 5 minutes explaining why F# code has less bugs and uses the point you alluded to, ie. "expressions can't be ignored", as a prime example.

pbachmann commented 11 months ago

Analysers

A number of contributors including @BentTranberg, @davidglassborow, @smoothdeveloper have suggested that analysers can help us out here. Indeed, right at the start of the discussion, @vzarytovskii said:

Make it another use-case for Analysers SDK which we're planning to work on around F# 9 / .NET 9.

Can we get some clarity as to what exactly an analyser is and how it cooperates with the compiler to produce a better experience for the developer?

To be sure, I agree with @Happypig375 when he emphasised the importance of a responsive IDE:

F# with all its type inference is already an IDE-dependent language. This is simply how the language is used.

Certainly, once the language starts to, for example, infer types, it has become an active part of the development process and the programmer will benefit enormously from the IDE being an active player. I have no problem with that at all, and am grateful that Visual Studio tells me, through tooltips and intellisense prompts, the inferences that F# has made on my behalf and how that affects my code.

What I like much less is where an analyser tries to paper over cracks in the underlying language. Maybe I’ll use an example from another language so the folks here can more readily agree with it.

Recently I was programming in a language called “C#”. I was demonstrating to a client that, after 20 years, C# had now reached a level of maturity where nullable strings were no longer a problem, and to illustrate this, I typed the following lines into a new console project:

var myName = GetStudentName();
string GetStudentName()
{
    return "Fred";
}

I told the client to hover his mouse over the var keyword, and in a successful attempt to make me look incompetent, the tooltip informed us that ‘myName’ is nullable string! After a frantic search of the internet, I found notes from a meeting of the C# development team where it was decided that this should indeed be the case.

So does that mean I have to test for null even though I know something is not nullable? No – because apparently in romps an “analyser”, and the analyser knows better than the compiler. The analyser takes over and suppresses any warnings that the string needs a null-check.

If you type myName on a new line and hover over it, the tooltip tells you that it is nullable string but the analyser reassures you that, at the moment, it isn’t null. Adjustments to the return type and return value of GetStudentName() often behave as you might hope, but seemingly not always. I have seen cases where I tested for a null value and the analyser decided that this, in itself, was proof that the string might be null and now started requiring null checks. Was I dreaming when I saw this? I don't know, because the analyser has its own rules and is not bound by any formal specification. It might be updated every time I install a patch release of Visual Studio.

Is this where F# should be headed?

pbachmann commented 11 months ago

@smoothdeveloper

Thanks for your response.

I will not respond to your points individually as I feel we have now moved on from point-to-point discussion and are now in a position to move forward, building on yesterday's suggestion by @Happypig375:

[...] since shadowing is useful as demonstrated in other comments, shouldn't this be part of their learning instead of trying to avoid it?

I fully agree with Happypig's approach and suggest the following points for moving forward:

The cover letter to our proposed amendments might look something like this:

Dear Learn.Microsoft.F# team,

It has come to our attention that your website conveys, at least in one regard, an inaccurate picture of established F# programming practice. While we think your overall efforts are excellent, we would like to assist you by providing a set of additions and amendments that would help new F# users understand the place of shadowed variables.

Unfortunately, despite having worked tirelessly and selflessly to understand one another, there have been some irreconcilable differences between members of our team, and we have not been able to agree on certain principles. Consequently, we would like to offer you multiple sets of amendments, each making sense in their own way:

[text of amendments]

Yours Sincerely, etc.

Happypig375 commented 11 months ago

@pbachmann For the C# nullability case, it's backward compatibility with code that may assign null to an existing variable. The type of that variable will allow null by default to avoid the assignment of null generating a new warning. image image Nullability analysis is independent of the type of the variable. image image image image

F# will NOT head in that direction because F#'s let is immutable. There is no assignment of null to be considered in most F# code. However, we might do the same analysis for let mutable since by default, they can hold nulls.

Also, you don't send an email for improving Microsoft Learn. You do it here: image It's still GitHub issues. Pull requests can also be done and in fact, submitting the change directly as a pull request is much preferable and faster. image

pbachmann commented 11 months ago

@Happypig375

I deliberately avoided descending into the details, as you have done, as I thought it would distract from the main points I wanted to make.

Happypig375 commented 11 months ago

@pbachmann It's a learning point that can be completed with a simple explanation using null assignment. Sure the ? may stand out but it is quite usable.

Tarmil commented 11 months ago

Yes F# variables are strongly typed. I was making fun of the fact that in F# you can write code like:

let myName = "Sam"
let myName = 2 + 2

which in most languages would seem ridiculous.

This is actually an interesting point. I also use shadowing all the time, but it's (almost?) always based on the previous value of the variable. I struggle to think of a scenario where I shadow a variable and completely throw away the old value -- I'd probably agree that that's much more likely to be a mistake. I'd support it a warning for this case in particular. It could say something like: "Warning: a value named 'myName' is shadowing another value without using it. Consider naming the values differently, or using the old value to compute the new value."

I'd be willing to argue that this lines up with the philosophy of FS0020 ("The result of this expression ... is explicitly ignored"), which is to avoid throwing away information.

I'd be fine with this being in either the compiler or analyzer.

Well, since the first variable is unused, this is already in the compiler as warning FS1182 (off by default): "The value 'myName' is unused".

BentTranberg commented 11 months ago

Just in case somebody actually does want to make an effort to improve the documentation with regard to shadowing, the issue has been touched on earlier in the other repo. I'm not sure action is needed in that particular case, but anyway here's the link:

F# allows re-defining name although documentation says otherwise https://github.com/dotnet/fsharp/issues/9900

pbachmann commented 11 months ago

@Tarmil

[unused value warning] this is already in the compiler as warning FS1182 (off by default): "The value 'myName' is unused".

I wonder whether you and @jwosty are talking about the same thing? It seems to me that what Mr Syme was explaining to Mr Hansleman in the above-mentioned YouTube video was that F# has less bugs because when F# code receives information, it must explicitly do one of three things:

What @jwosty alludes to is perfectly consistent with that, suggesting that to "completely throw away an old value" probably represents a coding error.

The curiosity arises because Mr Syme in the interview said, "F# won't let you do that; it won't let you just throw information away."

On the other hand, Mr Syme did not, as far as I am aware, suggest that labelled values must be explicitly used. Certainly, labelling something with a view to using it in yet-unwritten-code or just checking it out in the debugger is perfectly common in code that is still being developed.

The current remedy provided by both Visual Studio and Visual Studio Code is to slightly grey-out unused labels. This innovation seems quite perfect and whoever thought of it gets a big hug from me.

pbachmann commented 11 months ago

@BentTranberg

Can you point me to the specific F# documentation the issue alludes to? I can't find it.

EDIT: Thanks to @smoothdeveloper ...

https://learn.microsoft.com/en-us/dotnet/fsharp/tour#functions-and-modules