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

Top level F# progams - implicit async and access to command-line args

Open bugproof opened this issue 4 years ago • 12 comments

Top level F# progams - implicit async and access to command-line args

See https://github.com/dotnet/fsharp/issues/11631 and https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements for more detailed explanation

In C# 9 we can write code such as this

using System.Net.Http

using var httpClient = new HttpClient();
var str = await httpClient.GetStringAsync("https://example.com/");
Console.WriteLine(str);

You can also access args from top level statements which you can't do from F#.

The existing way of approaching this problem in F# is:

There's no good way. See this comment https://github.com/dotnet/fsharp/issues/11631#issuecomment-855052325

Pros and Cons

The advantages of making this adjustment to F# are: shorter, easier code. No need to remember code such as this

let theAsyncTask : Async<int> = ...

[<EntryPoint>]
let main argv =
  async {
    do! Async.SwitchToThreadPool ()
    return! theAsyncTask
  } |> Async.RunSynchronously

The disadvantages of making this adjustment to F# are: I don't see real disadvantages.

Extra information

Estimated cost (XS, S, M, L, XL, XXL): I don't know what the cost is.

Related suggestions: https://github.com/dotnet/fsharp/issues/11631

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:

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

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.

bugproof avatar Jun 05 '21 12:06 bugproof

Do we want an implicit async context or a task/backgroundTask context as specified in https://github.com/dotnet/fsharp/pull/6811? Moreover, you can get the equivalent of args with System.Environment.GetCommandLineArgs().[1..] (the first one is the program path and name which the C# args omit implicitly). Will it be too implicit to provide args without a definition?

Happypig375 avatar Jun 05 '21 13:06 Happypig375

Personally, I think that having an implicit async (or really, any implicit computation expression) in an implicit entry point is not a good idea, especially when noted above that there are multiple ways to represent an asynchronous operation (Async and Task), as well as potentially multiple different computation expression builders which can be used to build them. Rather, I think it would make more sense to determine what return value is used (i.e. the type of the last expression in the top level statements) to determine what type of entry point is created. If it is an int, wrap it in a regular entry point. If it is an int async, wrap it in an Async returning function, and have the generated entry point call the function passing the result into Async.RunSynchronously. If it is a Task<int>, do the same, except call GetAwaiter().GetResult() instead. Also, maybe allow unit types as well, as it appears this is how implicit entry points are handled currently.

Implemented this way, you would still need to wrap the code in async { ... } or task { ... } or whatever (or, at least, make sure the last statements are wrapped as such), but you would not need to perform the extra ceremony of applying Async.RunSynchronously or GetAwaiter().GetResult() or whatever else.

TheJayMann avatar Jun 05 '21 14:06 TheJayMann

Implemented this way, you would still need to wrap the code in async { ... } or task { ... } or whatever (or, at least, make sure the last statements are wrapped as such), but you would not need to perform the extra ceremony of applying Async.RunSynchronously or GetAwaiter().GetResult() or whatever else.

Which will in the end give you as much benefit as C#'s async main. That is to say, save you a handful of characters every time you need to write main from scratch. How often do you do that, how much typing/time will this realistically save? I'm not against this, but I feel the perceived benefit would be very low.

kerams avatar Jun 05 '21 14:06 kerams

I think we have a pretty good escape hatch in the form of FSI for what seems to me to be a primarily c# problem: having to make a full project for tiny executables to try things out, which leads to a certain amount of boilerplate overhead for each of these tiny executables.

baronfel avatar Jun 05 '21 15:06 baronfel

Well, I really like this feature in C# and it really simplifies code for small tools/scripts. The compiler detects what you use in top-level statements and generates appropriate main for you https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements#implicit-entry-point-method

Also see this https://github.com/dotnet/csharplang/issues/2765

bugproof avatar Jun 06 '21 09:06 bugproof

I would personally favor a succinct convention for getting the arguments, but I would not prefer an implicit async or task-based context. That complicates things for C# today, because it forces all type definitions to come at the bottom of the file. Whereas not having that makes the experience mostly the same today in current F#.

Another thing to consider is that this might be more of an educational problem for F# than anything else. If the answer to the scenarios where C# top-level projects are used is to use an F# script, and it's not obvious, then that should be addressed somehow.

cartermp avatar Jun 06 '21 14:06 cartermp

A succinct convention for getting the arguments:

let args = System.Environment.GetCommandLineArgs().[1..]

Happypig375 avatar Jun 06 '21 14:06 Happypig375

I would argue that's not terribly succinct. I realize it's the .NET way of doing sys.argv, but that's a pretty rough line of code. fsi.CommandLineArgs in scripts is a great way to do it, hence my comment about scripts and discovering they're the right choice for a scenario like this.

One approach could be to alias args in FSharp.Core. It would take care of handling name resolution correctly, but could also be confusing since it's such a common name for things in user code.

cartermp avatar Jun 06 '21 15:06 cartermp

cmdArgs or cmdLineArgs is an alternative to args.

Happypig375 avatar Jun 06 '21 15:06 Happypig375

I dislike the implicit async too, it could be a Task or Async or any other thing

But I think that a succinct args and maybe a default [<EntryPoint>] would be nice

lucasteles avatar Apr 27 '22 15:04 lucasteles

I wanted the availability personally for command line arg and I wanted EntryPoint accepting string[] -> Async because I was expecting this while creating a console app :

  1. fsharp libraries nowadays are for me personally already polluted with async
  2. EntryPoint expects a function accepting command line arg so I in turns expect command line arg be handed to me in top level without me personally get System.Environment.GetCommandLineArgs() and assign to a variable arg

Xyncgas avatar Feb 23 '25 19:02 Xyncgas

MinimalAPI-style host builders are becoming pervasive in the .NET ecosystem, and they expect args as first argument. Aspire, for example, uses one. And the lack of args in F# can bite in unexpected ways, see for example https://github.com/Tarmil/FSharp.Aspire.Hosting/issues/6.

Tarmil avatar Mar 04 '25 10:03 Tarmil