dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.91k stars 4.63k forks source link

[API Proposal]: Boolean should implement IBitwiseOperators #103141

Open Charlieface opened 3 months ago

Charlieface commented 3 months ago

Background and motivation

System.Numerics and the interface operators feature has allowed us to generic-ize over arbitrary numerical types, to use operators and functions such as + * Zero. One of those interfaces is IBitwiseOperators, which allows the operators & | ^ and ~. This is implemented by all the integer types.

I propose that Boolean should implement this as well, even though it doesn't implement any other Numerics interfaces. It does have all those operators, and this would allow it to be used anywhere that you specify IBitwiseOperators.

The motivation is writing generic code that can do arbitrary bitwise operations. For example, I'd like the following LINQ extension, which mimics the bit_and aggregate function in some SQL implementations.

public static T BitAnd<T>(this IEnumerable<T> source) where T : IBitwiseOperators<T, T, T> =>
    source.Aggregate((prev, next) => prev & next);

But this won't work with bool and requires a separate implementation just for it.

API Proposal

namespace System
{
    /// <summary>
    /// Represents a boolean (<see langword="true"/> or <see langword="false"/>) value.
    /// </summary>
    [Serializable]
    [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
    public readonly struct Boolean
        : IComparable,
          IConvertible,
          IComparable<bool>,
          IEquatable<bool>,
+         IBitwiseOperators<bool, bool, bool>,
          ISpanParsable<bool>
        static bool IBitwiseOperators<bool, bool, bool>.operator &(bool left, bool right) => left && right;

        static bool IBitwiseOperators<bool, bool, bool>.operator |(bool left, bool right) => left || right;

        static bool IBitwiseOperators<bool, bool, bool>.operator ^(bool left, byte right) => left ^ right;

        static bool IBitwiseOperators<bool, bool, bool>.operator ~(bool value) => ~value;

API Usage

As mentioned, bit aggregation operations are the most useful usage. ReadOnlySpan would be good to implement for also

public static T BitAnd<T>(this ReadOnlySpan<T> source) where T : IBitwiseOperators<T, T, T>
{
    if (source.Length == 0)
        return default;

    var value = source[0];
    for (var i = 1; i < source.Length; i++)
        value &= source[i];

    return value
}

Merge adjacent runs using &

public static IEnumerable<T> MergeAnd<T>(this IEnumerable<T> source) where T : IBitwiseOperators<T, T, T>
{
    using var enumer = source.GetEnumerator();
    while (enumer.MoveNext())
    {
        var left = enumer.Current;
        if (!enumer.MoveNext()) throw new InvalidOperationException("Not even number of elements");
        var right = enumer.Current;
        yield return left & right;
    }
}

An IMP function (logical implication)

public static T Imp<T>(T left, T right) where T : IBitwiseOperators<T, T, T> => ~left | right;

Alternative Designs

No response

Risks

What happens to the C#/Roslyn compiler in the presence of such functions, where bool is used directly? Does it use those functions, or is it hard-coded to use IL bytecode instead? This would be a similar problem to any other of the numeric interfaces, if it is actually an issue.

dotnet-policy-service[bot] commented 3 months ago

Tagging subscribers to this area: @dotnet/area-system-numerics See info in area-owners.md if you want to be subscribed.

eiriktsarpalis commented 3 months ago

I believe that this was considered and rejected at some point, cc @tannergooding

huoyaoyuan commented 3 months ago

Note that bitwise operators on bool does not short-circuit, unlike the logical operators. Sometimes they are used intentionally. So operator &(bool left, bool right) => left && right is incorrect and confusing.

Charlieface commented 3 months ago

@huoyaoyuan

Note that bitwise operators on bool does not short-circuit, unlike the logical operators. Sometimes they are used intentionally. So operator &(bool left, bool right) => left && right is incorrect and confusing.

The fact that && is used internally in the function seems immaterial to me, it will still be non-short-circuiting (and therefore side-affecting) in the case of functions. Only for variables and fields being passed in then the JIT and/or CPU could still short-circuit it, as it is not side-affecting.

The instances when & and | are used on purpose are small enough that you would want to know exactly what the CPU is doing anyway (eg cryptography side-channel problems).

tannergooding commented 3 months ago

I believe that this was considered and rejected at some point, cc @tannergooding

It wasn't outright rejected, it was just considered out of scope for the initial Generic Math feature work since it wasn't directly related to numeric types. The same was true for implementing IEqualityOperators on other types.

I'd in general be fine with taking this back to API review and think it would fit in well with some of the Tensor<T> work that's happening as it would allow Tensor<bool> to work with the helpers like Tensor.And.

CC. @stephentoub for secondary input.

eiriktsarpalis commented 3 months ago

What's the most derived numeric abstraction we might consider for bool? IBitwiseOperators fits the mould, technically speaking we could go as far as having it implement IBinaryInteger (in the sense of boolean operations being the same as modular arithmetic mod 2)

eiriktsarpalis commented 3 months ago

Should this be using explicit interface implementations?

tannergooding commented 3 months ago

I don't think we'd want IBinaryInteger, unlike char which explicitly has support for the full set of numeric operators in C# and which often warrants using those; bool really only supports the bitwise and equality operators.

There's also a lot of subtle quirks that come into play with regards to how IL vs C# view bool. Where IL says 0 is false and any other bit pattern is true, while C# says 0 is false, 1 is true, and any other bit pattern is undefined.

This can lead to quirks in some cases since 2 is a valid bool in IL and so from that perspective, x + y should be equivalent to operating over byte, but that breaks for various downstream operations and expectations when encountered.

So, I think we'd only want to implement the underlying interfaces that make sense and then only the APIs which are appropriately constrained could be invoked (which we already try to fit the minimum interface set needed).

Charlieface commented 3 months ago

@eiriktsarpalis (also @huoyaoyuan relevant to what we mentioned):

What's the most derived numeric abstraction we might consider for bool? IBitwiseOperators fits the mould, technically speaking we could go as far as having it implement IBinaryInteger (in the sense of boolean operations being the same as modular arithmetic mod 2)

I think there are a couple of completely missing interfaces here as well, because && and || short-circuiting are implemented in C# via true and false operators.

public interface ITruthOperators<TSelf>
    where TSelf : ITruthOperators<TSelf>
{
    static abstract bool operator true(TSelf val);

    static abstract bool operator false(TSelf val);

    static abstract TSelf operator !(TSelf val);
}

public interface ITruthIdentity<TSelf>
    where TSelf : ITruthIdentity<TSelf>
{
    static TSelf TruthyIdentity { get; }

    static TSelf FalseyIdentity { get; }
}

And then bool would have:

    public static bool operator true(bool val) => val;

    public static bool operator false(bool val) => !val;

    public static bool operator !(bool val) => !val;

    public static bool TruthyIdentity => true;

    public static bool FalseyIdentity => false;

This would allow short-circuiting && and || operators to work on anything implementing both ITruthOperators and IBitwiseOperators.

Having said that, I tried it, see this fiddle, and for unclear reasons I got CS0217: In order to be applicable as a short circuit operator a user-defined logical operator ('IBitwiseOperators<T, T, T>.operator &(T, T)') must have the same return type and parameter types

jeffhandley commented 1 month ago

Because of the potential alignment with Tensor<bool> here, I'm putting this into the 10.0.0 milestone so we make sure to consider it for the release. If that alignment with Tensor doesn't materialize though, we might move this back to Future.