117 comments

[ 3.6 ms ] story [ 201 ms ] thread
This is my most favorite feature in C#. It is the right combination of elegance and utility.
I'll just say if you enjoy using this stuff in C# (and linq) you really owe it to yourself to at least look at F#.

I'm biased as hell because I think it's a wonderful language, but even if you decide it's not for you, getting the basics down helped me better understand how to use such tools across languages.

If you had to start over, would you still recommend to yourself to learn F# in 2022?

I am asking this as someone who has dabbled in functional languages (F#/ Haskell) multiple times over the years but, despite liking the elegance of functional code, never reached a level of productivity that came close to what I am used to from imperative languages. Specifically for C#, given the trend of baking more and more of the cool functional stuff into the language (e.g. [1]), I am wondering whether F# still has enough unique features to justify the effort.

[1] see e.g. https://devblogs.microsoft.com/dotnet/early-peek-at-csharp-1...

As a former F# full-time dev, I'd say that there's a few things that really make it worth to use F# (and I will still use it instead of C#, where there's a possibility, in the future):

- Computation expressions make it possible to do ergonomically use value based error and nil handling, with option and result (very awkward to do in C#).

- type providers are a way to generate types at compile time based on some input. They let you do amazing stuff, like compile-time checking SQL against a specific database and automatically generating the correct types for inputs and outputs, or automatically generating types for stuff like XML or JSON files, based on a sample.

- really good type inference and (in my opinion) a better type system than C#'s, mostly wrt generics. You reap all the benefits of static typing, while also typing very very few types, and you get to express some things that just aren't unfeasible to express safely in C#. For example, the compiler will infer things like:

    let add a b = a + b * b
    //has type
    ^a -> ^c -> ^d
    when (^a or ^b) : (static member ( + ) : ^a * ^b -> ^d) and
    (^a or ^c) : (static member ( * ) : ^a * ^c -> ^b)
    //which means: a function from 'a' and 'c' to 'd', where either 'a' or 'b' has an add operator, and either 'a' or 'c' has a multiplication operator
you could also explicitly type it out as a type signature, by the way.

there's probably more, but these are the things that came to my head. DISCLAIMER: I usually reach for F# anyway, as I feel a lot more comfortable programming functionally compared to imperatively. F# is the only real option for actual FP on the CLR - so I'm a bit biased (and for much of the same reason, I would rather use Scala compared to Java on the JVM, for example). My actual favourite programming language to use is Haskell, so take that as you will :)

Shouldn’t the type constraint on multiplication be existence of an operator:

    (*) : ^c * ^c -> ^b ?
F# will always assume that the types can be heterogeneous, there's nothing in F# that gives (*) or (+) any sort of special behaviour, so it doesn't conform to an interface. Operators will usually always have an (inline) type of form

    ^x * ^y -> ^z 
because you could technically define a

    static member (*) (a: int) (b: float): double = ... 
and there's nothing saying that you can't. in that case:

   (*) : ^a * ^c -> ^b //^a = int, ^c = float, ^b = double
   (*) : ^c * ^c -> ^b //^c = int, ^c = float, ^b = double. 
and the second one won't work, because ^c =/= ^c
I see, but in this case the multiplication is “b * b”, which has type “^c * ^c”.
My experience says that no, unless you're in a team of F#-minded people. There are some companies like that but it's quite rare.

And the main challenge is that C# is good enough and nobody wants to switch over. As soon as F# evangelist leaves the team, all F# code slowly becomes obsolete and at some time rewritten in C#.

> If you had to start over, would you still recommend to yourself to learn F# in 2022?

Learn it, absolutely. A lot of comments here are arguing "maybe don't use it in Production", but I think it's a clear case that it is still worth learning even if you aren't "using".

Even though C# keeps adding a lot of F#-inspired features, there's still (and likely will always be) a large gap in the "native" idioms and the cultural best practices. There's still some big differences in encountering the "F#-like" stuff in the imperative wilds of C# than in the tame functional fields of F#. Learning those same tools in the "safer environment" where they feel more natural, learning the idioms that make them truly powerful in F# unlocks ways to think about these tools, that even if you aren't using them day to day in F# and are just applying them to C#'s relatives can still be very useful ways to think.

(A lot of what this article here talks about has a different perspective from F# and especially Haskell where "Lazy evaluation" is a much, more natural way of working than the imperative mindset of "every line does a thing immediately", which is what imperative means.)

Coincidentally, I've made several recommendations in recent PRs that developers take some time in F# lately. I really do strongly believe that learning it makes for better C# programmers in 2022, especially because of all the F# tools now in the language. It's too easy to fall into mental traps you don't even know might exist in LINQ and records and async/await and AsyncEnumerable and ReactiveX and so forth, with the list still growing, without the education of languages like F#. Even if the other comments can give you a lot of reasons why you maybe shouldn't push to use F# in Production, it's still an incredibly useful educational tool at least.

> If you had to start over, would you still recommend to yourself to learn F# in 2022?

Not GP, but I'd do it even for just the interactive shell on the .net ecosystem.

Absolutely. I'd probably recommend it earlier, but i also have a somewhat unique work environment. It's a very small team and we have somewhat nonstandard goals. I can't comment on how it'd work on a 50 man team or whatever on a larger project.

That said, I think F#'s beauty comes from how much easier it is to model your code, find errors in compile, and easily restructure once you really get the hang of doing proper design and passing functions. The few runtime errors that occur are pretty trivial to debug as well thanks to immutable by default.

It DOES require a major shift in how you approach your problems if you've been coding with something like C# for a long time, and that can take awhile. The tutorials are not great (often assume you're coming from something else), and the unfortunate mix of library support with awkward issues (you can use specter console, but if you follow the docs you'll hate your life) is brutal on adoption.

I do sincerely believe it's basically this diamond being totally ignored though because "well i'll just use C#" is a fair thing to say when at the end of the day we've all got shit to get done and the library docs are for that.

Learning F# though has made me a vastly better coder and I think it's EXTREMELY elegant in how it enforces good practices and lays out its code. It doesn't have all the advantages of something like haskell which is a shame (Higher kinded types and generic lens syntax for records would be AMAZING), but it also doesn't force you to learn monads to print to the screen.

Being able to jump back and forth between styles as needed is VERY powerful, and you don't have to worry about having sideeffects in your first mockup. And once you REALLY get it, having super light and easy syntax for passing functions around lets you do some CRAZY abstraction on your program logic that makes down the line adjustments/additions trivial and intuitive.

This comes from someone who spent nearly a year being TERRIBLE at the language. In part because the onboarding was even worse when i started (worse tutorials, lots of stuff referencing no longer working framework libraries, weird VS Code bugs that made things harder than it should be, me just not being very good at this stuff).

I think that the biggest shame is that F# almost never makes libraries because "Oh there's a dotnet one already", and while you TECHNICALLY can just use it, you quickly can wind up missing out on the neat features that brought you to F#, until you really learn how to easily swap between styles so you can invoke whatever features you want, and then get them back in idiomatic F# and start shoving around your functions.

Having spent a bit of time doing projects in F# I had to repeatedly go to C# language docs to understand what was going on when interacting with existing libraries.

So in my experience I'd say it makes no sense to start with F# if you're new to the .NET ecosystem.

Possible exception being if you are familiar with OCaml.
That's the opposite of what I mean. I have programmed a bunch in Standard ML and OCaml but the biggest missing chunk trying to learn F# was how C# works. It's unavoidable.
Every time I give F# a chance in production I get burned. A couple of weeks ago I needed to write some data sync code between some spreadsheets, asset folders and a database. A perfect scenario for a simple F# script - I'll use type providers and have the entire thing type safe and done in one file. Turns out type providers are the most brittle shit feature ever and getting it working in a CI/CD build without access to the resources is a major PITA. After wasting a day mucking around with useless tech I just wrote a C# CLI app next morning and moved on with my life.

Other scenarios I've seen it fail :

- enthusiastic team members try to push it, leave the company and nobody wants to touch that anymore

- tooling support being garbage - worked on a project where you had to disable F# projects just to get intellisense working

- second class citizen everywhere - using rider right now and C# experience is > VS (IMO), meanwhile while working on this script found one or two "not supported yet" scenarios and code analysis died randomly through using it (probably type providers)

At the end of the day C# added records, switch expression gets me pretty far, global usings and file scoped namespaces - it cut down on boilerplate and noise, it gives me most of the F# benefits while being well supported and widespread.

I also tried to get into F# and was turned off by really confusing type provider issues that I dont believe I ever figured out. I'll have to give it another go, but it wasn't a great first impression and C# is in a pretty good spot overall right now.
I'll say what I said to the other comment. Skip type providers. They're a neat concept but much like inheritance their use case winds up being massively more narrow that you'd first think.

The main draws are functions as a first class citizen (with a light syntax for that), low boiler plate, type inference with strong typing, and immutable by default. Oh and I also like the top down coding structure.

The amount of runtime errors i have in F# is minimal (generally because you're connecting to something wrong), because so much is caught in the compiler. Debugging is usually cake, and you're not afraid to really abstract out your code and start passing around functions because the syntax is light and easy.

> using rider right now and C# experience is > VS (IMO), meanwhile while working on this script found one or two "not supported yet" scenarios and code analysis died randomly through using it (probably type providers)

Jetbrains is the new company practicing “embrace, extend, extinguish”. They royally screwed up the implementation of YARD macros in Rubymine. Their F# IDE launch was a technical failure as well.

Never had any of those problems with Visual Studio.

I'll say this, I think type providers are somewhat of a trap. Not quite as much of an oops as inheritance, but their use case isn't quite what it gets sold as.

The best use i've seen of them is for quick, run once to get something, scripts where you're briefly connecting to a complicated dataset and want to rip some data out and do something with it.

As a long standing production tool.....i feel like you wind up running into enough issues with trying to adapt it to data changes that you're just better off not using it in the first place.

As for team members pushing it and leaving...well that's an issue with just about any language adoption, good or bad. We'd all still be on C++ if some people hadn't bothered to learn new toys.

Tooling...i dunno. I've never had something that severe but it's second classness shows. I use vs code/vs and its MOSTLY great, but there's some WEIRD shit that can happen for sure,

There's no doubt C# is more supported than F# and it's a shame because that'll always remain the case, but F# can work on top of C# very well (not so much the other way).

I really do see type providers as almost near inheritance though in the "oh this is cool until you use it and realize it's not". The main draws for me are that it's super easy to set up errorless models with types/records/etc and it's very low boilerplate while still actually having strong typing and immutable by default.

I did not know C# had this. That’s cool. I admit I see only a little of the C# that others at our company do.

But I have used the similar feature in Python for years. And I love it.

Is it just me, or are a lot of these languages starting to all feel a lot like each other?

This is indeed an amazing feature, and good languages copy ideas from each other. I believe C# has had this for years. C++ just got it with C++20 coroutines, though it still takes some work to use since std::generator didn't make it into C++20.

Lest folks think this is a new feature, it was invented by Barbara Liskov in her CLU language, as explained in this 1977 paper: https://web.eecs.umich.edu/~weimerw/2011-6610/reading/liskov... (PDF, see section 4.2.)

Cross-pollination of ideas is a good thing.

The only problem, what if your language adopts an idea which prevents you from having a better API?

As far as I know, coroutine/generator doesn't prevent you from doing anything.

I enjoy learning enough of novel (to me) languages so that I can do some fun stuff.

Usually when I look at a language One of the first things I do is look for familiarity: How to map, filter and reduce - and how to work with collections and lazy enumerables, whatever they may be called - IEnumerable, generator, Stream, etc.

It’s amazing how similar many popular languages are these days, at least in terms of developer experience. This doesn’t mean they are trivially interchangeable of course. You can’t write Elixir in an idiomatic C# style and you shouldn’t try to!

In case of C# and Python specifically, there's been quite a bit of cross-pollination. And now designers of both languages work at the same company...
This incredibly powerful facility is also available in C++20 with coroutines. 'yield return' is written as 'co_yield'. Also since "iterator" means something else in C++, this special usage of coroutines is called a "generator".

'co_yield' makes iterators/generators super easy to write as shown in this article. But also it enables great abstraction/encapsulation for where you want to provide an interface allowing callers to iterate over elements, without exposing the underlying container type.

(Note that 'std::generator' is not yet officially standardized, but with a little work we're using Microsoft's 'std::experimental::generator' across platforms, compiling with clang and gcc. Alternatively, I hear the cppcoro library is pretty great.)

Facebook already uses coroutines pretty heavily in certain C++ services. Was kind of surprising to see, I haven't seen coroutines used much in C++ because of the lack of standard library support (no composeable promises yet)
Very interesting, would love a follow up deep dive into the IAsyncEnumerable and await foreach() features!
As I was reading it I thought … okay next it’ll go into IAsyncEnumerable and could feel my self start to sweat. Then it was over!
IEnumerable is to C#/.NET, what std::begin() and std::end() are to C++, and LINQ is to C#/.NET what <algorithm> is to C++. Both interfaces and libraries respectively are the bread and butter of both languages.

I love both languages, and sincerely hope that C# is able to compile to native at some point in the future; there are already some efforts to compile it to WebAssembly with LLVM [0].

[0]: https://github.com/dotnet/runtimelab/tree/feature/NativeAOT-...

AOT is coming with NET 7 which will be released in November.

https://visualstudiomagazine.com/articles/2022/04/15/net-7-p...

When I read the advantages, all but restricted platforms sound like things that would help with server less (aws lambda, azure functions, etc) where we are doing seemingly stupid things like pinging a service once a while to keep it "warm". However, they don't mention this at all.

Is asp.net not just another console app?

Microsoft also said it was coming in NET 6, NET 5, Dotnet Core 3...

Hopefully they actually mean it this time!

We're already using it to ship a large shared .NET codebase in a native Mac(SwiftUI) application. It's there and it works!
Really interesting. Any references to what you are doing / how you are doing it?
Nothing to link to yet, sorry, but we really just followed the "standard procedure" - it basically involved writing a C API to our business logic, which wasn't all too bad for us as the actual API surface we need isn't huge. You could of course circumvent that and use another IPC mechanism based on your preferences.

In the end, we get a dylib that we can call like any other. A big pain point is debugging the generated native code, which isn't impossible but best to be avoided. I'd recommend having a good logging infrastructure in place and building abstractions that let you debug and test your .NET code without the UI layer.

C# has been able to compile to native since forever.

NGEN has been part of .NET since day one, and it always used a JIT, the only version of .NET that has ever done interpretation was the compact framework and micronet.

In regards to NGEN as AOT solution, there was mono AOT, CosmOS, IL2CPP, Windows 8 MDIL (Based on Singularity Bartok), .NET Native (Based on Midori efforts, initially known as Project N).

And now Native AOT, coming on .NET 7.

> [I] hope that C# is able to compile to native at some point in the future

Isn't that what Unity is doing?

begin/end is so C++. You get the parts, please use them responsibly and if you don't connect the ends together correctly you get random failures or a crash :)

With a 1990s lens it's a revolution, now, not so much.

They use "ToArray()" in several places, "ToList()" is perhaps more common.

This operation is technically called "to reify" the enumeration", or "Reification"

From the dictionary:

> reify: make (something abstract) more concrete or real.

https://dictionary.cambridge.org/dictionary/english/reify

ToList shouldn't be used as much as it is, most of the time ToArray is good enough, and it doesn't allocate a List on the heap
Just to add to this, they're essentially the same with ToList beating out ToArray at certain sizes due to how the internal array of a list is resized vs a raw array (true for .net core). A better rule of thumb is deciding if the result needs to added to/modified etc then picking accordingly.
I've written several rants about "ToList considered harmful". I think it is unfortunately too easy to (ab)use and often ToArray is better overall for the use cases of a reifying a query only to simply iterate it later (more than once; otherwise leave things lazy).

That rule of thumb "are there calls to Add after this query" is the biggest one where "you might need List<T>" (and even then maybe you are taking a too imperative approach to something that could be reified differently).

Though often I find what projects really need are ToDictionary or ToLookup, especially ToLookup, a lot of C# developers sleep on that, because it doesn't have an Add and doesn't implement ICollection<T> and you have to search NuGet for a mutable ILookup<T> implementation that you can just `new` yourself, but ILookup<T> is often the best shape of reified results that have multiple iterations of subsets. Take your O(n^2) operations and make them more O(n log n) or whatever if you pick the right key(s) for your subsets.

It's not uncommon for me to start a performance optimization pass on a C# app simply by doing a Find All for ToList and removing every single use.

> ToLookup, especially ToLookup, a lot of C# developers sleep on that ... and you have to search NuGet for a mutable ILookup<T> implementation

Interesting. I have recently replaced a List<int> with Hashset<int> This is a (IMHO, underused) part of the standard library https://docs.microsoft.com/en-us/dotnet/api/system.collectio...

C# Devs tend to know List<T>, Dictionary<K, V> but not HashSet<T>.

The reason for that replacement was simply: "we have a lot of 'business category ids' in this opt-in list, all we do is ask 'is the current id in the opt-in list or not?' so duplicates have no meaning, and ordering is possible but of secondary importance ... that's mathematically speaking, a 'Set' not a 'List'. "

So using Hashset reduces the check time from O(n) to O(1) and otherwise it's pretty much a drop-in replacement: Same initialiser, same 'Add' and 'Contains' methods, also found in 'System.Collections.Generic'.

I'm looking at ILookup<K, V> and it looks fairly similar to Dictionary<K, IEnumerable<V>> When would I go out of my way to use it instead?

Yeah, HashSet<T> is definitely another one to prefer over ToList often enough. There is a ToHashSet finally in Enumerable extensions now so maybe more developers will discover it now. (It was finally added somewhere around .NET Core 2, as I recall.) Also, a quick shoutout to System.Collections.Generic.Immutable for having great tools like ImmutableHashSet<T> and ToImmutableHashSet for when mutability distracts from your algorithm/data structure needs.

ILookup<K, V> is basically the "in memory join and group by" tool. If a query sort of naturally ends in a GroupBy because you want things sorted/grouped by an enum field or a foreign key field, but you still want all of the items in the groupings rather than just aggregate stats on the groupings, that's often where you want ToLookup instead of GroupBy to reify your query.

In general, Enumerable.Join and other in memory "join" operations (as opposed to the IQueryable's Join which more often than not gets sent to a highly tuned query planner in an SQL server somewhere) are often O(n^2) even at best. A lot of developers do notice this, that joins are expensive when performed in memory, but simply rewrite them by hand as nested foreach loops which still has the same O(n^2) worst cases, but it's hand-written so developers feel more comfortable with how slow it is. On the other side, in C# a lot of developers tend to naturally write these kinds of "in memory join operations" as nested foreach loops anyway (or a foreach loop with a big Enumerable.Where query inside acting as a second foreach loop on the other data source), just because they've always done it that way, and might not even think of them as "an in memory join" much less think to replace hand-written loops with something more LINQ-like anyway. ToLookup is generally the handiest tool to replace both sorts of O(n^2) "joins" the Enumerable.Join operations that do their best but don't know your model domain and don't have the query planner and indexes of an SQL database to fallback on, and the hand-written nested loops: you "index" the "right side" of your join (the inner foreach/if or Enumerable.Where in hand-written loops) on your join key with ToLookup.

As an example, perhaps: you've got a list of items in a shopping cart, which is only in memory in session data (so "naturally reified"), and a table of coupon/discount "rules" that apply primarily based on "item category ID" (with a Category FK table in between Item and CouponRule in your normalized DB structure). The most naive way of writing that would be something like:

    foreach (var item in shoppingCart) {
      var couponRules = db.CouponRules.AsQueryable()
        .Where(rule => rule.CategoryId == item.CategoryId);

      foreach (var rule in couponRules) {
        if (RulesService.DoesRuleApply(item, couponRule)) {
          RulesService.Apply(item, couponRule);
        }
      }
    }
Written that naively that's at least one database query for every item in the shopping cart, which is obviously not optimal so a developer's first instinct is going to be to reify that query somewhere. The simplest, equally naive reification is often this (though often by this point "hidden" as a cache inside "RulesService"):

    var allCouponRules = await db.CouponRules.AsQueryable()
      .ToListAsync();
    foreach (var item in shoppingCart) {
      var couponRules = allCouponRules
        .Where(rule => rule.CategoryId == item.CategoryId);

      foreach (var rule in couponRules) {
        if (RulesService.DoesRuleApply(item, couponRule)) {
          RulesService.Apply(item, couponRule);
        }
      }
    }
(Again, it's maybe more obvious written this way, but often at this point what I find is that allCouponRules cache moves inside RulesService and that Where and foreach get "hidden" insid...
Both types `List<T>`and `T[]` are `object` aka Rust's `Box<T>`. There's little profit most of the time for using `ToArray()` over `ToList()`, and in places where it does matter, there is a high chance that using `IEnumerable<T>` is bad idea due to its performance overhead.

I believe, the best implementation of `IEnumerable<T>` we have today are Rust's iterator types which provide the same functionality but with performance comparable or even better than manually written imperative loops.

That's not entirely true.

.NET's object is very different from Box<T> from my understanding. My rust understanding is rusty (not sorry), but .NET's object is much closer to &mut than Box<T> as it is the top type in the type system. There's the reference type/value type split "below" object. Reference types like `List<T>` are always heap allocated (like Box<T>), but value types like T[] might not be and may be stack allocated.

Small enough arrays can reify entirely in stack space (function locals) rather than heap space and with some uses of ToArray() the JIT compiler is smart enough to make the right call, whereas List<T> always creates heap allocations and ToList() unavoidably churns at least Gen 0 of the garbage collector that tiny bit more.

IEnumerable<T> doesn't give you enough information of the implementation details to know if that interface is implemented as a reference type or value type. Some implementations of IEnumerable<T> are non-heap allocated value types (structs)! Often many implementations in performance critical areas of the framework are structs.

Unfortunately, while I would love JIT to do such kind of optimizations, this is completely not how it works today.

First and foremost, '&mut' in Rust is a mutable reference. The C# alternative is just 'ref'. Both point to specific addresses. 'Box<T>' OTOH is a handle for a struct allocated on a heap, just like C#'s one is implementation-wise (except in C# they are further managed by runtime but that's besides the point).

Now regarding the arrays - 'T[]' is strictly object withouts ifs and buts. There is no "short array" optimisation that would substitute array type with stack-allocated inline buffer. If you want this, you have to write it manually. See: https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6b...

The last but not least, if the method signature accepts an argument by its interface type e.g. 'HandleEnumerable<T>(IEnumerable<T> values)', then the IEnumerable-implementing struct will be boxed before method can be called. C#/.NET doesn't do full generic monomorphization like Rust. Instead, for class-type generics a common '_Canon' method body will be JITted which will also implicitly accept generic-type arguments to correctly dispatch on an actual <T> of the method. OTOH, for structs, all method calls will be fully monomorphized per each struct type. This means that you can avoid struct boxing when dispatching by interface by rewriting method signature to 'HandleEnumerable<T, TEnumerable>(TEnumerable values) where TEnumerable : IEnumerable<T>'. In this case, unless you cast your struct to an interface type explicitly beforehand, the struct will not be boxed.

For the sake of simplicity, this explanation scope doesn't include advanced optimisations done by Tier-1 JIT compilation such as guarded de-virtualization, more aggressive inlining that can potentially elide boxing, etc.

Again, I still believe you are confusing lower-case CLR `object` (top-type) and upper-case BCL `System.Object` (root-type of "reference types"/"heap types" and below the top-type; they are confusingly named to allow for some abstractions where considering them the same thing makes it easier to reason with, but they are different concerns). Value types in stack space (function locals/small scoped variables) that fit entirely in stack space (size restrictions; "short arrays") never get "boxed" to the heap. `stackalloc` is an important need to restrict such types to the stack, because things like Span<T> require stack types and stack types only, but the general JIT rules apply that value types that don't leave stack scopes often are never "boxed" to the heap, including "short arrays" in function local scopes. (With the complicating caveat that things like async/await change what "function local" means and will increase boxing as things move to async state closures.)
You are free to believe this but if you want me to believe you, please provide official spec or code that says it works that way today. Because making your statements sound reasonable doesn't magically make them true - the truth is far removed from your statements, sorry for putting it bluntly.

JIT never did proper escape analysis for object stack allocation except for the small proposal prototype implementation which the runtime team decided to put away for better times: https://github.com/dotnet/runtime/issues/11192 It is never enabled and will not work the way you expect it to even if you try.

Huh, now that I think about it, this account might be posting messages generated by GPT-3 and I'm just wasting time.

The fundamentals of Value Types versus Reference Types: https://docs.microsoft.com/en-us/dotnet/visual-basic/program...

What you linked is an issue regarding storing Reference Types (classes) in stack space. Again, there is a difference in .NET between a Reference Type and Value Type, and this is a fundamental dichotomy going back to the earliest days of .NET.

I don't appreciate the ad hominem attack and suggest that you google any further follow ups yourself.

C# arrays are 'object', aka "Reference Type". The only reason I'm responding the way I do is because GPT-3 is too known for writing elaborate texts despite consistently getting fundamentals wrong. Your previous reply directly contradicts runtime implementation so I see no reason to apologize for my statements.

In addition, regarding Span<T> and other ref structs, they can hold any type and have no restriction to T : unmanaged. That's because objects by their nature of being on the heap are guaranteed to outlive the lifetime of any ref struct and the runtime supports tracking GC references held like this anyway.

In the early 2010s while learning C# and porting a Delphi app to Windows Forms, I learnt about this and many other C# features and patterns thanks to a ReSharper free trial which I used heavily for all of those 30 days. If it's as good now as it was over 10 years ago, I highly recommend it for every C# developer who uses Visual Studio.
Whenever new C# language features are announced people outside the .Net world criticize the language for becoming too bloated.

They forget that 99% of C# developers use full fledged IDEs that gradually teach you new language features by suggesting useful refactorings.

The only important C# specific language features that takes effort to understand is LINQ that came out in 2007 and async/await that came out in 2012.

Any C# programmer who has been in coma since 2012 can get up and running with the more recent language features in just a few hours.

Surprised to see a 2022 post about this without mentioning IAsyncEnumerable which is the companion to IEnumerable for async and allows async lazy eval of *each iteration*.

We’re using it extensively and coupled with the syntactic sugar that enables its consumption in foreach, it makes the code look as elegant as a traditional synchronous enumeration.

You should read the article, not just the title:

"And to muddy the waters just a little, not all iterators are synchronous; there’s also an IAsyncEnumerable interface (you can loop through it with await foreach)."

Duh, I had but missed that!
I read the article but missed it as well.

By the way, its against the guidelins to tell people to read the article ...

Even though it is mentioned in the article, it deserves more than just mention. I find the Async LINQ extensions somewhat lacking. I need to have IAsyncEnumerable all the way down: returning an array in `SelectMany` is not possible. Sometimes you mix and match with synchronous code and the `ToAsyncEnumerable()` isn't elegant.
Have you tried importing the System.Interactive.Async package? [0] It's sometimes referred to as "Ix" (Interactive Extensions) to relate it to its older brother System.Reactive (ReactiveX or Rx). It adds a ton of LINQ-style extensions that make flowing between async and sync code easier. Despite the "System" name you always have to install it from NuGet and it's not just available out of the box, but it lights up so many LINQ extensions when installed and paired with a `using System.Linq.Async;`.

Though I also think IAsyncEnumerable is even better when paired with Rx, too, as some things are easier to express as Observable "in the middle" as an AsyncEnumerable->Observable->AsyncEnumerable "sandwich". (You can sort of think of it as an pullable Event Source being spread into a push-based message bus/pipeline and then being collected back into a pullable Result feed.)

[0] https://www.nuget.org/packages/System.Interactive.Async/

It's worth mentioning that JavaScript has had this feature for a while now, most likely inspired by C#.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

I don't know the timeline but it's interesting how c#, js, python, php and maybe others got generators around the same period.
> I don't know the timeline but it's interesting how c#, js, python, php and maybe others got generators around the same period.

AFAICT, not really all that close in time:

Python 2.2 (2001)

.NET 1.1 (2003)

PHP (2012-ish, based on RFC date)

JS ES2015 (2015) [though nonstandardized impls in some engines prior]

Pretty sure IEnumerable was part of the .NET 2.0 release in 2005.

https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csh...

I don't see how you get to that conclusion. These release notes mention enumerators:

> C# version 2.0 brought iterators. To put it succinctly, iterators let you examine all the items in a List (or other Enumerable types) with a foreach loop. Having iterators as a first-class part of the language dramatically enhanced readability of the language and people's ability to reason about the code.

The implicit assumption in that sentence is that enumerables existed before iterators.

For Ruby:

- Enumerator are documented as soon as 1.9.1[1] in 2009[2]

- Enumerable are already present in 1.8.6[3] released in 2006[4], and the matching source code is easily trackable on Github through enumerable.c[5] back to 2005. The Git repository won’t bring any meaningful information before that on this file, due to the import from SVN if I well understand. There is also an enum.c file, but I’m not versed enough in Ruby code base to tell what is it about.

[1] https://ruby-doc.org/core-1.9.1/Enumerator.html

[2] https://www.ruby-lang.org/en/news/2009/01/30/ruby-1-9-1-rele...

[3] https://ruby-doc.org/core-1.8.6/Enumerable.html

[4] https://www.ruby-lang.org/en/news/2007/03/12/ruby-1-8-6-rele...

[5] https://github.com/ruby/ruby/commits/master?after=78425d7e74...

> Enumerator are documented as soon as 1.9.1[1] in 2009[2]

And Generator is present in 1.8 (though I can't determine how early in the 1.8 series). Same basic function, slightly different API (and Generator used Continuations under the hood, while Enumerator uses Fibers, which improved performance and, in theory, implementability in alternative Rubies which didn't all support Continuation, which was one motivation for moving Continuation out of the core language in 1.9.)

I'm very confused, for some reason I read about python generators way later than 2001 (2010?). Maybe they upgraded some syntax and it made me read about it.
> I'm very confused, for some reason I read about python generators way later than 2001 (2010?). Maybe they upgraded some syntax and it made me read about it.

After generators were introduced themselves, Generator expressions were added in 2.4, coroutine support with yield as an expression in 2.5, “yield from” to delegate to a subgenerator in 3.3.

So, yeah, that's plausible.

It's interesting how direction of feature flow changes over time. In 2005, C# got iterators from Python; in 2015, Python got async/await from C#.
It lacks the strong standard lib support that c# has sadly, and the open source alternatives are a really mixed bag in terms of quality or complexity of usage.
What a coincidence, I am just going through the C# in Depth chapter about iterators and this pops up on the first page of HN :D.
That is such a great book. Read it a while back… I imagine the modern version is every bit as good. I don’t remember it ever getting boring, but if it does, push through! It’s worth it.

Jon Skeet also has a (now old) blog series called edu-linq that basically implements linq just using generator blocks and it really makes you think about stuff like validation of arguments vs actually doing the operation. It’s very interesting. A couple of caveats in there.

Unity elegantly abuses these to provide a coroutine support. Basically you can “schedule” an enumerator to be run on the game loop. Engine iterates one by one on the iterator each frame. Can use for animations, delays etc.

It looked weird first time but makes sense.

Unity (the game engine, not the desktop environment) uses this concept heavily, although they use the slightly older IEnumerator instead of IEnumerable ¹).

It usually takes people a while to wrap their heads around it and (also hinted at in the article) forgetting to add "yield return" in front of an IEnumerator is a common mistake, but it is an awesome tool to flatten out callback hell or complicated every-frame checks.

¹) https://docs.unity3d.com/Manual/Coroutines.html

Yeah, it's a nice way to kludge in yielding/cooperative execution without bringing in the threading syntax of async/await.

It's still a bit of a miss match though. Delay instructions and waiting for other coroutines are implemented as type unsafe yield return values. Still, a pretty good hack. It ends up with a lot less garbage than async/await.

> Delay instructions and waiting for other coroutines are implemented as type unsafe yield return values

Can you elaborate? I was under the impression that `YieldInstruction` is a well-defined type.

You would use the base IEnumerator that yields object types. `YieldInstruction`s are a type that the Unity coroutine runtime will recognize but you can yield anything. Some have meaning like YieldInstruction, Coroutine, CustomYieldInstruction and others that Unity will recognize as some special command to wait before the next iteration. As far as I know there is no exhaustive public list. You can also just yield anything else from int to stream.

Yielding a final value that isn't a yield instruction is a technique to return a value from a coroutine. You can pull the final return value from the IEnumerator's Current property. Hacks on hacks but it gets the job done.

Not really. Tasks can be zero garbage, IEnumerable cannot. A 'task' in C# is just something that has a GetAwaiter() method, which gives you a callback on when something has happened.

This is how ValueTask is implemented in newer .NET versions which is a struct, but you can roll your own.

Yes really. It's huge effort to make this happen. ValueTasks bring their own gotchas, like not being able to be awaited multiple times. Yielding from an existing IEnumerator is free while every await is a chance to cause garbage with a new awaitable. Even Task.Yield() allocates garbage (in Unity at least) and trying to get Unity lifecycle events as cheap and safe awaitables is not really feasible. At least at the moment.
They simulate concurrency with coroutines because due to design all code that uses the engine functions should run in main thread, so you can't use Tasks or Threads.
The threading thing is a common requirement for game engines, though. Unreal also assumes engine functions are called only on the game thread (unless they're specifically marked otherwise).
You can use async Tasks as long as you keep them on the main thread. They end up making a lot of garbage though.
That's what ValueTasks are there for, no?
ValueTasks help but they can only be awaited once. The Unity runtime knows how to handles the same IEnumerator yielded by multiple parent routines.

Cancellation is another gotcha with Tasks. The main way to cancel tasks is to use exceptions as control flow. This causes cancellation to be very expensive. You can still use cancellation tokens and check for IsCancelled and finish your task cooperatively but then you're kind of doing thinks in an non-canononical way.

Unity also hasn't done the work to make cheap unity lifecycle delay calls such as Task wait for end of frame, (although they could). The work arounds are expensive.

Is that Unity-specific stuff? From my experience, cancellation in idiomatic C# is mostly done via tokens, not exceptions.
Tasks can be constrained to the main thread - you just need to replace the TaskScheduler and/or the SynchronizationContext.
[...] although they use the slightly older IEnumerator instead of IEnumerable ¹).

They are not older or newer, they belong together. IEnumerable has only one method GetEnumerator() which returns an IEnumerator. If you have an IEnumerator, you can iterate over the sequence once [at a time], if you have an IEnumerable you can obtain as many IEnumerator as you need or want.

Yes, unfortunately due to coroutines having been designed before async/await was a thing.

But seeing how async/await is a thing since like a decade, this is another display of Unity doing things their own way for no good reason.

There's actually a lot of good reasons to use generators over async/await. It's not feasible to use only async/await in Unity. Besides the inherent garbage with the design of Tasks, the fact that threading isn't even a topic for generators makes the UX a lot easier for newer/novice devs.
FYI, IEnumerator is the thing that can be enumerated, and IEnumerable is the thing that can give you an IEnumerator when you call GetEnumerator() on it.

Both are supported as return types with yield return, in fact if you use IEnumerable, the generated iterator's implementation will be:

     IEnumerator GetEnumerator() => return this;
I started on a Unity Squad based RTS prototype in 2021 using generic state machines, but I eventually shelved that project due to art issues.

This year I began another RTS prototype, but this time using coroutines instead of state machines for doing procedural animation, AI controllers etc.

It doesn't take that long to learn and once you get how it works it makes development a lot nicer. Just a side note, when using these in Unity make sure to look at More Effective Coroutines (free/paid versions available) on the asset store to make sure you're not unnecessarily allocating memory.

Stack Overflow is a remarkable company. They bet on C# and ASP.NET MVC 1.0 in 2008 and they still host their own server infrastructure in their basement while being one of the 20 most visited sites in the world.
They also utilize just a few servers even if they have a massive traffic because they optimize the code. :)

A lazy company would use some dynamic language and tons of cloud VMs and pay 100x for the hosting.

Some businesses cannot afford to optimize the code because the requirements are kept changing/unclear. Any attempts to optimize will be obsolete in some years.

It's worse when the project is outsourced to vendors, since time used for optimization is a wasted cost for vendor, while business is unlikely to pay for optimization unless it's really severe.

(comment deleted)
Lazy evaluation is nice, but as a beginner, if you don't know if a particular IEnumerable or IQueryable is lazy evaluated or not, you are going to get burned.
One must assume you don't know whether it is (so it could be). If you want to realize it into a list, you can do that - but if you don't know anything about the enumerable, you might get burned anyway. If I make an IEnumerable<long> which generates all even numbers, then anyone doing ToList() on it is in for a surprise.
I found it surprising that when critizing Go, no one hardly talked about Go's lack of a common iterator interface like Java's Iterator (let alone a more powerful generator like C#'s IEnumerable). This was partly due to the lack of generics back then. Now that we have generics, hopefully we can have it in Go soon.
Oracle Database beat C# to language-level iterator support by four years with pipelined functions as part of the 9i release in 2001. Its syntax uses "pipe row" instead of "yield return".

In fact, you can connect a .NET app to an Oracle Database, invoke a PL/SQL pipelined function and, while that function is emitting results via pipe row inside a PL/SQL for loop, your C# wrapper streams the results to its called via yield return inside a C# for loop.

For for anyone wanted to try C# and a yield method,

https://try.dot.net/

I've had trouble running this in Firefox recently, but Chromium seems fine

Very powerful but also very error prone. I am almost thinking that it could use a strict/pure mode with more restrictions. Or maybe just compile warnings for some things.
> Every time we iterate over numbers, it will start over at the beginning of the iterator method and yield all the same values over again. (You can’t expect this from every iterator; more on that later.)

They never did, or did I miss it?

They did:

> An iterator could query a database, for example—including the unfortunate possibility that it might alter data, or that iterating through it twice might yield completely different results! Some tools (like ReSharper) will warn you against multiple enumeration for this reason.

Oh I saw that. I was expecting that they were gonna discuss stateful iterators more generally. Database iterators are just a tiny subset of stateful iterators. I think discussing the latter more generally would have been more useful than a one-liner about databases.
IEnumerable is fun. A while ago I wrote a lazy streaming no-accumulation Batch extension method using a bunch of features like yield return and manual enumeration that happens to be one of my more upvoted answers.

    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
https://stackoverflow.com/a/44505349/213246
One of my favorite ways to show off enumerables to someone is with an endless list. It really makes someone think when I show an IEnumerable instance that contains every prime number, for example [0].

Obviously you can never evaluate the whole thing, but because of the lazy evaluation you can get enumerators and pull out values all day long. Just don’t .ToList() the darn thing.

[0] https://gist.github.com/arlm/6647474#file-sieveoferatosthene...

But EFCore doesnt support async enumerables does it?
It's not very well documented, but EF Core has supported IAsyncEnumerable<T> since EF Core 3.0 or thereabouts. Just about any of its IQueryable<T> LINQ implementations are castable to IAsyncEnumerable<T>, easiest through .AsAsyncEnumerable() and generally just work when you do that.

Most people just use the .ToListAsync() and .ToArrayAsync() "materialization" tools which I believe use IAsyncEnumerable<T> under the hood, but you can use IAsyncEnumerable<T> directly if you want.

Generally though, EF Core's support for IAsyncEnumerable<T> is mostly known for the confusion it causes with "Ix" (Interactive Extensions; the System.Interactive.Async nuget package that adds a bunch of LINQ extensions for IAsyncEnumerable<T> to the `using System.Linq.Async;` namespace) because if you install that package and have such using statements there's overload conflicts between IQueryable<T> and IAsyncEnumerable<T> for many LINQ operators and you have to explicitly add .AsQueryable() to almost all of your EF Core LINQ to make sure to avoid those operator conflicts and force a cast to just IQueryable<T>, not both IQueryable<T> and IAsyncEnumerable<T>.