Fsharp: Module level let - in expressions hide the rest of the module

Created on 8 Aug 2020  路  4Comments  路  Source: dotnet/fsharp

In light syntax, when you do not use in with your let binding, it is implied and inserted by the precompiler (at least, that's how it is explained in the F# spec 15.1.1 and 15.1.2).

This works fine, you can use the in or not, it doesn't seem to matter in local bindings.

However, when you use it in a module-level let binding, it hides everything that follows.

Repro steps

```f#
module X =
let x = 42 in ignore x
let y = "test"
()

let x = X.y // error: 'y' is not defined
```

Expected behavior

After the in clause + expr, the declaration should end.

Actual behavior

Anything after the in clause is hidden. You can suddenly have multiple module-level let-bindings with the same name, though they won't be accessible.

Known workarounds

Do not use in to create a shortcut-expression at module-level.

There's no syntax that allows ending the in (at least none that I know of), though it should be ;;, but this isn't valid syntax. Keywords like end or done are not allowed here.

Related information

This appears to have been the case for as long as I could look back, possibly since F# 1.0, so by that one could argue that this is "by design". Though I couldn't find this defined anywhere, and on Slack there seemed to be similar surprise: (thread starts here) https://fsharp.slack.com/archives/C1R50TKEU/p1596912832495400

Area-Compiler Severity-Low bug

Most helpful comment

I agree this wart should be ironed out.

All 4 comments

New insight/workaround, if you place the stuff after in on its own line, the problem goes away:

```f#
module X =
let x = 42 in
ignore x
let y = "test"

let x = X.y // no error anymore
```

(though still not sure why the scoping goes the way it does in the original code, though I think it has something to do with offside rules?)

still not sure why the scoping goes the way it does in the original code

Yes I think that in in conjunction with the default #light syntax has improper scoping. The following code compiles, when I would expect ignore to create an offside position, making the final x out of scope:

let y =
    let x = 42 in
        ignore x
    x // This compiles but shouldn't

While:

let y =
    let x = 42 in
        ignore x
        x // This doesn't compile but should

Do not use in to create a shortcut-expression at module-level.

I'm not sure whether the best thing is to fix this scoping and the module issues, or deprecate usage of in in light mode. It doesn't add a lot of value to F#, and the scoping issues here suggest to me that I should remove the couple of occasions I use in to make one-liners.

Possibly irrelevant notes on OCaml

Interestingly, the following OCaml code compiles, because it has insignificant whitespace.

let y =
  let x = 42 in ignore x;
  x;;

But that doesn't mean the F# code should compile. The F# code should be equivalent to:

let y =
  let x = 42 in ignore x; ;
  x;; (* Error: Unbound value x *)

OCaml doesn't have the module issues described here, unsurprisingly as in gets a lot more testing in OCaml. E.g. the following code does not compile, while the original code with let y = "Test" does compile:

module X = struct
  let x = 42 in ignore x
  let y = x (* Error: Unbound value x *)
end
let z = X.y

With apologies for errors, first time using OCaml. Aside: the fact that verbosity allows spacing to be completely constrained is an interesting advantage.

I agree this wart should be ironed out.

It looks similar to #7741.

Was this page helpful?
0 / 5 - 0 ratings