Как заставить оператор умножения (*) вести себя как короткое замыкание?

У меня много вычислений, особенно умножение, где первая часть иногда равна нулю, и я не хочу оценивать второй операнд в этом случае. В С# есть как минимум два оператора короткого замыкания: && и ||, которые оценивают второй операнд только в случае необходимости. Я хочу добиться аналогичного поведения с оператором умножения.

В .net вы не можете напрямую перегружать оператор &&, но вы можете перегружать операторы & и false, чтобы вы могли использовать ваши точки расширения для изменения поведения коротких сообщений, оператор цепи. Вы можете найти более подробную информацию в этой статье Перегрузка оператора С#: '& & Оператор

Есть ли какие-либо средства для достижения этого или подобного поведения для оператора умножения?

Это чисто синтаксический вопрос, потому что реализация очень проста. Следующий метод обеспечивает именно то, что я хочу с точки зрения функциональности:

public static double ShortCircuitMultiply(double val, Func<double> anotherValue)
{
    var epsilon = 0.00000001;
    return Math.Abs(val) < epsilon ? 0 : val * anotherValue();
}

Примечание: эта реализация не заполнена: в С#, если вы умножаете 0.0 на Double.NaN или Double.NegativeInfinity или Double.PositiveInfinity, вы получите NaN, но в терминах ShortCircuitMultiply - только ноль. Пусть игнорируют эту деталь, и это действительно неуместно в моем домене.

Итак, теперь, если я назову его ShortCircuitMultiply(0.0, longOperation), где longOperation - Func<double>, последний член не будет оценен, а результат операции будет фактически равен нулю.

Проблема в том, что, как я уже сказал, у меня было бы много вызовов ShortCircuitMultiply, и я хочу сделать код более читаемым. Я хочу, чтобы код был похож на 0.0 * longOperation(), если это возможно.


Еще одно замечание: я попытался создать оболочку на double и создать неявное литье для двойного, а также для перегрузки *. Я понимаю, что это, вероятно, избыточно: я хотел получить читаемость, но пытаюсь создать еще одну оболочку. В любом случае следующий код демонстрирует мое намерение:

class MyDouble
{
    double value;
    public MyDouble(double value)
    {
        this.value = value; 
    }

    public static MyDouble operator *(MyDouble left, MyDouble right) 
    {
        Console.WriteLine ("* operator call");
        return new MyDouble(left.value * right.value);
    }

    public static implicit operator double(MyDouble myDouble)
    {
        Console.WriteLine ("cast to double");
        return myDouble.value;
    }

    public static implicit operator MyDouble(double value)
    {
        Console.WriteLine ("cast to MyDouble");
        return new MyDouble(value);
    }
}

Теперь, если я иду:

MyDouble zero = 0;

Console.WriteLine (zero * longOperation()); //longOperation is still Func<double>

Получаю:

cast to MyDouble            
called longOperation        <-- want to avoid this (it printed from longOperation body)
cast to double             
cast to MyDouble
* operator call
cast to double   
0

Но, как вы можете видеть, longOperation вычисляется задолго до того, как вызывается перегруженный оператор, и я не могу заменить один из параметров Func или Expression, чтобы сделать его ленивым.

Ответ 1

Проблема с вашим классом-оболочкой MyDouble заключается в том, что вы используете ее, вызывая непосредственно longOperation. Поскольку * не имеет короткого замыкания, он будет вызываться напрямую.

Вместо этого вы можете просто сделать ваш оберткой accept Func<double> как второй параметр вместо самого двойного значения. Таким образом, он будет работать как функция ShortCircuitMultiply:

public static MyDouble operator *(MyDouble left, Func<double> right)
{
    return Math.Abs(left.value) < epsilon ? new MyDouble(0) : new MyDouble(left.value * right());
}

Затем вы будете использовать его следующим образом:

MyDouble x = 0;
Console.WriteLine(x * LongOperation);

И даже цепочки:

MyDouble x = 5;
Console.WriteLine(x * OperationReturingZero * LongOperation);

Полный пример

class Program
{
    static void Main()
    {
        MyDouble x = 0;
        Console.WriteLine(x * LongOperation);

        MyDouble y = 5;
        Console.WriteLine(y * OperationReturningZero * LongOperation);

        Console.ReadLine();
    }

    private static double LongOperation()
    {
        Console.WriteLine("LongOperation");
        return 5;
    }

    private static double OperationReturningZero()
    {
        Console.WriteLine("OperationReturningZero");
        return 0;
    }
}

class MyDouble
{
    private static double epsilon = 0.00000001;
    private double value;

    public MyDouble(double value)
    {
        this.value = value;
    }

    public static MyDouble operator *(MyDouble left, Func<double> right)
    {
        Console.WriteLine("* (MyDouble, Func<double>)");
        return Math.Abs(left.value) < epsilon ? new MyDouble(0) : new MyDouble(left.value * right());
    }

    public static MyDouble operator *(MyDouble left, MyDouble right)
    {
        Console.WriteLine("* (MyDouble, MyDouble)");
        return new MyDouble(left.value * right.value);
    }

    public static implicit operator double(MyDouble myDouble)
    {
        Console.WriteLine("cast to double");
        return myDouble.value;
    }

    public static implicit operator MyDouble(double value)
    {
        Console.WriteLine("cast to MyDouble");
        return new MyDouble(value);
    }
}

Вывод:

cast to MyDouble
* (MyDouble, Func<double>)
cast to double
0
cast to MyDouble
* (MyDouble, Func<double>)
OperationReturningZero
* (MyDouble, Func<double>)
cast to double
0

Ответ 2

Нет никакого способа легко сделать то, что вы хотите. Язык С# - очень "нетерпеливый" язык, поскольку он всегда оценивает операнды до того, как он запускает операторы, даже если вы заметили, что вы могли бы пропустить один, зная другой. Единственными исключениями являются ? : и его эквиваленты &&, || и ??. (Все они могут быть уменьшены до ? :.)

Как вы правильно заметили, вы можете достичь лености через использование лямбда; a Func<T> представляет a T, который будет вычисляться по требованию. Но, как вы также правильно заметили, синтаксис для этого довольно тяжелый.

Рассмотрите возможность написания вашей программы в Haskell, если у вас должна быть ленивая арифметика. Это очень лениво, и я понимаю, что очень легко определить собственную семантику оператора. F # также является опцией и, вероятно, легче изучить для программиста на С#.

Ответ 3

Ну, вы можете написать метод расширения для double, но я не уверен, действительно ли это то, что вы ищете.

Тогда у вас может быть такой код:

double z = someNumberThatMightBeZero();
double r = z.Times(number);

где number - метод, возвращающий двойной.

using System;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            double z = zero();
            double r = z.Times(number);
            Console.WriteLine(r);
        }

        static double zero()
        {
            return 0;
        }

        static double number()
        {
            Console.WriteLine("in number()");
            return 100;
        }
    }

    public static class DoubleExt
    {
        public static double Times(this double val, Func<double> anotherValue)
        {
            const double epsilon = 0.00000001;
            return Math.Abs(val) < epsilon ? 0 : val * anotherValue();
        }
    }
}

Ответ 4

Ближайшее, что я вижу, это что-то вроде этого:

  struct LazyDouble {
    Func<double> Func;

    public LazyDouble(Func<double> func) : this() { Func = func; }

    public static implicit operator LazyDouble (double value) { 
      return new LazyDouble(()=>value); 
    }

    public static LazyDouble operator * (LazyDouble lhs, LazyDouble rhs) {
      var lhsValue = lhs.Func();
      if ( lhsValue == 0)
        return 0;
      else 
        return new LazyDouble(() => lhsValue * rhs.Func());
    }
    // other operators as necessary
  }