André Slupik
Posted on March 3, 2022
What's wrong with this code?
// C#:
int Sum(IEnumerable<int> items)
{
int total = 0;
foreach(var item in items)
{
total += item;
}
return total;
}
// F#
let sum (items: seq<int>) =
let mutable total = 0
for item in items do
total <- total + item
total
If you said, "calling this function may hang forever", you'd be right. Here's a valid value for IEnumerable<int>
(seq<int>
in F#) that we could pass into our Sum
function:
// C#
IEnumerable<int> GenerateValues()
{
while (true)
{
yield return 1;
}
}
// F#
let generateValues () =
Seq.initInfinite id
Now, Sum(GenerateValues())
hangs forever.
What's wrong with this code?
// C#
interface IConfigValuesProvider
{
IEnumerable<ConfigValue> ConfigValues { get; }
}
var values = myConfigValueProvider.ConfigValues;
if (values.Count() > 0)
{
foreach(var v in values) { ... }
}
// F#
type IConfigValuesProvider =
abstract ConfigValues : seq<ConfigValues>
let values = myConfigValueProvider.ConfigValues
if Seq.length values > 0 then
for v in values do
...
Well, nothing, probably, but you can't really be sure. Since ConfigValues
is an IEnumerable<T>
, it could technically be infinite, causing Count
/Seq.length
to hang, although, granted, that's unlikely. What sounds more plausible is that ConfigValues
is implemented as the output of calls to Where
or Select
, and that iterating it is computationally expensive. Note that we're potentially causing it to be iterated twice here (calling Count
and then doing a foreach
), which would be bad if it was computationally expensive. Of course, the caller expects this to behave like an Array-like collection, but as a matter of fact, IEnumerable<T>
makes no such promises. A potential solution here would be calling ConfigValue.ToArray()
and using the result of that, but that's going to create a pointless copy if ConfigValues
is actually just a collection. We can't know, so either we make unsafe assumptions, or we code defensively and waste resources.
IEnumerable<T>
/seq<T>
does not represent a collection of items, so it should not be used to represent collections of items.
So what should we use? Is there another interface that all collections implement and provide a guarantee that it's actually a collection? Yes, there is, and it's called IReadOnlyCollection<T>
, introduced in .NET 4.5, August 15th, 2012. If you want to be more specific and get true array-like behavior (with indexing support), IReadOnlyList<T>
has got your back. I'm leaning towards IReadOnlyList<T>
for everything that has an index (arrays, List<T>
, ImmutableArray<T>
, etc.) and IReadOnlyCollection<T>
for everything else (LinkedList<T>
, Queue<T>
, Stack<T>
, etc.) There might be cases where concrete immutable collections like ImmutableArray<T>
are better though, especially as a return type. Returning a concrete type seems to provide greater value. I haven't found great, clear guidance on e.g. IReadOnlyList<T>
vs ImmutableArray<T>
; if you have an opinion on that, please share!
Finally, if IEnumerable<T>
does not represent collections, and we have a perfect replacement for it, what use does it have?
IEnumerable<T>
is anything that supports enumeration: collections, infinite sequences, computations of successive values. It is the perfect type to define mappings over arbitrary, non-finite sequences of data. Think of LINQ operators Where
, Select
, which also return IEnumerable<T>
: those make perfect sense and could not use a better type. Do not think of scalar-returning functions like Enumerable.Sum
or Enumerable.Count
which IMO shouldn't exist, as they don't make sense (and won't work) over anything but finite collections.
I suspect that the late addition of IReadOnlyCollection<T>
and friends to .NET is largely to blame for the widespread use of IEnumerable<T>
as a read-only view of a collection. After all, IEnumerable<T>
arrived in .NET 2, early 2006, and System.Linq
arrived in .NET 3.5, late 2007. F#'s Seq
module, probably designed around the same time, is full of the same issues as System.Linq
: Seq.sum
, Seq.length
, Seq.append
, Seq.toList
, Seq.toArray
, etc. are all very practical, but definitely not sound.
Posted on March 3, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.