Реализация связанных расчетов

6yrop

Начну с примера. Пример из 2-x сущностей и 3-х формул будет не так ярко высвечивать проблему, поэтому приведу более объемный пример из 4-x сущностей и 10-и формул. Ссылка на pdf-файл с примером.
Как вы реализовываете подобного рода расчеты в своих проектах?
Хочется просто писать формулы в произвольном порядке и больше ни о чем не думать.
В computer science это известно под термином dataflow.
Моя реализация примера. Ссылка на сам проект Reactive Relation.
Наконец-то, C# перестает уступать Excel-ю в плане выполнения расчетов :).

Helga87


Аццко. 10 WTF/min, вспоминая единственно верную метрику

Dasar

Как вы реализовываете подобного рода расчеты в своих проектах?
пытаемся свести к следующему:
Expression<decimal> PartTotal
{
get {return Quantity * Price;}
}
поверх которого уже строится dataflow

6yrop

пытаемся свести к следующему:
Похоже, вы идете тем же путем что и проекты:
Continuous LINQ/Bindable LINQ/Obtics/LiveLinq (см. codeplex.com)
То есть через парсинг expression tree.
Я отказался от этого еще на ранней стадии. Expression tree приносят слишком жесткие ограничения. В моем подходе все инструменты языка C# остаются доступными, я это отметил в документации:
Замечательной особенностью проекта Reactive Relation является то, что он не приносит собой никаких ограничений. Все инструменты языка C# остаются доступными – вычисления можно выделять в отдельные методы или классы. Фактически Reactive Relation реализует механизм выноса абстрактной логики пересчета с учетом абстрактной реляционной структуры данных. Указанный механизм реализован с помощь обычных средств языка без анализа дерева выражений (expression tree). Дерево выражений используется только в качестве ссылки на свойство (property expression).
http://propertyexpression.codeplex.com/Project/Download/File...

6yrop

Аццко. 10 WTF/min, вспоминая единственно верную метрику
По поводу синтаксиса см. 4-ый раздел документации проекта http://propertyexpression.codeplex.com/Project/Download/File...

Dasar

Expression tree приносят слишком жесткие ограничения
какие, например?

6yrop

какие, например?
например, не работает обычный extract method

Dasar

Я отказался от этого еще на ранней стадии.
т.е. у тебя недо-dataflow, который не пересчитывает выходы при изменении входов?

Helga87

А почему ты не хочешь вместо
binder[@Price] = it => it[@Quote][@PartSubtotal].Percent(it[@PartPercent]) + it[@Quote][@LaborSubtotal].Percent(it[@LaborPercent]) + it[@Fix];

дать возможность написать
engine.add("Price = (Quote.PartSubtotal * PartPercent + Quote.LaborSubtotal * (LaborPercent + Fix) / 100");

При регистрации формулы парсить и переводить уже в твой вид инъекцией кода. Таким образом, это хотя и не compile-time check, но startup check (не путать с runtime check).
Достоинство этого улучшения еще и в том, что все формулы одного flow можно собрать в одном очень компактном текстовом файле или фрагменте так, что вся логика будет легко видна и модифицируема.

6yrop

т.е. у тебя недо-dataflow, который не пересчитывает выходы при изменении входов?
по-видимому, не совсем понимаю тебя. Формулки, конечно, пересчитываются при изменении аргументов.

Dasar

При регистрации формулы парсить и переводить уже в твой вид инъекцией кода. Таким образом, это хотя и не compile-time check, но startup check (не путать с runtime check).
8. ПП должен легко модифицироваться (из п.2а)
...
b) ПП должен быть достаточно формализовано (должна оставаться возможность автоматического\автоматизированного рефакторинга)

Dasar

по-видимому, не совсем понимаю тебя. Формулки, конечно, пересчитываются при изменении аргументов.
а как ты понимаешь, что ее надо пересчитать?

Helga87

(должна оставаться возможность автоматического\автоматизированного рефакторинга)
Find/Replace?
Какие еще рефакторинги формул, кроме переименования аргументов известны? Почему в Excel люди сидят и не страдают от отсутствия рефакторинга в этом месте?

6yrop

При регистрации формулы парсить и переводить уже в твой вид инъекцией кода. Таким образом, это хотя и не compile-time check, но startup check (не путать с runtime check).
т.е. ты предлагаешь свой язычок выражений? Таких решений уже много, например, вот http://flee.codeplex.com/
Но хочется оставаться "внутри" основного языка, в частности, C#.

Dasar

> Какие еще рефакторинги формул, кроме переименования аргументов известны?
все те же самые, которые доступны для кода (выделение метода, замена аргументов, перенос метода между классами и т.д.)
> Почему в Excel люди сидят и не страдают от отсутствия рефакторинга в этом месте?
во-первых, откуда известно что они не страдают?
во-вторых, а они не знают что можно по другому.
в-третьих, а что на Excel-е уже делают хорошие, стабильные, масштабируемые решения?

6yrop

Find/Replace?
Какие еще рефакторинги формул, кроме переименования аргументов известны? Почему в Excel люди сидят и не страдают от отсутствия рефакторинга в этом месте?
На самом деле, от Excel-я есть существенное отличие. Rective Relation поддерживает проходы по связям один-ко-многим в обе стороны.

6yrop

> Почему в Excel люди сидят и не страдают от отсутствия рефакторинга в этом месте?
во-первых, откуда известно что они не страдают?
во-вторых, а они не знают что можно по другому.
в-третьих, а что на Excel-е уже делают хорошие, стабильные, масштабируемые решения?
да, под этим тоже подписываюсь. Вообще, аргумент Красина по поводу Excel-я какой-то странный... Могу объяснить это только тем, что человек далек от проблематики.

6yrop

а как ты понимаешь, что ее надо пересчитать?
при регистрации формула говорит от чего она зависит.

Dasar

при регистрации формула говорит от чего она зависит.
т.е. это надо говорить руками?

6yrop

т.е. это надо говорить руками?
нет, конечно, иначе ради чего это все.
Все что надо сделать это написать формулы как тут
В сорцах проекта этот пример есть называется Demo. И unit-тесты с пересчетом есть.

Dasar

например, не работает обычный extract method
что значит не работает?
был код

static int X;
static Expression<Func<int>> X2
{
get
{
return => X + 5;
}
}
static Expression<Func<int>> Y
{
get
{
return => X + Call(X2) * 10;
}
}


static T Call<T>(Expression<Func<T>> t)
{
return t.Compile;
}


хотим вынести X2 * 10 как отдельный метод - жмем extract method

static Expression<Func<int>> Y
{
get
{
return => X + NewMethod;
}
}

private static int NewMethod
{
return Call(X2) * 10;
}

заменяем руками int на Expression<Func<int>>, а NewMethod на Call(NewMethod
можно чуток потрахаться со студией, и сделать чтобы это делалось по какой-нибудь кнопке

Dasar

Все что надо сделать это написать формулы как тут
т.е. все входные переменные должны прогонятся через шаманство it => it._
но это же легко забыть...
и фиг это автоматически проверишь...
с expression-ами можно требовать чтобы все члены были оформлены как expression-ы (допустим, за исключением специально помеченных)

Dasar

скорость кстати тестил?

6yrop

т.е. все входные переменные должны прогонятся через шаманство it => it._
но это же легко забыть...
и фиг это автоматически проверишь...
не забудешь, по другому ни интелесен ни компилятор не дадут.

6yrop

скорость кстати тестил?
нет. Пефоманс я отложил, но там можно абсолютно все кешировать на уровне приложения, т.е. фактически все сведется к поиску по хештейблу и прямым вызовам методов.

6yrop

X + Call(X2) * 10;
вот, видишь, ты тут не можешь просто написать "X2" надо писать "Call(X2)"
 
заменяем руками int на Expression<Func<int>>, а NewMethod на Call(NewMethod
можно чуток потрахаться со студией, и сделать чтобы это делалось по какой-нибудь кнопке

Т.е. при extract method я должен держать в голове что это Expression-ы, и у них свои правила. А могу просто забыть сделать вот эти вставки. Все сбилдиться. А найти такую ошибку чрезвычайно сложно, поскольку надо пройти тест, где меняется X2.
B тут опять Call надо вставлять.
На первый взгляд, вот такое смешивание обычного кода и Expression<D> запутывает.

6yrop

А когда эти экспрешены обрабатываются? Т.е. можешь описать вкраце общий цикл?

Dasar

На первый взгляд, вот такое смешивание обычного кода и Expression<D> запутывает.
добавление it => it., _ => _, Mult вместо * и т.д. - запутывает еще больше.

Dasar

А когда эти экспрешены обрабатываются? Т.е. можешь описать вкраце общий цикл?
окончательного решения еще нет, но что-то типа следующего

abstract class A
{
public int B {get;set;}
public int C {get;set;}
public abstract int D {get;}
}

public static class AHlp
{
public static Expression<int> D(this A a)
{
return => a.b + a.c;
}
}

void Main
{
//DataFlow создает наследника через emit, и через emit же добавляет метод D
var dataflow = DataFlow.Create<A,AHlp>

A a = dataflow.GetAs<A>

var value = dataflow.GetValueA a) => a.D);

var changeableValue = dataflow.GetChangeableValueA a) => a.D);
}

6yrop

завязка на совпадение имен методов D?

6yrop

добавление it => it., _ => _, Mult вместо * и т.д. - запутывает еще больше.
с этим путаницы никакой нет, правило тупое и простое — вызов свойства идет через синтаксис "_=>_.", ни о чем другом думать не надо. Соблюдение этого правила компилятор и интелесенс проверяют.

Dasar

завязка на совпадение имен методов D?
в этом примере - да.
но в окончательном решении еще не знаю

6yrop

в этом примере - да
так же там нет контроля того, что метод AHlp.D принимает параметр типа A. Еще лучше, чтобы тип параметра выводился, а не указывался ручками

6yrop

Синтаксис можно улучшить через генерацию простых оберточных extension методов. Для каждого свойства дата-объекта надо сгенерировать пару методов. Пример для свойства PartTotal класса QuotePart):

public static IPropertyValue<decimal> PartTotal(this IPropertyHolder<QuotePart> it)
{
return it._(_ => _.PartTotal);
}

public static void PartTotal(
this IBinder<IEmpty, QuotePart> binder,
Func<IPropertyBindContext<IEmpty, QuotePart, decimal>, IValue<decimal>> func)
{
binder._(_ => _.PartTotal, func);
}

Тогда формулы для примера из первого поста будут выглядеть следующим образом.
Теперь необычно смотрятся только методы вместо свойств и методы вместо операторов “+”, “*”. Но джависты же живут без этих вкусностей :). На самом деле, идеальный синтаксис не является самоцелью, но об этом чуть позже.
Пока это в статусе эксперимента. Код находится в ветке DevBranches.

6yrop

что значит не работает?

под термином "extract method" я в первую очередь имел ввиду обычный ручной прием выделения общего куска кода в отдельный метод.
Ты привел слишком простой пример — метод без параметров. Как у тебя будет работать вот такое введение метода NewMethod в формуле из моего примера

Price = NewMethod(this.Quote, this)
+ ...

decimal NewMethod(Qoute qoute, QuoteOtherExpense expense)
{
return qoute.PartSubtotal * expense.PartPercent / 100;
}

При анализе экспрешена, имхо, не отличишь параметр метода от локальной переменой. Или у тебя нельзя внутри метода использовать локальные переменные?

6yrop

Отмечу, что и C# 3.0 привествовал .
Но его замечание относительно типизированного датабайдинга, действительно, оказалось полезным, и проект стал лучше. За что -у большое спасибо! :D
Оставить комментарий
Имя или ник:
Комментарий: