How many tuple types are there in C#?

In a recent StackOverflow question the poster asked about the difference between tupled and curried form of a function in F#. In F#, you can use pattern matching to easily define a function that takes a tuple as an argument. For example, the poster's function was a simple calculation that multiplies the number of units sold n by the price p:

1: 
let salesTuple (price, count) = price * (float count)

The function takes a single argument of type Tuple<float, int> (or, using the nicer F# notation float * int) and immediately decomposes it into two variables, price and count. The other alternative is to write a function in the curried form:

1: 
let salesCurried price count = price * (float count)

Here, we get a function of type float -> int -> float. Usually, you can read this just as a function that takes float and int and returns float. However, you can also use partial function application and call the function with just a single argument - if the price of an apple is $1.20, we can write salesCurried 1.20 to get a new function that takes just int and gives us the price of specified number of apples. The poster's question was:

So when I want to implement a function that would have taken n > 1 arguments, should I for example always use a curried function in F# (...)? Or should I take the simple route and use regular function with an n-tuple and curry later on if necessary?

You can see my answer on StackOverflow. The point of this short introduction was that the question inspired me to think about how the world looks from the C# perspective...

To curry or not to curry?

I will not repeat the whole answer in the blog post. The key idea is that you should use tuple when the tuple has some logical meaning. For example, if you have a function that takes a range or 2D coordinates, it makes sense to use float * float.

This makes sense because you can then nicely compose multiple functions that work with ranges. For example, let's say we have a function normalizeRange and expandRange:

1: 
2: 
3: 
4: 
5: 
let normalizeRange (lo, hi) =
  if lo > hi then (hi, lo) else (lo, hi)

let expandRange offset (lo, hi) =
  (lo - offset, hi + offset)

Now we can easily write code that takes some range, normalizes it and expands it by 10:

1: 
2: 
expandRange 10 (normalizeRange(50, 30))
val it : int * int = (20, 60)

So, if your tuple has some logical meaning, taking tuple as an argument leads to more composable code and makes it easier to understand. On the other hand, if there is no logical connection, it is better to use the curried form - this makes it possible to use partial function application.

How about tuples in C#?

In C#, we can work with tuples using the Tuple<T1, T2, ...> family of types. This is certainly possible, but it is not particularly convenient, because you need to write the long type name repeatedly (you can use var inside method, but not in the method declaration).

However, there is another place where tuples appear in C# - it is perfectly reasonable to treat all .NET methods as functions that take a single tuple as the input and return some other type as the result. This is how .NET methods look when you call them from F#:

1: 
Math.Round(4.5, MidpointRounding.ToEven) 

We do not usually think about this as a tuple - it is just a method call - but what if C# had (in some future version) syntactic support for tuples and let you write (42, "Hello world") to create a tuple value of type Tuple<int, string>?

How many tuple types are there in .NET?

This inspired me to do a quick analysis of the standard .NET libraries to have a look at the tuples that standard .NET methods take. How many of them follow the good practice and take a tuple that actually means something? And how many of them should instead use the curried form, because the tuple has no logical meaning?

Checking the logical meaning will be difficult, but we can see how many of the tuples are used by more than one or two methods. If they are used in multiple places, it likely means that they represent some common pattern or some common single-purpose data structure.

This is pretty easy analysis to do using F# Interactive. Let's first look at all the types in the current AppDomain (this uses assemblies that are loaded by default in F# - so nothing fancy). We also only look at "mscorlib" and "System" assemblies:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
open System
open System.Reflection

// Get all types in currently loaded assemblies
let types = seq {
  for asm in AppDomain.CurrentDomain.GetAssemblies() do
    if asm.FullName.StartsWith("System") || 
       asm.FullName.StartsWith("mscorlib") then
      yield! asm.GetTypes() }

types |> Seq.length

The code is a simple sequence expression that iterates over all assemblies and yields all types. On my machine, this gives us some 17000 types. Now, let's get a list with all tuples - we'll iterate over all methods in each type and generate a list with the names of parameter types. We skip all methods with less than 2 parameters:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
let tuples = seq { 
  for typ in types do
    // Get declared, public, both instance and static methods
    let flags = BindingFlags.DeclaredOnly ||| BindingFlags.Public |||
                BindingFlags.Static ||| BindingFlags.Instance
    let methods = typ.GetMethods(flags)
    // Generate tuples with parameters types for each method
    for meth in methods do
      let pars = meth.GetParameters()
      if pars.Length > 1 then
        yield [ for p in meth.GetParameters() -> p.ParameterType.FullName ] }

tuples |> Seq.length

So, on my machine there are 16463 methods in .NET that take some tuple as an argument. Now, the question is, how many of them are used repeatedly? We can easily group the tuples by the list of strings (F# implements structural comparison, so this is easy to do), calculate the counts for each group and sort the results:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
let counts =
  tuples 
  |> Seq.groupBy id
  |> Array.ofSeq
  |> Array.map (fun (k, vs) -> k, Seq.length vs)
  |> Array.sortBy snd
  |> Array.rev

Most common tuples in .NET

If we run Seq.length counts, we get 5805 as the result. This means that there are 5 thousand distinct tuples (among roughly 15 thousand different methods). That certainly does not look like most of them have some logical connection. But some of the top ones certainly do - here are the top 8 (ignoring generics) with their counts:

  1. string * string (714) - looks like many methods take two strings - not sure if there is any logical meaning, but there probably are a few common uses
  2. byte[] * int * int (341) - this one looks like an array with offset and length - clearly this is a nice tuple with logical meaning
  3. int * int (327) - similar to two strings
  4. object * object (180) - hmm, maybe .NET likes untyped API :-)
  5. int * object (165) - I was a bit puzzled by this one, so I checked the methods that use this type. Good old untyped collections from the .NET 1.0 days!
  6. char[] * int * int (159) - similarly to the number 2, another nice logical tuple!
  7. string * string * string (156) - wow, so many methods take 3 strings
  8. ITypeDescriptorContext * Type (152) - huh??

How many are actually useful?

It looks like there is quite a few tuple types that actually mean something useful. But what is the distribution? Let's use the FSharp.Charting library to draw a quick chart that draws a column chart plotting the counts for every single of the 5000 tuple types:

1: 
2: 
3: 
4: 
#load "..\packages\FSharp.Charting.0.84\FSharp.Charting.fsx"
open FSharp.Charting

Chart.Column(Seq.map snd counts).WithYAxis(Log=true)

If you create a chart using just Chart.Column, then you will not see very much - the number of counts drops very quickly from the high numbers that we've seen for the first 10 types. But if we make the Y scale logarithmic (a good way to create misleading charts!) then we can actually see something:

How many times are common tuples used in .NET?

The cart shows that a vast majority of tuples are used less than 10 times and only 2000 (of some 5000) are used more than once. The analysis based on just the number of occurrences is definitely not precise, but let's say that tuples which are used more than 10 times are useful and those that are used more than 3 times are possibly useful. We can then easily draw a chart showing the proportions:

1: 
2: 
3: 
4: 
5: 
6: 
counts 
|> Seq.countBy (fun (k, v) -> 
    if v <= 2 then "Useless"
    elif v <= 10 then "Maybe useful"
    else "Useful")
|> Chart.Doughnut

This snippet gives us the following nice chart (I tweaked the look a bit - a nice feature of F# chart is that you can use Ctrl+G to open a property grid and change the fonts rather than doing everything from code):

Proportion of useful tuple types in .NET

Surely, this is ridiculous!

Yes, I can hear that. I'm comparing incomparable here - it does not make sense to look at .NET libraries as if they were F# libraries and then claim that they are poorly designed. The new version of my blog does not even have comments, but you can still argue with me on Twitter.

But before doing that - I'm not trying to criticise the design of .NET libraries in any way. If your only option is to define a method that takes parameters "as a tuple" then that's the way to go. I'm certainly not suggesting that .NET should use curried form using Func<T1, ...> delegates or that people should use Tuple<T1, ...> instead of ordinary methods.

This article is merely a thought experiment with some interesting analysis of .NET types. We can see that there are a few "natural tuples" in .NET library design (like byte[] * int * int) but the parameters of a majority of methods do not logically form a tuple.

So, is it better to use languages that make a clear distinction between (curried) functions and functions taking a tuple? I think so - it makes it easier to write composable code (by writing functions that take and return simple "ad-hoc" types as tuples) and it gives you an easy way of grouping related types. There is no class repesenting array range in .NET because adding an entire class for this would be over-kill. A simple type like tuple (supported by the language) makes this perfectly possible. On the other hand, you need to think more carefuly about library design to make sure that you use tuples correctly.

val salesTuple : price:float * count:int -> float

Full name: Tuples-in-csharp_.salesTuple
val price : float
val count : int
Multiple items
val float : value:'T -> float (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.float

--------------------
type float = Double

Full name: Microsoft.FSharp.Core.float

--------------------
type float<'Measure> = float

Full name: Microsoft.FSharp.Core.float<_>
val salesCurried : price:float -> count:int -> float

Full name: Tuples-in-csharp_.salesCurried
val normalizeRange : lo:'a * hi:'a -> 'a * 'a (requires comparison)

Full name: Tuples-in-csharp_.normalizeRange
val lo : 'a (requires comparison)
val hi : 'a (requires comparison)
val expandRange : offset:int -> lo:int * hi:int -> int * int

Full name: Tuples-in-csharp_.expandRange
val offset : int
val lo : int
val hi : int
type Math =
  static val PI : float
  static val E : float
  static member Abs : value:sbyte -> sbyte + 6 overloads
  static member Acos : d:float -> float
  static member Asin : d:float -> float
  static member Atan : d:float -> float
  static member Atan2 : y:float * x:float -> float
  static member BigMul : a:int * b:int -> int64
  static member Ceiling : d:decimal -> decimal + 1 overload
  static member Cos : d:float -> float
  ...

Full name: System.Math
Math.Round(d: decimal) : decimal
Math.Round(a: float) : float
Math.Round(d: decimal, mode: MidpointRounding) : decimal
Math.Round(d: decimal, decimals: int) : decimal
Math.Round(value: float, mode: MidpointRounding) : float
Math.Round(value: float, digits: int) : float
Math.Round(d: decimal, decimals: int, mode: MidpointRounding) : decimal
Math.Round(value: float, digits: int, mode: MidpointRounding) : float
type MidpointRounding =
  | ToEven = 0
  | AwayFromZero = 1

Full name: System.MidpointRounding
field MidpointRounding.ToEven = 0
namespace System
namespace System.Reflection
val types : seq<Type>

Full name: Tuples-in-csharp_.types
Multiple items
val seq : sequence:seq<'T> -> seq<'T>

Full name: Microsoft.FSharp.Core.Operators.seq

--------------------
type seq<'T> = Collections.Generic.IEnumerable<'T>

Full name: Microsoft.FSharp.Collections.seq<_>
val asm : Assembly
type AppDomain =
  inherit MarshalByRefObject
  member ActivationContext : ActivationContext
  member AppendPrivatePath : path:string -> unit
  member ApplicationIdentity : ApplicationIdentity
  member ApplicationTrust : ApplicationTrust
  member ApplyPolicy : assemblyName:string -> string
  member BaseDirectory : string
  member ClearPrivatePath : unit -> unit
  member ClearShadowCopyPath : unit -> unit
  member CreateComInstanceFrom : assemblyName:string * typeName:string -> ObjectHandle + 1 overload
  member CreateInstance : assemblyName:string * typeName:string -> ObjectHandle + 3 overloads
  ...

Full name: System.AppDomain
property AppDomain.CurrentDomain: AppDomain
AppDomain.GetAssemblies() : Assembly []
property Assembly.FullName: string
String.StartsWith(value: string) : bool
String.StartsWith(value: string, comparisonType: StringComparison) : bool
String.StartsWith(value: string, ignoreCase: bool, culture: Globalization.CultureInfo) : bool
Assembly.GetTypes() : Type []
module Seq

from Microsoft.FSharp.Collections
val length : source:seq<'T> -> int

Full name: Microsoft.FSharp.Collections.Seq.length
val tuples : seq<string list>

Full name: Tuples-in-csharp_.tuples
val typ : Type
val flags : BindingFlags
type BindingFlags =
  | Default = 0
  | IgnoreCase = 1
  | DeclaredOnly = 2
  | Instance = 4
  | Static = 8
  | Public = 16
  | NonPublic = 32
  | FlattenHierarchy = 64
  | InvokeMethod = 256
  | CreateInstance = 512
  ...

Full name: System.Reflection.BindingFlags
field BindingFlags.DeclaredOnly = 2
field BindingFlags.Public = 16
field BindingFlags.Static = 8
field BindingFlags.Instance = 4
val methods : MethodInfo []
Type.GetMethods() : MethodInfo []
Type.GetMethods(bindingAttr: BindingFlags) : MethodInfo []
val meth : MethodInfo
val pars : ParameterInfo []
MethodBase.GetParameters() : ParameterInfo []
property Array.Length: int
val p : ParameterInfo
property ParameterInfo.ParameterType: Type
property Type.FullName: string
val counts : (string list * int) []

Full name: Tuples-in-csharp_.counts
val groupBy : projection:('T -> 'Key) -> source:seq<'T> -> seq<'Key * seq<'T>> (requires equality)

Full name: Microsoft.FSharp.Collections.Seq.groupBy
val id : x:'T -> 'T

Full name: Microsoft.FSharp.Core.Operators.id
type Array =
  member Clone : unit -> obj
  member CopyTo : array:Array * index:int -> unit + 1 overload
  member GetEnumerator : unit -> IEnumerator
  member GetLength : dimension:int -> int
  member GetLongLength : dimension:int -> int64
  member GetLowerBound : dimension:int -> int
  member GetUpperBound : dimension:int -> int
  member GetValue : params indices:int[] -> obj + 7 overloads
  member Initialize : unit -> unit
  member IsFixedSize : bool
  ...

Full name: System.Array
val ofSeq : source:seq<'T> -> 'T []

Full name: Microsoft.FSharp.Collections.Array.ofSeq
val map : mapping:('T -> 'U) -> array:'T [] -> 'U []

Full name: Microsoft.FSharp.Collections.Array.map
val k : string list
val vs : seq<string list>
val sortBy : projection:('T -> 'Key) -> array:'T [] -> 'T [] (requires comparison)

Full name: Microsoft.FSharp.Collections.Array.sortBy
val snd : tuple:('T1 * 'T2) -> 'T2

Full name: Microsoft.FSharp.Core.Operators.snd
val rev : array:'T [] -> 'T []

Full name: Microsoft.FSharp.Collections.Array.rev
namespace FSharp
namespace FSharp.Charting
type Chart =
  static member Area : data:seq<#value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string -> GenericChart
  static member Area : data:seq<#value * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string -> GenericChart
  static member Bar : data:seq<#value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string -> GenericChart
  static member Bar : data:seq<#value * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string -> GenericChart
  static member BoxPlotFromData : data:seq<#value * #seq<'a2>> * ?Name:string * ?Title:string * ?Color:Color * ?XTitle:string * ?YTitle:string * ?Percentile:int * ?ShowAverage:bool * ?ShowMedian:bool * ?ShowUnusualValues:bool * ?WhiskerPercentile:int -> GenericChart (requires 'a2 :> value)
  static member BoxPlotFromStatistics : data:seq<#value * #value * #value * #value * #value * #value * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string * ?Percentile:int * ?ShowAverage:bool * ?ShowMedian:bool * ?ShowUnusualValues:bool * ?WhiskerPercentile:int -> GenericChart
  static member Bubble : data:seq<#value * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string * ?BubbleMaxSize:int * ?BubbleMinSize:int * ?BubbleScaleMax:float * ?BubbleScaleMin:float * ?UseSizeForLabel:bool -> GenericChart
  static member Bubble : data:seq<#value * #value * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string * ?BubbleMaxSize:int * ?BubbleMinSize:int * ?BubbleScaleMax:float * ?BubbleScaleMin:float * ?UseSizeForLabel:bool -> GenericChart
  static member Candlestick : data:seq<#value * #value * #value * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string -> CandlestickChart
  static member Candlestick : data:seq<#value * #value * #value * #value * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Color * ?XTitle:string * ?YTitle:string -> CandlestickChart
  ...

Full name: FSharp.Charting.Chart
static member Chart.Column : data:seq<#value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Drawing.Color * ?XTitle:string * ?YTitle:string -> ChartTypes.GenericChart
static member Chart.Column : data:seq<#value * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Drawing.Color * ?XTitle:string * ?YTitle:string -> ChartTypes.GenericChart
val map : mapping:('T -> 'U) -> source:seq<'T> -> seq<'U>

Full name: Microsoft.FSharp.Collections.Seq.map
val countBy : projection:('T -> 'Key) -> source:seq<'T> -> seq<'Key * int> (requires equality)

Full name: Microsoft.FSharp.Collections.Seq.countBy
val v : int
static member Chart.Doughnut : data:seq<#value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Drawing.Color * ?XTitle:string * ?YTitle:string -> ChartTypes.DoughnutChart
static member Chart.Doughnut : data:seq<#value * #value> * ?Name:string * ?Title:string * ?Labels:#seq<string> * ?Color:Drawing.Color * ?XTitle:string * ?YTitle:string -> ChartTypes.DoughnutChart

Discuss on twitter, .
Send corrections via GitHub pull requests.