fsharp / fslang-suggestions

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

Remove 'no module' restriction on .fsx files #1077

Open pkese opened 3 years ago

pkese commented 3 years ago

Remove restriction that main .fsx file mustn't define a module

Dotnet fsi fails if the main .fsx file defines a module:

module Hello
printfn "Hello world"
> dotnet fsi Hello.fsx
Hello.fsx(3,1): error FS0010: Unexpected start of structured construct in definition. Expected '=' or other token.

What is allowed is

module Hello =
    printfn "Hello world"

or just

printfn "Hello world"

A full-fledged F# project however does not have this restriction.

This makes it impractical to experiment with interactive workspaces, because one always has to remove the module definition before testing a single file with fsi and then put it back if one wishes to reference the file from somewhere else.

I would therefore propose to remove this 'no module' restriction.

This is based on report: https://github.com/dotnet/fsharp/issues/11699

Pros and Cons

The advantages of making this adjustment to F# are

The disadvantages of making this adjustment to F# are

Extra information

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

Related suggestions: none that I know

Affidavit (please submit!)

Please tick this by placing a cross in the box:

Please tick all that apply:

For Readers

If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

Happypig375 commented 3 years ago

This makes it impractical to experiment with interactive workspaces

You can always do #if INTERACTIVE as a workaround. "Impractical" is an overstatement here.

SchlenkR commented 3 years ago

"Impractical" is an overstatement here.

It might be impractical for beginners who are learning F# by using FSI. Learning a language is hard enough, and being confronted with misalignments (here: works in projects, not in FSI) is then even harder (I remember that I stumbled across this, too).

One thing to keep in mind: Working in FSI means often: The developer has to know which code was evaluated when and how, or in other words: One has to mind-map the code displayed on screen vs. the program currently available in FSI. Having a

module X
[code]

on top of an fsx file would hopefully not be understood as some kind of directive for tooling in a way that all code in that file will be always related to that module, no matter how it is evaluated. Instead, it would behave ideitically to:

module X =
    [code]

This is important to understand when adding / changing code incrementally that is not in global scope.

smoothdeveloper commented 3 years ago

I don't have a strong opinion, I also faced a bit of head scratching on this matter when I picked up FSI originally, but it made sense once I understood how to use #load and the various implications about partial evaluations in context of FSI and the implicit "put in a module" if you #load an .fsx which doesn't even bear a module name.

This makes it impractical to experiment with interactive workspaces, because one always has to remove the module definition before testing a single file with fsi and then put it back if one wishes to reference the file from somewhere else.

I'd say impractical here is a bit strong, despite this limitation, FSI is practicable, was, even when I was learning.

Maybe a stop gap suggestion is to improve the error message with specific guidance if this occurs in FSI?

If there is an easy way to fix this issue, which doesn't have negative impact on how things would be evaluated on subsequent partial evaluations (which I'm not clear there is so far), I agree removing this hump could be helpful, but enhancing the error message would also achieve this IMO.

pkese commented 3 years ago

@Happypig375
Imagine I have a library that I typically use from somewhere else with #load.
But I also like to keep a little test for it, which I run by executing just this particular file.

module Sum

let add x y = x + y

if fsi.CommandLineArgs = [| __SOURCE_FILE__ |] then
    assert(Sum.add 1 2 = 3)
    printfn "my code tested fine"

I don't know, how I can fix this issue using #if INTERACTIVE, because it is always interactive:
If I reference it in another intractive script using #load, then it's interactive,
and if I run it directly using dotnet fsi it is also interactive.

There are lots of cases with similar use, e.g. code with database access functions (queries and inserts) which optionally parses some command line flags and renders create table statements when run on its own.
It is a common pattern in non-compiled languages.

dsyme commented 3 years ago

Generally I'm in favour of this. However one question, for this:

module M
let x = 1
module M = 
    let x = 2
let y = M.x

What would be the result when executing the first fource lines then the last line as separate intereactions in F# Interactive? Is y 1 or 2?

That is, does executing

module M
let x = 1

actually establish a module M? Or is module M ignored in script fragment executions in favour of the implicit module for each script fragment?

SchlenkR commented 3 years ago

@pkese I think @Happypig375 idea was this:

file "sum.fsx"

#if COMPILED
module Sum
#endif

let add x y = x + y

if fsi.CommandLineArgs = [| __SOURCE_FILE__ |] then
    assert(add 1 2 = 3)
    printfn "my code tested fine"   

Now, using "add" by #load-ing that file from another file:

#load "sum.fsx"
open Sum

printfn $"sum of 10 and 15 is {add 10 15}"

This works as long as the module name ("Sum") equals the file name without extension ("sum.fsx" - first char case insensitive), because when #load is used, FSI creates a module (fst letter capitalized) according to the loaded .fsx file name. According to this pattern, I propose this:

ntwilson commented 3 years ago

Another consideration is private constructors. Suppose in one file I have

namespace My.Namespace

type MyType = private MyLeft | MyRight 

and in another file (MyOtherModule.fs) I have

#if COMPILED
module My.Namespace.MyOtherModule
#endif

let lft = MyLeft

Then it will compile just fine, but you won't be able to use it in FSI at all, because it will complain about MyLeft being inaccessible from this location. Also, making FSI just ignore the module My.Namespace.MyModule line won't resolve this at all. I actually don't know of any way of getting MyLeft in scope in FSI since I don't believe any sort of namespacing is allowed.

dsyme commented 1 year ago

@pkese I've marked this as approved-in-principle