1

I am trying to define an operator overloads in C# for 2 classes: Fraction and BasicFraction. On the outside, they act the same, but internally Fraction uses PrimeFactorization (stores number as a list of prime factors) and BasicFraction uses int for storing the nominator and denominator.

They would follow this interface (if I use JSDoc type definitions):

public interface IFraction
{
  public (int|Primefactoriztion) a; // nominator
  public (int|Primefactoriztion) b; // denominator
}

But since I can't do that like this, is there a simple way to use Fraction and BasicFraction interchangeably?
I can define all operator overloads for evaluating interactions between int and PrimeFactorization. However, I don't know how to tell C# to accept both to be passed to methods.

Tl;dr: how to make one property allow 2 data types?

Notes:

  • I am new to advanced C# (I know static typing and basic OOP, but nothing more). I am coming from JS and Python background.
  • I know they are probably good libraries with this functionality. I am writing this in order to learn C#, not for production.

Edit: Currently, PrimeFactorization stores factors and their power in Dictionary<int, BasicFraction> (I would use Fraction, but it causes recursion; this is basically the only usage of BasicFraction).

Maneren
  • 98
  • 1
  • 8
  • Search for `c# Generics`. – CodingYoshi Feb 21 '21 at 14:19
  • Can you explain. You want one method of your class to return different types? Or you want one method to get two different types as an input, but return the same type? – Ivan Khorin Feb 21 '21 at 14:22
  • @Ivan Khorin in short, I want to use intechangably two classes which I know have compatible interfaces, but can't define that interface to compiler – Maneren Feb 21 '21 at 19:48
  • Please be aware that the idea of using 1 property for 2 data types is *not* a normal thing to do in C#. While there are several ways to do it, the mindset of C# is that the type system should be relied on as much as possible to maintain correct code – Brian Feb 23 '21 at 14:09

2 Answers2

2

You want a discriminated union type. C# does not support them natively as a first-class language feature but you can hack-it using OneOf: https://github.com/mcintyre321/OneOf

So you would end-up with:

public interface IFraction
{
  public OneOf< int, Primefactoriztion > a; // nominator
  public OneOf< int, Primefactoriztion > b; // denominator
}

but it's still a code-smell. I think you need to reconsider your entire approach. Also, using interfaces to represent values probably isn't a good idea (due to all the heap-allocation), especially because your a and b members appear to be mutable.


A better idea is to change your Primefactoriztion type (which I hope and assume is a struct, not a class) to support implicit conversion from int and some member for explicit conversion to int if the operation is unsafe (e.g. if the value is not an integer):

struct Primefactoriztion
{
    public static implicit operator int(Primefactoriztion self)
    {
        return ...
    }
}

That way you can safely eliminate the OneOf<int and use just Primefactoriztion:

public interface IFraction
{
  public Primefactoriztion a; // nominator
  public Primefactoriztion b; // denominator
}
Dai
  • 141,631
  • 28
  • 261
  • 374
  • Well, `PrimeFactorization` is class, because I don't know the difference between `class` and `struct` (from JS I only know the concept of class). It stores factors in `Dictionary` (again, because of JS objects). I am only using the `BasicFraction` to prevent recursion (`Fraction` would create `PrimeFactorization` which would create `Fraction`...). – Maneren Feb 21 '21 at 18:55
  • See: [What's the difference between struct and class in .NET?](https://stackoverflow.com/questions/13049/whats-the-difference-between-struct-and-class-in-net). – Olivier Jacot-Descombes Feb 22 '21 at 18:40
2

What you have in mind is called discriminated union and is not available as language feature in C#.

I would use a hybrid approach where a fraction would contain nominator and denominator as int plus prime factor lists as read-only properties. You could initialize the fraction with either ints or prime factor lists through two constructors. The missing entries would be calculated lazily to minimize the calculation overhead.

Making the properties read-only increases the robustness of the code. Especially if you are using structs. See Mutating readonly structs (Eric Lippert's blog: Fabulous adventures in coding). But note that the struct is still mutable because of the lazy evaluation.

public struct Fraction
{
    public Fraction(int nominator, int denominator)
    {
        _nominator = nominator;
        _denominator = denominator;
        _nominatorPrimeFactors = null;
        _denominatorPrimeFactors = null;
    }

    public Fraction(IList<int> nominatorPrimeFactors, IList<int> denominatorPrimeFactors)
    {
        if (nominatorPrimeFactors == null || nominatorPrimeFactors.Count == 0) {
            throw new ArgumentNullException(
                $"{nameof(nominatorPrimeFactors)} must be a non-null, non-empty list");
        }
        if (denominatorPrimeFactors == null || denominatorPrimeFactors.Count == 0) {
            throw new ArgumentNullException(
                $"{nameof(denominatorPrimeFactors)} must be a non-null, non-empty list");
        }
        _nominator = null;
        _denominator = null;
        _nominatorPrimeFactors = nominatorPrimeFactors;
        _denominatorPrimeFactors = denominatorPrimeFactors;
    }

    private int? _nominator;
    public int Nominator
    {
        get {
            if (_nominator == null) {
                _nominator = _nominatorPrimeFactors.Aggregate(1, (x, y) => x * y);
            }
            return _nominator.Value;
        }
    }

    private int? _denominator;
    public int Denominator
    {
        get {
            if (_denominator == null) {
                _denominator = _denominatorPrimeFactors.Aggregate(1, (x, y) => x * y);
            }
            return _denominator.Value;
        }
    }

    private IList<int> _nominatorPrimeFactors;
    public IList<int> NominatorPrimeFactors
    {
        get {
            if (_nominatorPrimeFactors == null) {
                _nominatorPrimeFactors = Factorize(Nominator);
            }
            return _nominatorPrimeFactors;
        }
    }

    private IList<int> _denominatorPrimeFactors;
    public IList<int> DenominatorPrimeFactors
    {
        get {
            if (_denominatorPrimeFactors == null) {
                _denominatorPrimeFactors = Factorize(Denominator);
            }
            return _denominatorPrimeFactors;
        }
    }

    private static List<int> Factorize(int number)
    {
        var result = new List<int>();

        while (number % 2 == 0) {
            result.Add(2);
            number /= 2;
        }

        int factor = 3;
        while (factor * factor <= number) {
            if (number % factor == 0) {
                result.Add(factor);
                number /= factor;
            } else {
                factor += 2;
            }
        }
        if (number > 1) result.Add(number);

        return result;
    }

    public override string ToString()
    {
        if (_nominatorPrimeFactors == null && _denominatorPrimeFactors == null) {
            return $"{_nominator}/{_denominator}";
        }
        string npf = ListToString(_nominatorPrimeFactors);
        string dpf = ListToString(_denominatorPrimeFactors);

        if (_nominator == null && _denominator == null) {
            return $"({npf}) / ({dpf})";
        }
        return $"{_nominator}/{_denominator}, ({npf}) / ({dpf})";


        static string ListToString(IList<int> primeFactors)
        {
            if (primeFactors == null) {
                return null;
            }
            return String.Join(" * ", primeFactors.Select(i => i.ToString()));
        }
    }
}

Note that declaring the prime factor lists a IList<int> allows you to initialize the fraction with either int[] or List<int>.

But it is worth considering whether the prime factors really need to be stored. Isn't it enough to calculate them when required by some calculation?

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • Holy! Thanks for the thorough answer. I added some info to my question. Meanwhile I managed to hack together pretty wonky solution: dynamically converting from `Fraction` to `BasicFraction` and vice versa, whenever needed. It sorta works but I had to define operator overloads for 3 classes (and many of them same calculation just with different class names). And now I am painfully bug hunting many for little typos in all the calculations and conversions. (Still gotta learn how to use debugger and unittesting in C#) – Maneren Feb 21 '21 at 20:02
  • the prime factorization is mainly for handling numbers like `2^(-2/5)` so it doesn't just spit out decimal numbers. So if I would calculate it on the fly, wouldn't I lose this or end up with ridiculous fractional approximations of decimal numbers? – Maneren Feb 21 '21 at 23:26
  • When you convert decimal numbers to fractions you must introduce a precision limit. What should the result of converting PI be? Working with a precision of 0.001 you might end up with 22/7. Without such a limit you might end up by dividing 2 huge integers, which is nonsensical. Even with a decimal representation of 1/7 this might lead to nonsensical results because the endless decimals will be truncated at some place and introduce an error. – Olivier Jacot-Descombes Feb 22 '21 at 14:34
  • Whether the result of converting between the `int/int` and prime factor representation is stored or calculated on the fly makes no difference in precision and has nothing to do with converting between decimal and fraction. – Olivier Jacot-Descombes Feb 22 '21 at 14:41
  • I mean, I am using `PrimeFactorization` so I don't run in those rounding errors and at the same time have the values pretty printed in the end – Maneren Feb 22 '21 at 15:19
  • 2
    Though you are correct that discriminated unions are not a *language feature* of C#, they are a *framework feature* of the CLR and possible to make it work from C#; doing so is in my opinion a bad idea and should be used only for interoperability with unmanaged code that uses discriminated unions. What you do to make a union is use the FieldOffsetAttribute to tell the CLR that two struct fields overlap each other. – Eric Lippert Feb 22 '21 at 17:26