louthy / language-ext

C# functional language extensions - a base class library for functional programming
MIT License
6.43k stars 417 forks source link

How are we supposed to use code gen now that CodeGeneration.Roslyn.BuildTime is no longer maintained? #886

Closed MrYossu closed 3 years ago

MrYossu commented 3 years ago

I've been playing around with code gen, which is very powerful, but am not sure how we are supposed to use it. If you add the LanguageExt.CodeGen package to a project, you get a message telling you to use CodeGeneration.Roslyn.BuildTime, but if you look at the project page for that package, it says right at the top...

This project is no longer maintained now that Roslyn offers source generators...

I looked at the link to the announcement about source code generators, as well as the example migration, but to be honest, neither of them made much sense to me. I'm not interested in writing a source generator, I just want to be able to use this one.

What is the recommended way of setting up your project nowadays? I tried the (presumably outdated) way shown in LanguageExt's documentation, which still works, but I'm nervous about relying on a package that is not maintained, especially if Roslyn now supports this out of the box.

To compound this problem, I tried committing a project which uses CodeGeneration.Roslyn.BuildTime to a GitHub repository, but the build failed with the message...

C:\Users\runneradmin.nuget\packages\codegeneration.roslyn.buildtime\0.6.1\build\CodeGeneration.Roslyn.BuildTime.targets(73,5): error CGR1001: CodeGeneration.Roslyn.Tool (dotnet-codegen) is not available, code generation won't run. Please check https://github.com/AArnott/CodeGeneration.Roslyn for usage instructions.

Given that the code builds locally, and includes references to the required packages, why won't it build on GitHub?

Some guidance would be greatly appreciated. Thanks

erwinkn commented 3 years ago

The move to source generators is planned: see #875.

That issue also explains an implicit requirement of the dotnet codegen tool: the host environment needs to have the .NET 2.1 SDK installed. See this comment.

Otherwise, the code generation itself seems to work fine, even with the latest .NET 6 Preview 5. A major problem I encountered while testing is that it seems to crash the OmniSharp server on VS Code. This is likely due to it not having the .NET 2.1 SDK available. There is probably a way to configure it to work, but I have not investigated it yet.

@MrYossu, from the other issue, I guess your main use of CodeGen is for union types? In that case, another package may help you out in the meantime: https://github.com/mcintyre321/OneOf It is less convenient (= more boilerplate) and provides overall less functionality than the union type from LanguageExt.CodeGen, but it enables exhaustive pattern matching.

MrYossu commented 3 years ago

@erwinkn Thanks for the reply, very helpful.

The comment you linked mentions needing SDK 2.1, but the comment directly after that says...

you should not need 2.1 sdk. try to define roll forward policy

...and links you to an issue on the package which says you have three options...

  • manually install runtime v2.1 (which you probably already have on your desktop, but is not available when you do setup-dotnet action with 3.1.100 sdk;
  • declare environment variable: DOTNET_ROLL_FORWARD=Major
  • wait for 0.7 which will Just Work™

The first is what you suggested, but the second looks much simpler. However, from what I can see from a pull request over there, this issue was fixed in Dec '19, so should Just Work™.

Given that it Doesn't Work™ (for me at least), I wonder how much I can rely on that.

Any thoughts? Thanks again.

MrYossu commented 3 years ago

@erwinkn Sorry, forgot to comment on your last bit.

My only use for CodeGen right now is a union, but that's because I've only just got going with it. Once I get more of a feel for it, I can see it being very useful for other things as well.

That other package does look nice, and if I didn't have LanguageExt, I guess it would be a no-brainer, but assuming I can get CodeGen working fine with LanguageExt, I'd prefer to stick with something that Does It All™ 😀.

As for exhaustive pattern matching, don't we get that with the switch expression? I get a compiler warning if it detects missing patterns. What else does OneOf offer?

Thanks again.

erwinkn commented 3 years ago

Looking at the NuGet page for CodeGeneration.Roslyn.BuildTime, it seems the 0.7 version, supposed to work with .NET 3, was never fully released, with only a 0.7.5-alpha available. Haven't tried it, but LanguageExt.CodeGen uses the 0.6.1 version for now, so it may not change anything.

The DOTNET_ROLL_FORWARD=Major flag doesn't seem to work for me either.

Regarding pattern matching: the problem with the union type from LanguageExt.CodeGen is that it requires a fallback case in the switch statement:

string Result => Union switch {
    CaseA => "Case A",
    CaseB b => "Case B: " + b,
    CaseC => "Case C",
    _ => "" // fallback
};

That means that if you forget a case, or if you add a new case CaseD, the compiler will not enforce that you handle it in the switch - it will just land in the fallback.

Personal example: I use a union type in a very simple type system, used to compile a domain-specific language (DSL), which my app exposes to users. As a result, that union type is used in many places in some of the trickiest parts of the code.

New types will be have to be added in the future by other developers. Exhaustive pattern matching makes it very easy for them: they add a type to the union and the compiler indicates all the places they have to update, ensuring no corner case is forgotten.

Aside from union types, the code generation part of LanguageExt is mostly useful for specific scenarios:

Hence my suggestion that, while we wait for an update to source generators, the OneOf package should cover most use cases for code generation from LanguageExt (at the cost of less convenience and more boilerplate) :+1:

MrYossu commented 3 years ago

@erwinkn Thanks for the reply.

You obviously read the CodeGeneration.Roslyn.BuildTime docs more carefully than I did! As it happens, I tried adding SDK v2.1 to my build steps, and it worked fine, so that is an easy option (no pun intended!) if I stick with LanguageExt.

I see what you mean about the switch, I do get a bit annoyed at needing to add a fallback branch, so I guess OneOf is a good option. I think I'm probably bias, having been bitten by the power of LanguageExt, I want to use it for everything, but you're right, for unions (which is all I'm likely to use code-gen for), OneOf does seem a better option.

Thanks again

louthy commented 3 years ago

@erwinkn Just for balance, because you've brushed over some significant losses in functionality by using OneOf:

I did consider this approach when thinking about how to implement unions in language-ext, but decided the downside wasn't worth it.

It is true that C#'s switch expression doesn't allow for completeness checking. But you'd be much better off writing your own Match extension method for a language-ext [Union] than losing the functionality listed above.

louthy commented 3 years ago

In v3.5.32-beta the union-code gen now generates a Match function. One that deals properly with generics, even if the cases have their own generic parameters.

MrYossu commented 3 years ago

Wow, this gets better and better! Any estimate for release of that version?

Thanks for all your hard work.

louthy commented 3 years ago

It's uploading to nuget now

louthy commented 3 years ago

Hold off, just found a bug :D

MrYossu commented 3 years ago

I'm hanging 😎

louthy commented 3 years ago

Fixed in v3.5.33-beta

One thing to note: unions nested inside other classes won't work now, as the Match extension method needs to be in a top-level class. I should be able to fix this for abstract class derived unions, but won't be able to fix it for interface derived unions (until I migrate to Source Generators).

MrYossu commented 3 years ago

@louthy When you've got this done, and out of beta, please can you provide some examples. I find that these make it much easier to understand.

Thanks again.

louthy commented 3 years ago

It works exactly the same way as Match on Option and all the other types.

erwinkn commented 3 years ago

Well, @louthy, you strike once again with your incredible involvement in this library.

Thanks for all the precisions regarding union types! I probably should have been clearer: OneOf is not a perfect solution, but it is a useful drop-in if you need union types on .NET 3/5/6. Some things like Blazor applications, especially ones shipping in WebAssembly, really benefit from the latest runtimes.

Setting up LanguageExt.CodeGen on those versions is possible, but I prefer to keep things simple for other engineers on the team. So OneOf is a good-enough hack (at least in my case) in the meantime.

Although, having used OneOf quite a bit now, I can confirm it requires a lot more boilerplate (I have ~40-60 lines of code per union type) and is less performant. For instance, I have ~40-60 lines of code per union type. The performance is not a limiting factor in my case, but could be problematic if you require union types in a critical path.

In any case, thank you so much for all your work @louthy, LanguageExt is an amazing library! It's hard imagining writing C# any other way now.

MrYossu commented 3 years ago

Thanks for all the precisions regarding union types! I probably should have been clearer: OneOf is not a perfect solution, but it is a useful drop-in if you need union types on .NET 3/5/6. Some things like Blazor applications, especially ones shipping in WebAssembly, really benefit from the latest runtimes.

Setting up LanguageExt.CodeGen on those versions is possible, but I prefer to keep things simple for other engineers on the team. So OneOf is a good-enough hack (at least in my case) in the meantime.

Well, I'm doing Blazor, and it was a piece of cake setting up code-gen. All I did was add the following to my project file...

  <ItemGroup>
    <PackageReference Include="LanguageExt.Core"
                      Version="3.4.15" />

    <PackageReference Include="LanguageExt.CodeGen"
                      Version="3.4.15"
                      PrivateAssets="all" />

    <PackageReference Include="CodeGeneration.Roslyn.BuildTime"
                      Version="0.6.1"
                      PrivateAssets="all" />

    <DotNetCliToolReference Include="dotnet-codegen"
                            Version="0.6.1" />

  </ItemGroup>

...and I was away. This was a one-off, so no-one else needed to get involved. I also needed to add .NET SDK 2.1 to the build steps, but again, that was a very quick and simple one-off, so I don't see any problem in using LanguageExt's code-gen 😀

In any case, thank you so much for all your work @louthy, LanguageExt is an amazing library! It's hard imagining writing C# any other way now.

That's an understatement!

MrYossu commented 3 years ago

It works exactly the same way as Match on Option and all the other types.

Ah, sorry, I thought there were some extra issues, but looking back, that was just a comment about nested unions.

Thanks again.

louthy commented 3 years ago

The nested unions issue is gone now too:

https://github.com/louthy/language-ext/releases/tag/v3.5.34-beta

MrYossu commented 3 years ago

@louthy Do you ever stop for breath? 😀

Any idea when this will be out of beta?

Thanks again.

louthy commented 3 years ago

Version 4 isn’t far away, just need to build a few more unit tests!

MrYossu commented 3 years ago

@louthy OK, I'll wait with bated breath!