The unit type is an alternative to the native C# struct Void
(with the global alias void
).
The C# compiler handles Void
/void
very different to all other types in C#:
Void
can never be used. You must always usevoid
void
can not be instantiatedvoid
can not be used as a generic argument to any function- A function (sometimes also called method) "returning"
void
does not return a value, and the result of the function (void
) can not be assigned to any variable - When using reflection, void-methods will return
null
because the compiler can not know during compile-time what the dynamically dispatched method will return
These limitations on the void type introduce annoying behaviour, especially with expressions and generics.
Example 1 - the "void is not a real type" dilemma:
This examples uses a simple algebraic datatype with a generic match method:
public abstract class SimpleAlgebraicDatatype
{
private SimpleAlgebraicDatatype()
{
}
public abstract TResult Match<TResult>(
Func<Variant1, TResult> variant1,
Func<Variant2, TResult> variant2);
public class Variant1 : SimpleAlgebraicDatatype
{
public Variant1(string someValue)
{
SomeValue = someValue;
}
public string SomeValue { get; }
public override TResult Match<TResult>(
Func<Variant1, TResult> variant1,
Func<Variant2, TResult> variant2)
=> variant1(this);
}
public class Variant2 : SimpleAlgebraicDatatype
{
public Variant2(int someValue)
{
SomeValue = someValue;
}
public int SomeValue { get; }
public override TResult Match<TResult>(
Func<Variant1, TResult> variant1,
Func<Variant2, TResult> variant2)
=> variant2(this);
}
}
It then can be used like that:
SimpleAlgebraicDatatype variant = new SimpleAlgebraicDatatype.Variant1("Hey");
// get the variant as string
var value = variant.Match(
variant1: variant1 => variant1.SomeValue,
variant2: variant2 => Convert.ToString(variant2.SomeValue));
Console.WriteLine(value);
But if you don't want to use the return value, you're stuck with returning a value you don't want. This does not compile:
// Error [CS0411] The type arguments for method 'method' cannot be inferred from the usage. Try specifying the type arguments explicitly.
variant.Match(
variant1: variant1 => Console.Write(variant1.SomeValue),
variant2: variant2 => Console.Write(Convert.ToString(variant2.SomeValue)));
Now you have to decide what it returns. One option is to return null - the best fitting return type is probably object?
in that case:
variant.Match<object?>(
variant1: variant1 =>
{
Console.Write(variant1.SomeValue);
return null;
},
variant2: variant2 =>
{
Console.Write(Convert.ToString(variant2.SomeValue));
return null;
});
This is very unstatisfying however. We have to trick the type-system. There should be a more expressive way.
Funcky.Unit to the rescue:
variant.Match(
variant1: variant1 =>
{
Console.Write(variant1.SomeValue);
return Unit.Value;
},
variant2: variant2 =>
{
Console.Write(Convert.ToString(variant2.SomeValue));
return Unit.Value;
});
Now this isn't really less noise. This is why we created ActionToUnit.
This clears up the code to:
variant.Match(
variant1: variant1 => ActionToUnit(() => Console.Write(variant1.SomeValue)),
variant2: variant2 => ActionToUnit(() => Console.Write(Convert.ToString(variant2.SomeValue))));
See ActionToUnit for an explanation.
Example 2 - the "switch expression must return something" dilemma:
The following two code snippes do not comple:
// Error [CS0201]: Only assignment, call, increment, decrement, and new object expressions can be used as a statement.
variant switch
{
SimpleAlgebraicDatatype.Variant1 variant1 => Console.Write(variant1.SomeValue),
SimpleAlgebraicDatatype.Variant2 variant2 => Console.Write(Convert.ToString(variant2.SomeValue)),
_ => throw new Exception("Unreachable"),
};
// Error [CS0029]: Cannot implicitly convert type 'thorw-expression' to 'void'
// Error [CS9209]: A value of type 'void' may not be assigned.
_ = variant switch
{
SimpleAlgebraicDatatype.Variant1 variant1 => Console.Write(variant1.SomeValue),
SimpleAlgebraicDatatype.Variant2 variant2 => Console.Write(Convert.ToString(variant2.SomeValue)),
_ => throw new Exception("Unreachable"),
};
One way to resolve this dilemma is to return a method that returns null from every arm, and execute it after:
// very verbose and not very readable
Func<SimpleAlgebraicDatatype, object> action = variant switch
{
SimpleAlgebraicDatatype.Variant1 variant1 => _ =>
{
Console.Write(Convert.ToString(variant1.SomeValue));
return null;
},
SimpleAlgebraicDatatype.Variant2 variant2 => _ =>
{
Console.Write(Convert.ToString(variant2.SomeValue));
return null;
},
_ => _ => throw new Exception("Unreachable"),
};
action(variant);
If we use ActionToUnit once again, we can simplify this code, by a lot:
_ = variant switch
{
SimpleAlgebraicDatatype.Variant1 variant1 => ActionToUnit(() => Console.Write(variant1.SomeValue)),
SimpleAlgebraicDatatype.Variant2 variant2 => ActionToUnit(() => Console.Write(Convert.ToString(variant2.SomeValue))),
};
If you cannot use the discard syntax (_ =
), simply use var _
or similar, and ignore the variable after.