C
C#13mo ago
SWEETPONY

✅ Is it possible to use union type in C#?

I'm not sure that it's called exactly Union Type Let me introduce the issue I have this class which represent generator:
public class UISchemaGenerator
{
private readonly ConcurrentDictionary<(CustomFieldSet, CultureInfo), UISchemaSpecification> _cache =
new();

public UISchemaSpecification Generate(
CustomFieldSet fieldSet,
MandatorySecurityScope scope,
CultureInfo culture = default)
{
culture ??= CultureInfo.CurrentCulture;

return _cache.GetOrAdd(
key: (fieldSet, culture),
valueFactory: _ => InternalGenerator(fieldSet, scope, culture));
}

private UISchemaSpecification InternalGenerator(
CustomFieldSet fieldSet,
MandatorySecurityScope scope,
CultureInfo culture)
{
var schemaBuilder = new UISchemaBuilder();

foreach (var group in fieldSet.Groups)
{
schemaBuilder
.StartGroup(group.Title)
.AddFields(scope, group.Fields)
.EndGroup();
}

return schemaBuilder.Build();
}

private UISchemaSpecification InternalGenerator(
CustomField.CustomField fieldSet,
MandatorySecurityScope scope,
CultureInfo culture)
{
var schemaBuilder = new UISchemaBuilder()
.AddField(fieldSet)
.Build();

return schemaBuilder;
}
}
public class UISchemaGenerator
{
private readonly ConcurrentDictionary<(CustomFieldSet, CultureInfo), UISchemaSpecification> _cache =
new();

public UISchemaSpecification Generate(
CustomFieldSet fieldSet,
MandatorySecurityScope scope,
CultureInfo culture = default)
{
culture ??= CultureInfo.CurrentCulture;

return _cache.GetOrAdd(
key: (fieldSet, culture),
valueFactory: _ => InternalGenerator(fieldSet, scope, culture));
}

private UISchemaSpecification InternalGenerator(
CustomFieldSet fieldSet,
MandatorySecurityScope scope,
CultureInfo culture)
{
var schemaBuilder = new UISchemaBuilder();

foreach (var group in fieldSet.Groups)
{
schemaBuilder
.StartGroup(group.Title)
.AddFields(scope, group.Fields)
.EndGroup();
}

return schemaBuilder.Build();
}

private UISchemaSpecification InternalGenerator(
CustomField.CustomField fieldSet,
MandatorySecurityScope scope,
CultureInfo culture)
{
var schemaBuilder = new UISchemaBuilder()
.AddField(fieldSet)
.Build();

return schemaBuilder;
}
}
I want somehow add following logic:
public UISchemaSpecification Generate(
Union<CustomField, CustomFieldSet> fieldSet,
MandatorySecurityScope scope,
CultureInfo culture = default)
{
// if someone passed CustomField then call InternalGenerator for CustomField
// if someone passed CustomFieldSetthen call InternalGenerator for CustomFieldSet
}

public UISchemaSpecification Generate(
Union<CustomField, CustomFieldSet> fieldSet,
MandatorySecurityScope scope,
CultureInfo culture = default)
{
// if someone passed CustomField then call InternalGenerator for CustomField
// if someone passed CustomFieldSetthen call InternalGenerator for CustomFieldSet
}

I don't want to override method because it will be too much code in one class dictionary x2, Generate x2, InternalGenerator x2
9 Replies
SWEETPONY
SWEETPONY13mo ago
I think it does not answer my question I mean I need smth like Either maybe
Zendist
Zendist13mo ago
language-ext has some things like this if you prefer FP: https://github.com/louthy/language-ext The language doesn't have it by default (yet). What you're looking for is a discriminated union, right?
GitHub
GitHub - louthy/language-ext: C# functional language extensions - a...
C# functional language extensions - a base class library for functional programming - GitHub - louthy/language-ext: C# functional language extensions - a base class library for functional programming
Zendist
Zendist13mo ago
Or tagged union, depending on which FP circles you identify with.
Thinker
Thinker13mo ago
There's a million either/option/DU libraries out there. LanguageExt is cool although a bit intrusive to the average C# workflow. There's also OneOf which is a pretty solid either implementation. You could also make your own, they're not too difficult to implement, and with a few helper methods it's pretty workable.
public readonly struct Either<TA, TB>
{
private readonly T1? a;
private readonly T2? b;
private bool isA;
private bool isB;

public T1 A => isA ? a : throw new InvalidOperationException("Value isn't A.");
public T2 B => isB ? b : throw new InvalidOperationException("Value isn't B.");

public Either(TA a)
{
this.a = a;
b = default;
isA = true;
isB = false;
}

public Either(TB b)
{
a = default;
this.b = b;
isA = false;
isB = true;
}
}
public readonly struct Either<TA, TB>
{
private readonly T1? a;
private readonly T2? b;
private bool isA;
private bool isB;

public T1 A => isA ? a : throw new InvalidOperationException("Value isn't A.");
public T2 B => isB ? b : throw new InvalidOperationException("Value isn't B.");

public Either(TA a)
{
this.a = a;
b = default;
isA = true;
isB = false;
}

public Either(TB b)
{
a = default;
this.b = b;
isA = false;
isB = true;
}
}
or if you fancy records and better pattern matching capabilities
public abstract record Either<TA, TB>
{
public sealed record A(TA value) : Either<TA, TB>;
public sealed record B(TB value) : Either<TA, TB>;
}
public abstract record Either<TA, TB>
{
public sealed record A(TA value) : Either<TA, TB>;
public sealed record B(TB value) : Either<TA, TB>;
}
SWEETPONY
SWEETPONY13mo ago
thanks, guys that is what I needed
Zendist
Zendist13mo ago
I like how concise the abstract record is. Can you show an example of usage?
Thinker
Thinker13mo ago
with a couple helper/extension methods and implicit conversions
public abstract record Either<TA, TB>
{
public sealed record A(TA Value) : Either<TA, TB>;
public sealed record B(TB Value) : Either<TA, TB>;

public static implicit operator Either<TA, TB>(TA value) => new A(value);
public static implicit operator Either<TA, TB>(TB value) => new B(value);

public Either<TResult, TB> MapA<TResult>(Func<TA, TResult> f) => this switch
{
A a => f(a.Value),
B b => new Either<TResult, TB>.B(b.Value),
};

public Either<TA, TResult> MapA<TResult>(Func<TB, TResult> f) => this switch
{
A a => new Either<TA, TResult>.A(b.Value),
B b => f(b.Value),
};

public T Match<T>(Func<TA, T> ifA, Func<TB, T> ifB) => this switch
{
A a => ifA(a.Value),
B b => ifB(b.Value),
};
}
public abstract record Either<TA, TB>
{
public sealed record A(TA Value) : Either<TA, TB>;
public sealed record B(TB Value) : Either<TA, TB>;

public static implicit operator Either<TA, TB>(TA value) => new A(value);
public static implicit operator Either<TA, TB>(TB value) => new B(value);

public Either<TResult, TB> MapA<TResult>(Func<TA, TResult> f) => this switch
{
A a => f(a.Value),
B b => new Either<TResult, TB>.B(b.Value),
};

public Either<TA, TResult> MapA<TResult>(Func<TB, TResult> f) => this switch
{
A a => new Either<TA, TResult>.A(b.Value),
B b => f(b.Value),
};

public T Match<T>(Func<TA, T> ifA, Func<TB, T> ifB) => this switch
{
A a => ifA(a.Value),
B b => ifB(b.Value),
};
}
public Either<User, string> GetUser(int id)
{
if (db.UserExists(id)) return db.GetUser(id);
else return "user does not exist";
}
public Either<User, string> GetUser(int id)
{
if (db.UserExists(id)) return db.GetUser(id);
else return "user does not exist";
}
var user = GetUser(id);
var username = user.MapA(user => user.Username);

Console.WriteLine(username.Match(
ifA: username => username,
ifB: error => $"GetUser returned an error: {error}"));
var user = GetUser(id);
var username = user.MapA(user => user.Username);

Console.WriteLine(username.Match(
ifA: username => username,
ifB: error => $"GetUser returned an error: {error}"));
This has a lot of room for improvement but imo it's the nicest you'll probably get Unfortunately C#'s type inference isn't powerful enough to make this like... actually nice. You run into a lot of annoyances really quickly when you try doing something like this. If you look at for instance Rust then you'll find language features (besides just plain DU support) which make things like this really good.
SWEETPONY
SWEETPONY13mo ago
looks good, thanks for explanation I will try to use this