fsharp / fslang-suggestions

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

Support attributes on lambda expressions #984

Open cartermp opened 3 years ago

cartermp commented 3 years ago

I propose we support attributes on lambda expressions. A way this could get used is in the experimental aspnet Houdini project, which is experimenting ways to cut the cruft in aspnet core service development, including an alternate routing system than MVC. One of their approaches would be mapping endpoints with lambdas:

record Todo(int Id, string Name, bool IsComplete);

app.MapAction([HttpPost("/")] ([FromBody] Todo todo) : Todo => todo);
app.MapAction([HttpGet("/")] () : Todo => new(Id: 0, Name: "Play more!", IsComplete: false));

Note that the lambda itself has an atttribute. In F#, the above snippet would probably look something like this:

type Todo = { Id: int; Name: string; IsComplete: bool }

app.MapAction([<HttpPost("/")>] fun ([<FromBody>] todo) -> todo) |> ignore
app.MapAction([<HttpGet("/")>] fun () -> { Id = 0; Name = "Play more!"; IsComplete = false }) |> ignore

The existing way of approaching this problem in F#, using the above example, would be to pull out the lambda bodies into separate functions and pass them into a constructed Func:

type Todo = { Id: int; Name: string; IsComplete: bool }

[<HttpPost("/")>]
let echoTodo ([<FromBody>] todo) = todo

[<HttpGet("/")>]
let getTodo () = { Id = 0; Name = "Play more!"; IsComplete = false }

app.MapAction(Func<Todo,Todo>(echoTodo)) |> ignore
app.MapAction(Func<Todo>(getTodo)) |> ignore

This isn't really a bad alternative though, the main thing is that it's annoying to have to construct a func. Instead, lobbying to get MapAction to support equivalent FSharpFunc types as overloads and simply have those overloads construct the appropriate Func type could make this approach nice from an F# perspective.

However, I still think it's worth considering (even if people decide it's not really useful for F#) since this is an approach to programming against an API that more library authors may take in the future.

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): M

Related suggestions: (put links to related suggestions here)

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.

charlesroddie commented 3 years ago

ways to cut the cruft in aspnet core service development

So near and yet so far.

A lot of object-oriented programming styles are so averse to using objects. I wonder why they do not go all the way and replace all type information with annotation?

[<HttpGet("/")>]
[<Id(0)>]
[<Name<"Play more!">>]
[<IsComplete(false)>]
let todo = object()

Then everything is object()!

Overall MapAction seems uninteresting to F# because it's intrinsically annotation-based and has weird JSON dependencies which imply that everything is implicit not explicit. F# should not support this sort of thing. Intstead there should be an action creator with a genuine type signature, such that when given objects of the right types the code will work.

Happypig375 commented 3 years ago

We can use this to support static lambdas, if we decide to do it.

halter73 commented 3 years ago

We're planning to make the route pattern an explicit parameter again rather than take it from an attribute which should hopefully reduce "call site complication". https://github.com/dotnet/aspnetcore/issues/30448

With this change, the F# example would look more like the following:

type Todo = { Id: int; Name: string; IsComplete: bool }

app.MapPost("/", fun ([<FromBody>] todo) -> todo) |> ignore
app.MapGet("/", fun () -> { Id = 0; Name = "Play more!"; IsComplete = false }) |> ignore
charlesroddie commented 3 years ago

@halter73 An API that respects the .Net type system would avoid nongeneric classes like Delegate, annotations like [<FromBody>]. It would be something like:

MapPost(string pattern, System.Func<System.Web.HttpRequest, System.Web.Mvc.ActionResult> poster)
MapGet(string pattern, System.Func<System.Web.Mvc.ActionResult> getter)
davidfowl commented 3 years ago

Then we'd need infinity (hyberbole) overloads. Which makes things even more complex (any number of arguments, sync vs async, ValueTask vs Task)

charlesroddie commented 3 years ago

Then we'd need infinity (hyberbole) overloads. Which makes things even more complex (any number of arguments, sync vs async, ValueTask vs Task)

Sync/ValueTask/Task is 3. Why would you need "any number of arguments"?

halter73 commented 3 years ago

Why would you need "any number of arguments"?

For convenient parameter binding. So instead of having let! todo = httpRequest.ReadFromJsonAsync<Todo>() |> Async.AwaitTask, the framework can do this for you before calling a user-defined Action<Todo> or Func<Todo, Task<MyResponse>> or whatever.

If a developer also wants to get an id from the route (ex: "/api/todos/{id}"), they can add an int parameter instead of calling let id = int httpRequest.RouteValues.["id"] and having to somehow deal with invalid ints. By adding an int parameter named id, you declaratively tell the framework that you expect the {id} part of the route to be an int and to not even call into user code if it's not.

davidfowl commented 3 years ago

Sync/ValueTask/Task is 3. Why would you need "any number of arguments"?

We can even natively support F# async without defining that overload.

Because we want to support binding arguments from various sources:

The beauty of supporting any function type is that we can add support for new things in the future, without adding more overloads.

davidfowl commented 3 years ago

@cartermp It occurs to me that this is just one of the many features we need to make this work smoothly on F#. The other is around supporting conversion from F# lambdas to Delegate without an explicit cast.

dustinmoris commented 3 years ago

So thanks to @davidfowl I'm going to leave some feedback here, but unfortunately I have many random thoughts which are not strictly limited to this specific issue. I appreciate that this is not the right place to log them all, but if it's ok with you I'll dumb my thoughts here for now and then move any actionable issue elsewhere with your guidance.

Ok, so here we go...

First I welcome the changes in ASP.NET Core to introduce a more flexible API which feels closer to the metal. At least this is what my impression is by looking at what you guys are working on and I think it's a great idea!

Personally I think the current approach is not bold enough. It's just another "big" framework with a slightly different syntax. IMHO the approach taken tries to solve too many responsibilities at once. The new Houdini style framework tries to appease to too many developers, people like myself who want to have simple to use and flexible lower level API as well as to the "spoilt" developer who just wants a "plug & play" MVC-style API. I think trying to achieve both with a single framework will ultimately fail to deliver the right abstraction which can appeal to both groups and will only change the perception temporarily at best.

If it was my decision then I'd suggest to create a very low level (almost Go like) HTTP API which is super flexible and truly bare bones which gives developers maximum control and yet a good enough convenience API which can empower the minimalist developer to feel extremely productive and not having to fight the tools. Using those low level APIs Microsoft could create a second higher level "plug & play" framework for the easy beginner friendly path. Currently this was meant to be ASP.NET Core and MVC, but I think they failed because the lines were not clearly drawn and many ASP.NET Core APIs were specifically built with MVC in mind and not thought of as a standalone library. I almost think that both these things should get built by different teams to avoid the same mistakes and not mud the waters.

Now taking a bit more specifically about the low level API which I'd like to see. I don't think it needs (or should) have attributes at all. Declarative APIs are very high level. It means there is something else sitting below which expects certain idioms to be followed in order to carry out useful tasks. If something doesn't follow these idioms then things stop working or fall apart. At least things will not work entirely as advertised which is what will users give the feeling of fighting the tools. Regardless how well one thinks they have designed those idioms, you can only design what you anticipate for. Developers will always want to do something new, something unconventional, something which seemed silly one day but makes sense today and then a declarative API will quickly show its cracks.

Therefore I suggest to keep it truly low. No attributes, no declarative model binding. Just have low level functions such as

Does it mean that things like authentication through a single middleware might not work or that another middleware won't be able to produce comprehensive Swagger docs? Yes, but that is ok. I would even suggest that concerns such as authentication and CORS should be moved closer to the endpoints where they are actually being required and not sit in a global middleware which therefore relies on a heavy declarative API in order to centrally control every single endpoint.

Again, this only has to be true for the low level API. Move things closer to where it is required. Having something like app.MapAction("/foo", requiresBasicAuth(cors((someHandler))) would be really nice.

Yes it's ugly, but it's low level beauty. People who will use that will build their own meaningful wrappers and APIs to build the app which they want. It empowers them and gives them 100% control and lets them create the beautiful APIs which they see fit.

On top of these simple idioms you can have a separate attribute-driven API which gives you a more out of the box experience. Of course, in return this will force a user down a certain path but it's a trade-off that people will accept when they know that they can break out into a lower level world if they know that exists.

The other thing which I would try to keep in mind is really the naming of things and how it affects the beauty of APIs. @davidfowl mentioned it on Twitter and I couldn't agree more. Beauty is important and in my opinion a big principle of beauty is to ensure that the name of a method or function must match the complexity underneath, otherwise people will think it's magic or too verbose.

For example, mapping a single string to a route feels like a very trivial task. A method name like MapAction doesn't strike me as a great name as it's unnecessarily long and verbose:

app.MapAction([HttpPost("/")] ([FromBody] Todo todo) : Todo => todo);

Better:

app.GET("/", ctx => someFunctionToHandle)

However, if something is more complex then it is ok to have a slightly longer name.

Bad:

var model = ctx.Bind<T>()

Good:

var model = ctx.Request.Body.Bind<T>()

Now coming back to F#....

Overall I think it would be a mistake to introduce Attributes as a core concept into F#. We already have them, but their use is very limited and often only in relation to C# interop, but such a change would make attributes a first class citizen which feels wrong if it's only needed in order to mould F# around a new .NET web framework which couldn't let go of a declarative API.

EDIT:

Until ASP.NET Core offers true low level APIs which don't need attributes or where endpoints must rely on centrally controlled authentication/CORS/etc middlewares then developers will always come and label the current framework as too heavy and too bloated because if they reject the existing API they are left with very little to no other options. Please believe me, I can guarantee you 100% that this will be the case because it's always been like that.

davidfowl commented 3 years ago

That's really good feedback, feel free to close this issue. I see now that this isn't idiomatic at all in F# abs users are better served by something like giraffe. We'll definitely consider adding something like bind (though we do that already have some bring for JSON, and it's not clear that we need any more than that).

As for the requireAuth(cors(...)) I'm not sure we'd add something like that since we already have an imperative way of adding metadata to endpoints.

Again, this is good feedback, muchly appreciated

cartermp commented 3 years ago

@davidfowl

The other is around supporting conversion from F# lambdas to Delegate without an explicit cast.

F# supports direct conversions at the call site like so:

open System
type C() =
    member _.M(f: Func<int, int>) = ()
    member _.M(f: Action<int>) = ()

let c = C()

c.M(fun x -> x + 1)  // direct func interop
c.M(fun x -> printfn $"{x}") // action interop

let funcThing = fun x -> x + 1
let actionThing = fun x -> printfn $"{x}"

c.M(funcThing)  // direct func interop
c.M(actionThing) // action interop

But there can be some gaps, and it'd probably ease current interop patterns with ASP.NET Core. In the preview bits I played with for some reason I couldn't just interop directly like I can above, so that could be addressed separately.

As for the general point @dustinmoris and @davidfowl I don't think there's much harm in F# adopting smoother .NET interop support for existing constructs like this. It is also a matter of orthogonality that is worth discussing - if you can put an attribute on an F# function, why not a lambda when they have near-identical correspondence? I think there are certainly some drawbacks to using it, and I would personally prefer not to use an API that requires lots of attributes. But that's also why we have wonderful choices like Giraffe, Saturn, Falco, Suave, etc.

@davidfowl perhaps a better way to ease F# use of aspnetcore is to work out a few of the pain points with @dustinmoris and identify things where:

I don't have context on the former piece, but I'd see it as akin to MVC supporting routes that are written directly as F# async. When that was done, MVC apps didn't need to call Async.StartAsTask at the end of each route if they wanted to keep using F# async.

dsyme commented 3 years ago

But there can be some gaps, and it'd probably ease current interop patterns with ASP.NET Core. In the preview bits I played with for some reason I couldn't just interop directly like I can above, so that could be addressed separately.

FWIW these issues are, I believe, ironed out by https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1093-additional-conversions.md. However we should trial and check (it may not apply to overloaded cases for example).

I don't think there's much harm in F# adopting smoother .NET interop support for existing constructs like this. It is also a matter of orthogonality that is worth discussing - if you can put an attribute on an F# function, why not a lambda when they have near-identical correspondence?

I agree we should address this as it's a basic matter of interop-capability for F# and we'll inevitably encounter attribute-aware .NET frameworks

halter73 commented 3 years ago

The other is around supporting conversion from F# lambdas to Delegate without an explicit cast.

I guess this got lost when this got turned into an issue. Here's the original email for context.

Hi Phillip,

I'm reaching out because we're planning to add support for lambda attributes in C# 10 and I'm wondering if this also makes sense for F#. It's going to be important for a MapAction() API we're planning to add to ASP.NET.

Here's a small example of using MapAction with and without the new lambda features in C#:

Comparing main..lambda-attributes-return · halter73/HoudiniPlayground (github.com)

And the comparison in F#:

Comparing f1b94d2...b70aa73 · halter73/HoudiniPlaygroundFSharp (github.com)

Aside from supporting attributes on lambdas and lambda arguments, this also relies on lambdas having a "natural type" when assigned to a "Delegate" (the type expected by MapAction). We want to avoid an error like the following:

C:\dev\halter73\HoudiniPlaygroundFSharp\Startup.fs(24,36): error FS0193: Type constraint mismatch. The type ↔ 'unit -> int' ↔is not compatible with type↔ 'Delegate'

Even trying to downcast with :?> Func<Todo> fails with the following, so the "before" in my F# before and after sample doesn't even compile today.

C:\dev\halter73\HoudiniPlaygroundFSharp\Startup.fs(31,37): error FS0016: The type 'unit -> Todo' does not have any proper subtypes and cannot be used as the source of a type test or runtime coercion.

Do you think this is something F# could support? I think an API like MapAction lends itself to interactive programming of the type F# seeks to enable. In fact, I've been using .NET Interactive for a lot of testing. It's been with C# so far, but I'd like to be able to use F#!

@cartermp Then contributed some PRs to the HoudiniPlaygroundFSharp repo that's gotten it closer to something that could theoretically work: https://github.com/halter73/HoudiniPlaygroundFSharp/pull/1 and https://github.com/halter73/HoudiniPlaygroundFSharp/pull/2, but ~neither version works yet~ the MapPost doesn't work yet because ASP.NET cannot see the [<FromBody>] attribute on todo.

Edit: The MapGet does work because there aren't any attributes required.

davidfowl commented 3 years ago

Right I'd like the examples that @halter73 posted to work with F# as well, attributes aside

halter73 commented 3 years ago

We have a new design at https://github.com/dotnet/aspnetcore/issues/30248 that will infer the sources for some parameters without attributes that follow conventions common to ASP.NET Core MVC.

Because of these conventions, even the [<FromBody>] attribute on the POST can now be removed leaving no attribute left in the original sample!! Attributes will still be important to get the full flexibility of the API.

After the changes, the following will work. The MapGet already works today if you use the nightlies.

type Todo = { Id: int; Name: string; IsComplete: bool }

app.MapPost("/", Func<Todo,Todo>(fun todo -> todo)) |> ignore
app.MapGet("/", Func<Todo>(fun () -> { Id = 0; Name = "Play more!"; IsComplete = false })) |> ignore

Having to write out Func<Todo,Todo> feels a lot like the unnecessary casting we currently have to do in C#, but since that allows for type inference in the lambda definition maybe it's not so bad in F#.

charlesroddie commented 3 years ago

@dustinmoris If it was my decision then I'd suggest to create a very low level (almost Go like) HTTP API which is super flexible and truly bare bones with maximum control but good enough in terms of convenience so that power users can be productive without fighting the tools and then a second plug&play framework on top of that low level API for the easy beginner friendly path.

YES. And the low level API should be a type-safe one. The convenience API can be completely designed with typical C# devs in mind.

@davidfowl and @halter73 were suggesting that for convenience we should lose static typing and use a dynamic convention-based system.

The ASP.Net team shouldn't need to bother about a convenience layer for F# since we should be able to use a type-safe layer directly or write our own convenience layer. The important thing is that all of this can be built on top of a type-safe core API. What is not acceptable is forcing people to use a dynamic API in .Net! Any dynamic API should be optional (nuget package or linker-compatible so it can get linked out if not used).

davidfowl commented 3 years ago

Thanks @charlesroddie. We don't want to force anyone to use anything and want to expose the right building blocks so other libraries can build idiomatic layers. We already have a strongly typed core built around a strongly typed RequestDelegate (giraffe is built on top of it). We've also added APIs to read and write JSON from and to the request and response and will continue to do so as we push more features into the core as building blocks.

That doesn't change what we're asking for here, but I think it's partially up to the F# community to decide if this is anti F# or not. The building blocks already exist and we will continue to build more of them but we also like the declarative model for C# as it can remove lots of boilerplate in lots of cases. This isn't zero sum.

toburger commented 3 years ago

That doesn't change what we're asking for here, but I think it's partially up to the F# community to decide if this is anti F# or not. The building blocks already exist and we will continue to build more of them but we also like the declarative model for C# as it can remove lots of boilerplate in lots of cases. This isn't zero sum.

I think that's the crucial point here: When speaking of declarative programming in C# often an AOP approach is meant (applying attributes to parameters to control application specific semantics) that weakens the type checking (everything comes together at runtime). In F# declarative programming is achieved by using F#'s idioms such as expressions (instead of statements), EDSLs and type safe programming which composes at the type checker level.

dustinmoris commented 3 years ago
app.MapPost("/", Func<Todo,Todo>(fun todo -> todo)) |> ignore

I don't know what type app is here, but if there was a Router object I'd love to use an API like this:

let router = new Router()
router.GET("/", indexHandler)

...
// During web host creation:
app.Endpoints.MapRouter(router)

app.Run()

Also the |> ignore is extremely annoying in F#. That means that your current MapGet methods return an object to do chained mapping probably like MapGet(....).MapPost(...). I wonder if that is really required? Is this how YOU see the API should be used? If not and if all examples map one route per statement then why not just make it a void method and then F# developers don't have to ignore everything? I'm just asking because it seems that in C# you actually don't really envision it to be chained so if you change the return type to match your own expectations then you'd make a huge difference to F# developers as well! The other alternative would be to have an overload or another set of the same methods just voided so that it's more natural from F#.

EDIT:

That's really good feedback, feel free to close this issue.

@davidfowl I didn't mean to put a downer on the Houdini API or suggest that this issue should get closed. Clearly the ASP.NET team sees a desire for a web framework which appeals more to the minimalist developer and I only wanted to share some thoughts of someone who considers themselves a minimalist developer with what I thought could be a problem. I think the developers who want to be productive with a higher level declarative API are already very well served with MVC and I think the new Houdini API doesn't necessarily have to compete with MVC but maybe could focus more on what MVC doesn't do well today.

I don't know how much is already set in stone, but I'm happy to provide as much constructive feedback as you think is still helpful at this point :)

halter73 commented 3 years ago

The app is an IEndpointRouteBuilder. The returned object isn't for chained mapping like MapGet(....).MapPost(...). It's for adding metadata to the route for things like auth.

app.MapPost("/", Func<Todo,Todo>(fun todo -> todo)).RequireAuthorization() |> ignore

I agree the |> ignore is extremely annoying though.

If you're interested the docs are at https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0#routing-concepts

dustinmoris commented 3 years ago

Right, I see....

Another option which I think could work nicely for C# and F# could be by making the handler method the final void part of the chain, forcing metadata to be set beforehand:

 app.POST("/api/foo").RequiresAuth().EnableCors().Handle(fun todo -> todo)

However, the problem only exists because your current framework requires those extra metadata functions in order to feed the information into a central auth middleware and other middlewares. If The API was "lighter" then none of that would even be an issue to begin with :)

dustinmoris commented 3 years ago

Also, re naming, see how my example reads so much simpler and quicker and allows a developer to comprehend what is happening than the version where everything is stretched much longer with MapAction/MapPost, RequireAuthorization, ...

app.MapPost("/", Func<Todo,Todo>(fun todo -> todo)).RequireAuthorization() |> ignore

vs

app.POST("/").RequiresAuth().Handle<Func<Todo,Todo>>(fun todo -> todo)

The benefit of a final Handle chain function could also solve the type casting like so:

type TodoHandler = Func<Todo,Todo>
app.POST("/").RequiresAuth().Handle<TodoHandler>(fun todo -> todo)
halter73 commented 3 years ago

Personally, I think void Handle method at the end of the chain reads well and like that it gets rid of the |> ignore. We already ship MapGet/MapPost/... RequestDelegate overloads with the metadata chained at the end though, so it doesn't really make sense to switch conventions at this point. I do think libraries like giraffe can help here by building more idiomatic F# APIs on top of it and similar APIs like RequestDelegateFactory (https://github.com/dotnet/aspnetcore/issues/31181).

dustinmoris commented 3 years ago

Also one more thing, I'm not even sure if RequiresAuth() is a good abstraction, because that means it will lead to something like this:

type TodoHandler = Func<Todo,Todo>
app.POST("/").RequiresAuth(fun options ->
    // configure auth options
).Handle<TodoHandler>(fun todo -> todo)

Which leads to chains of builders within builders which are really not nice. They are not nice in production code and they are not nice in demos. Many layers of builders make C#/F# code look like a snake with a lot of indentation going in and out multiple times, a bit like a huge HTML file with tons of nested divs. Just the raw look of it makes things look "huge" and "bloated" and "complicated". This kinds of feeds back into the beauty of APIs again. The more the final code is just a set of simple top level statements the nicer it feels and reads. Just my own opinion.

I'd suggest to stick with the first level of abstraction and just make better use of that like so:

type TodoHandler = Func<Todo,Todo>
app.POST("/").BasicAuth().JWTAuth().SessionCookies().Handle<TodoHandler>(fun todo -> todo)
dustinmoris commented 3 years ago

@halter73

Sure I understand that certain APIs have already been shipped and are set in stone, but if there is still an opportunity to make things nice across all of .NET then it's worth considering.

After all Giraffe is not a different web framework, I never saw it that anyway. It's just a wrapper library to make ASP.NET Core work for F#. Honestly, I wish this wasn't required in the first place if you see what I mean.

halter73 commented 3 years ago

I completely agree we should make ASP.NET Core as usable out of the box as possible for F#. I appreciate the input. There's still a lot of open design space for the API, but the other existing overloads for MapGet/MapPost/... do create expectations.

Another API you might be interested in is our new "direct hosting" API https://github.com/dotnet/aspnetcore/issues/30354. It's heavily inspired by Feather HTTP.

dustinmoris commented 3 years ago

Cool will take a look. Thanks for linking!

davidfowl commented 3 years ago

That's good feedback about some of the fluent APIs and how it has the potential to complicate the code and make it look like a snake. In practice that doesn't happen because options aren't specified the way you describe.

Also one big design goal is NOT to change the existing idioms we're already created. We're not building a new framework, we're refining what we have carefully so we don't break the existing patterns and can take advantage of all the code that is and the ecosystem has written over the last 5 years. That is an explicit design goal.

davidfowl commented 3 years ago

PS: I also like the handle method at the end of the fluent chain of calls. We can propose an alternative API for building up a route, that'd be interesting.

lambdakris commented 3 years ago

I still program mostly in C# for work and I gotta admit that I really like the look of something like what @dustinmoris suggested, for both F# and C#!

app.Post("/").RequiresAuth().Handle((Todo todo) => 
{ 
  return todo;
});

So I like the idea of looking into an alternative API for building up a route, for C# as much as for F# :)!

P.S. I also support the idea of adding support for Attributes on lambda expressions in F#, if for nothing else than completeness/symmetry

Happypig375 commented 3 years ago

@lambdakris That's a weird hybrid of C# and F# there

smoothdeveloper commented 3 years ago

@Happypig375 the kind of weird hybrid that @cartermp code fixers will fix in F# editor 😄

lucasteles commented 1 year ago

Despite the minimal-api applicability, I feel like the attributes on lambda expressions is something F# should have, in the end, will be an interop limitation, if not with minimal-api, with other libraries or frameworks of .NET ecosystem

T-Gro commented 1 year ago

I have tried searching for documentation on this, but failed - how does a typical C# library code search for attributes on arguments of lambdas? Ideally following a doc recommending how these things are typically written. Ideally in an example of real C# code, that does the basic checks (alongside of checking attributes on classes, methods and their arguments).

Why I ask this: The F# codegen can put an attribute on the generated method behind the F# lambda, but that still gives no guarantee that C# written library code will find the attribute where it expects to. (e.g. if designed around Func, I could imagine it might expect to see the argument placed on the generated delegateArg0 here https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AbEAzAzgHwxgBcACAcxIDFcALAQygAcAZegW2ABN6AKASlIBeUr2wBXAHakAnqTgA+WaQDUpAKz8AsACgiZPA2ZtOPYRWp1GrDtz7a9JUvVwBBKlLDmAyjNzEYdgA6D0kwAB4AS0liNGjiJTErY1sefiA=).

CosminSontu commented 1 year ago

Any chance this will make its way into F# 8 ? Thanks

vzarytovskii commented 1 year ago

Any chance this will make its way into F# 8 ? Thanks

No, it needs a formal approval, as well as F# 8 is frozen feature-wise.

En3Tho commented 1 year ago

Wanted to share that there is a really cool library for cli based apps which has minimal api inspired design: https://github.com/Cysharp/ConsoleAppFramework

And example is the topic is not working.

type Todo = { Id: int; Name: string; IsComplete: bool }

[<HttpPost("/")>]
let echoTodo ([<FromBody>] todo) = todo

[<HttpGet("/")>]
let getTodo () = { Id = 0; Name = "Play more!"; IsComplete = false }

let oneMoreTest(webApp: WebApplication) =
    webApp.Map("", Func<Todo,Todo>(echoTodo)) |> ignore
    webApp.Map("", Func<Todo>(getTodo)) |> ignore

results in (decompiled):

public static void oneMoreTest(WebApplication webApp)
    {
      RouteHandlerBuilder routeHandlerBuilder = webApp.Map("", (Delegate) new Func<WebApplicationTests.Todo, WebApplicationTests.Todo>(WebApplicationTests.oneMoreTest@49.Invoke));
      routeHandlerBuilder = webApp.Map("", (Delegate) new Func<WebApplicationTests.Todo>(WebApplicationTests.oneMoreTest@50-1.Invoke));
    }

[CompilationMapping(SourceConstructFlags.Closure)]
    [Serializable]
    [SpecialName]
    [StructLayout(LayoutKind.Auto, CharSet = CharSet.Auto)]
    internal static class oneMoreTest@49
    {
      internal static WebApplicationTests.Todo Invoke(WebApplicationTests.Todo delegateArg0) => delegateArg0;
    }

    [CompilationMapping(SourceConstructFlags.Closure)]
    [Serializable]
    [SpecialName]
    [StructLayout(LayoutKind.Auto, CharSet = CharSet.Auto)]
    internal static class oneMoreTest@50-1
    {
      internal static WebApplicationTests.Todo Invoke() => new WebApplicationTests.Todo(0, "Play more!", false);
    }

As you see Func's are created from syntesized type which does not even propagate the attribute. Is is simply lost. Parameter names are lost too.

jkone27 commented 9 months ago

This is going to be in F# 9?

vzarytovskii commented 9 months ago

This is going to be in F# 9?

This hasn't been approved, so not likely.