Про типы и логику - С++
От: jazzer Россия Skype: enerjazzer
Дата: 10.02.15 08:11
Оценка: 171 (13) +2
Сначала хотел ответить тут
Автор: Mamut
Дата: 05.02.15
, но сообщение получилось слишком длинным, плюс оно несколько шире, чем обсуждаемая задача.

В каком-то смысле это попытка реализации зависимых типов на С++, я ее начал писать очень давно (еще до С++11, так что кое-что будет излишне многословно), когда заинтересовался бурным обсуждением зависимых типов здесь; но вот с подачи Мамута подвернулась относительно реальная задача, на которой можно проверить мои построения.

(На всякий случай, я этим постом не утверждаю, что С++ лучше Агды или любого другого языка — наоборот, в тех языках все должно выражаться намного прямее и короче.)

M>Мне просто действительно интересно. Многие утверждают, что типы помогают (чуть ли не) во всем, вплоть до того, что «тайпчекер проверяет именно логику».


M>У меня есть простейшая задача, мне просто интересно, как она будет решаться типами, и/или как типы могут помочь ее решить. Самое главное, если вся логика упаковывается в типы, каким образом проверяется правильность реализации этой логики на типах?


Нет никакой реализации логики (т.е. реальных действий) на типах. На типах реализуются ограничения, которым должен удовлетворять код. При этом в коде реализации самих ограничений могут быть ошибки, естественно.

Не рассчитывая на честное судейство, покажу то, что у меня есть (весь код ниже рабочий, ошибки компиляции реальны).
Пусть у нас есть ордер:
struct Order
{
  int id;
  bool shipped, prepaid_, risk;
  double amount;

  // эти строчки просто для разных способов определения свойств ордера, помимо o.shipped
  bool prepaid() const { return prepaid_; }
  friend bool has_risk(const Order& o) { return o.risk; }
};

Это базовый тип, и на него мы будем навешивать свойства. Тип ордера со свойствами (в каком-то виде это зависимый тип) будет выглядеть так: Type<Order, Properties>, где Properties — это просто compile-time map, например Type<Order&, boost::mpl::map<_{HasRisk:false_ }, _{Shipped:false_}, _{Prepaid:false_}> > (тут, для краткости, _{K:V} — это boost::mpl::pair<K,V>). Метафункции для добавления/убирания/изменения/проверки свойств пишутся элементарно, так что я их тут не привожу.

Теперь делаем функцию increase_amount(Order), которая будет проверять, обладает ли ордер нужными свойствами. Проверку можно написать прямо в определении функции, а можно вынести отдельной метафункцией (потому что автоматически появтся возможность написать для нее юнит-тесты):
struct HasRisk; // Свойство, которое мы хотим проверять в compile time

// предикат (CheckExact - это проверка того, что свойство HasRisk строго равно false.
//           понятно, что можно сделать другие предикаты, типа AtLeast и т.п.)
template<class O> using can_increase_amount = CheckExact< O, HasRisk, bm::false_ >;

//разрешаем (enable_if) функцию только если проверка прошла
template<class O>
typename boost::enable_if< can_increase_amount<O>, O >::type
increase_amount(O o, double incr)
{
  std::cout << "increasing amount of order " << unwrap(o) << " by " << incr << std::endl;
  unwrap(o).amount += incr;
  return o;
}

// ругнемся, чтоб программеру было понятно, что он написал что-то не то.
// в принципе, эту функцию можно и не писать, но тогда сообщение об ошибке 
// будет слишком загадочным ("функция не найдена")
template<class O>
typename boost::disable_if< can_increase_amount<O>, O >::type
increase_amount(O o, double incr)
{
  BOOST_MPL_ASSERT_MSG(false, YOU_CANT_INCREASE_AMOUNT_FOR_SUCH_ORDER, (O));
  return o;
}


Теперь пишем функцию, в которой мы хотим увеличивать amount ордера (это реальный работающий код, если что):
void try_to_change_amount(Order& o)
{
  //increase_amount(o, 2.71);  - не скомпилируется: мы ничего не знаем о риске
  
  PROP_IF(o, has_risk(o), HasRisk) {
    //increase_amount(o, 2.71); - не скомпилируется: ордер помечен как рисковый
  }
  PROP_ELSE(o) {
    increase_amount(o, 2.71);
  };

Что делает PROP_IF — он навершивает свойство HasRisk в соответствии с проверкой has_risk(o), и дальше внутри if и else объект o будет обладать этим свойством. Это приведет к тому, что вызов increase_amount скомпилируется только внутри ветки else. Если раскомментировать вызов внутри ветки if или вызов за пределами if — будет ошибка компиляции:
error: ************::YOU_CANT_INCREASE_AMOUNT_FOR_SUCH_ORDER::************(Type<Order&, mpl::map<_{HasRisk:true_ }> >)

Это была реализация требования

  • если товар помечен, как risk, то изменять сумму нельзя


  • Теперь добавим следующее требование:

  • если заказ отправлен, нельзя увеличивать, можно уменьшать

  • В коде это приведет просто к добавлению этого требования к предикату:
    struct Shipped; // новое свойство
    
    template<class O> using can_increase_amount =
        bm::and_< CheckExact< O, HasRisk, bm::false_ >
                , CheckExact< O, Shipped, bm::false_ > // новое ad-hoc требование
                >;

    запускаем компиляцию — опаньки, не компилируется:

    error: ************::YOU_CANT_INCREASE_AMOUNT_FOR_SUCH_ORDER::************(Type<Order&, mpl::map<_{HasRisk:false_ }> >)

    Не компилируется потому, что требуется проверить свойство Shipped, а его нет.
    Идем туда, где не компилируется, добавляем соответствующий PROP_IF:
    void try_to_change_amount(Order& o)
    {
      PROP_IF(o, has_risk(o), HasRisk) {
        // сообщить об ошибке
      }
      PROP_ELSE(o) {
        // increase_amount(o, 2.71); -- больше не работает: надо проверять Shipped
        PROP_IF(o, o.shipped, Shipped ) {
          //increase_amount(o, 2.71); // (*)
          // сообщить об ошибке
        }
        PROP_ELSE(o) {
          increase_amount(o, 2.71);
        };
      };

    если раскомментировать вызов increase_amount внутри Shipped==true (*) — получим, опять же, ошибку компиляции:

    error: ************::YOU_CANT_INCREASE_AMOUNT_FOR_SUCH_ORDER::************(Type<Order&, mpl::map<_{HasRisk:false_ }, _{Shipped:true_}> >)

    Вместо раскомментирования может быть неудачный copy-paste, например, или неудачный merge.

    Надеюсь, идея понятна. Дополнительные требования навешиваются аналогично. Если элементарных свойств/требований становится слишком много, то можно их просто упаковать в мета-свойство IncreaseAmountOK, которое будет взводиться теперь уже обычной функцией bool can_increase_amount(Order, amount) (да, тут в проверке появился amount; и аналогично появятся какие-то другие внешние вещи из конфигурации, БД и прочая) — и таким образом мы вернемся к простому первоначальному варианту, только вместо HasRisk/has_risk(o) будет это мета-свойство и функция-предикат, соответственно.

    Естественно, это не убережет нас от ошибок в реализации can_increase_amount (или has_risk выше) — но компилятор не пропустит вызова increase_amount для ордера, у которого не прошла соответствующая проверка, какой бы они не была. Компилятор теперь будет гарантировать, что increase_amount может быть вызван только для ордера со свойством IncreaseAmountOK=true:
    template<class O>
    typename boost::enable_if< IncreaseAmountOK<O>, O >::type
    increase_amount(O o, double incr);
    
    void try_to_change_amount(Order& o, double new_amt)
    {
      PROP_IF(o, can_increase_amount(o, new_amt), IncreaseAmountOK) {
        increase_amount(o, new_amt);
      };
    }

    попытка вставить вызов increase_amount в любое другое место за пределами сработавшего if вызовет ошибку компиляции. Именно благодаря типам.

    Более того, можно в PROP_IF навесить свойство не только на ордер, но и на new_amt, с которым прошла проверка, или даже на пару order+new_amt — чтобы было невозможно позвать increase_amount с другой суммой new_amt2, которая проверку не проходила:
    template<class OrderAndAmount>
    typename boost::enable_if< IncreaseAmountOK<OrderAndAmount>, OrderAndAmount >::type
    increase_amount(OrderAndAmount oa)
    {
      unwrap(oa.order).amount += oa.amount;
    }
    
    void try_to_change_amount(Order& o, double new_amt)
    {
      PROP_IF(order_and_amount, can_increase_amount(o, new_amt), IncreaseAmountOK) {
        increase_amount(order_and_amount);
      };
    }


    И не сказать, что кода стало на 100500 порядков больше, чем с решением в лоб.
    Обрати внимание, логика в смысле того, что происходит внутри change_amount, внутри can_increase_amount, внутри has_risk, и даже внутри try_to_change_amount в смысле вызовов проверок/предикатов, генерации разнообразных сообщений об ошибках входных данных и прочая, на типах не пишется.
    На типах пишется логика ограничений: когда, что и как может быть вызвано.
    И проверяет их компилятор за нас, автоматически, и не пропустит ни строчки кода, не соответствующего требуемым (или изменившимся) ограничениям.
    jazzer (Skype: enerjazzer) Ночная тема для RSDN
    Автор: jazzer
    Дата: 26.11.09

    You will always get what you always got
      If you always do  what you always did
    Отредактировано 10.02.2015 8:44 jazzer . Предыдущая версия .
    Re: Про типы и логику - С++
    От: artelk  
    Дата: 10.02.15 09:36
    Оценка:
    Задача стала интересна сама по себе, безотносительно Мамута и попыток ему что-то объяснить или доказать.
    Re[2]: Про типы и логику - С++
    От: jazzer Россия Skype: enerjazzer
    Дата: 10.02.15 09:53
    Оценка: 21 (1)
    Здравствуйте, artelk, Вы писали:

    A>Задача стала интересна сама по себе, безотносительно Мамута и попыток ему что-то объяснить или доказать.


    Ну да, меня зависимые типы очаровали сразу, в первую очередь тем, что я никак не мог понять, можно ли их в принципе реализовать в С++, где все должно быть известно во время компиляции. Судя по всему, ограниченно — можно, вот таким вот способом.
    Я это у себя тогда давно тестил на числах (ну чтоб там нельзя было в принципе взять корень из отрицательного числа, а логарифм — из неположительного), но никак не мог прийти к удобному PROP_IF — все время очень много писанины получалось.
    А с лямбдами С++14 получилось вот, и даже без макросов не так уж все страшно, главная фишка — переупаковка объекта с добавлением вычисленного свойства:
    // no idea why parens are needed around, probably a GCC bug
    #define PROP_IF(object, getter, property) \
      if (auto ifres = \
            bool_result<property>( object \
                                 , ([&object = unwrap(object)]{return getter;})() /*this is just a boolean expr*/ \
                                 ) ) run_lambda*[object = ifres.yes()]
    
    #define PROP_ELSE(object) ; else run_lambda*[object = ifres.no()]

      дальнейшие потроха
    struct RunLambda
    {
      template<class F>
      void operator*(F f) const { f(); }
    } run_lambda;
    
    template< class T, class Prop>
    struct bool_result_t
    {
      T& object;
      bool val;
      
      auto yes() const { return add_prop<Prop, bm::true_ >(object); }
      auto no()  const { return add_prop<Prop, bm::false_>(object); }
      explicit operator bool() const { return val; }
    };
    
    template< class Prop, class T>
    auto bool_result( T& x, bool val ) { return bool_result_t<T, Prop>{ x, val }; }
    jazzer (Skype: enerjazzer) Ночная тема для RSDN
    Автор: jazzer
    Дата: 26.11.09

    You will always get what you always got
      If you always do  what you always did
    Re: Про типы и логику - С++
    От: Mamut Швеция http://dmitriid.com
    Дата: 10.02.15 09:55
    Оценка: :)
    Вот это ответ так ответ

    Спасибо.

    Возможно у меня еще будут вопросы, но я должен перечитать сообщение еще раз, а у меня скоро обед


    dmitriid.comGitHubLinkedIn
    Re: Про типы и логику - С++
    От: WolfHound  
    Дата: 10.02.15 14:51
    Оценка:
    Здравствуйте, jazzer, Вы писали:

    В этом решении мне не нравится то что проверка не привязана к свойству.
    Если переделать PROP_IF так:
    #define PROP_IF(property, object, ...) \
      if (auto ifres = \
            bool_result<property>( object \
                                 , property ## CheckProp(object, __VA_ARGS__) \
                                 ) ) run_lambda*[object = ifres.yes()]

    Можно будет писать так:

    struct HasRisk;
    
    template<typename O>
    bool HasRiskCheckProp(O& o)
    {
      return has_risk(unwrap(o));
    }
    
    struct Shipped;
    
    template<typename O>
    bool ShippedCheckProp(O& o)
    {
      return unwrap(o).shipped;
    }
    
    void try_to_change_amount(Order& o)
    {
      PROP_IF(HasRisk, o) {
        // сообщить об ошибке
      }
      PROP_ELSE(o) {
        // increase_amount(o, 2.71); -- больше не работает: надо проверять Shipped
        PROP_IF(Shipped, o) {
          //increase_amount(o, 2.71); // (*)
          // сообщить об ошибке
        }
        PROP_ELSE(o) {
          increase_amount(o, 2.71);
        };
      };

    struct IncreaseAmountOK;
    
    template<typename O>
    bool IncreaseAmountOKCheckProp(O& o, double new_amt)
    {
      return can_increase_amount(unwrap(o), new_amt);
    }
    
    void try_to_change_amount(Order& o, double new_amt)
    {
      PROP_IF(IncreaseAmountOK, o, new_amt) {
        increase_amount(o, new_amt);
      };
    }
    ... << RSDN@Home 1.2.0 alpha 5 rev. 62>>
    Пусть это будет просто:
    просто, как только можно,
    но не проще.
    (C) А. Эйнштейн
    Re[2]: Про типы и логику - С++
    От: Evgeny.Panasyuk Россия  
    Дата: 10.02.15 15:02
    Оценка: +1
    Здравствуйте, WolfHound, Вы писали:

    WH>В этом решении мне не нравится то что проверка не привязана к свойству.

    WH>Если переделать PROP_IF так:
    WH>Можно будет писать так:

    Тогда уж лучше проверку ассоциировать не с именем свойства, а с типом, например:
    struct HasRisk;
    
    template<typename O>
    bool CheckProp(HasRisk, O& o);
    
    // or:
    
    template<typename O>
    bool CheckProp(Property<HasRisk>, O& o);
    Re[2]: Про типы и логику - С++
    От: jazzer Россия Skype: enerjazzer
    Дата: 10.02.15 15:47
    Оценка:
    Здравствуйте, WolfHound, Вы писали:

    WH>Здравствуйте, jazzer, Вы писали:


    WH>В этом решении мне не нравится то что проверка не привязана к свойству.

    WH>Если переделать PROP_IF так:

    Да можно, конечно.
    Можно просто засунуть проверку внутрь самого свойства, чтоб сущность была self-contained (не знаю, как по-русски; в общем, чтоб все в одном месте было):
    struct HasRisk
    {
      template<typename O>
      bool operator()(O& o) const {
        return has_risk( unwrap(o) );
      }
    };

    Или даже так, чтоб напрямую в if можно было использовать:
    struct HasRisk: bool_result_t {...};


    Моей задачей тут было доказать саму возможность такого построения кода на С++ и примерно набросать фреймворк, это ни в коем случае не законченное и вылизанное решение.

    Тем более что свойства не только булевские бывают — можно еще и switch/case устроить, унификацию (это то, с чего я начал до задачи Мамута — когда NonNegative+Positive превращается в Positive).
    jazzer (Skype: enerjazzer) Ночная тема для RSDN
    Автор: jazzer
    Дата: 26.11.09

    You will always get what you always got
      If you always do  what you always did
    Re: Про типы и логику - С++
    От: AlexRK  
    Дата: 04.03.15 16:30
    Оценка:
    Здравствуйте, jazzer, Вы писали:

    J>Теперь пишем функцию, в которой мы хотим увеличивать amount ордера (это реальный работающий код, если что):

    J>
    J>void try_to_change_amount(Order& o)
    J>{
    J>  //increase_amount(o, 2.71);  - не скомпилируется: мы ничего не знаем о риске
      
    J>  PROP_IF(o, has_risk(o), HasRisk) {
    J>    //increase_amount(o, 2.71); - не скомпилируется: ордер помечен как рисковый
    J>  }
    J>  PROP_ELSE(o) {
    J>    increase_amount(o, 2.71);
    J>  };
    J>


    У меня такой вопрос, как у человека, не очень сильно разбирающегося в метапрограммировании на С++. Проверка PROP_IF может стоять не в той функции, в которой мы вызываем increase_amount, а где-то выше по стеку?
    Re[2]: Про типы и логику - С++
    От: jazzer Россия Skype: enerjazzer
    Дата: 05.03.15 10:36
    Оценка: +1
    Здравствуйте, AlexRK, Вы писали:

    J>>Теперь пишем функцию, в которой мы хотим увеличивать amount ордера (это реальный работающий код, если что):

    J>>void try_to_change_amount(Order& o)


    ARK>У меня такой вопрос, как у человека, не очень сильно разбирающегося в метапрограммировании на С++. Проверка PROP_IF может стоять не в той функции, в которой мы вызываем increase_amount, а где-то выше по стеку?


    Конечно. Если ты хочешь, чтоб try_to_change_amount принимала ордер с произвольно навешенными на него свойствами, надо просто объявить ее шаблоном:
    template<class O>
    void try_to_change_amount(O& o);
    jazzer (Skype: enerjazzer) Ночная тема для RSDN
    Автор: jazzer
    Дата: 26.11.09

    You will always get what you always got
      If you always do  what you always did
    Re[3]: Про типы и логику - С++
    От: AlexRK  
    Дата: 05.03.15 12:29
    Оценка:
    Здравствуйте, jazzer, Вы писали:

    J>Конечно. Если ты хочешь, чтоб try_to_change_amount принимала ордер с произвольно навешенными на него свойствами, надо просто объявить ее шаблоном:

    template<class O>
    void try_to_change_amount(O& o);


    А если мы — по ошибке — не объявим ее шаблоном?
    Re[4]: Про типы и логику - С++
    От: jazzer Россия Skype: enerjazzer
    Дата: 05.03.15 12:48
    Оценка: 6 (1)
    Здравствуйте, AlexRK, Вы писали:

    ARK>А если мы — по ошибке — не объявим ее шаблоном?


    Ну тогда зависит от того, как у тебя написан класс со свойствами. У меня он написан с поддержкой неявного преобразования в обычный Order (так как возможны функции, которым пофиг на свойства, например, функция печати ордера в лог), но можно написать и без и тогда будет ошибка компиляции.
    jazzer (Skype: enerjazzer) Ночная тема для RSDN
    Автор: jazzer
    Дата: 26.11.09

    You will always get what you always got
      If you always do  what you always did
    Re: Про типы и логику - С++
    От: Ikemefula Беларусь http://blogs.rsdn.org/ikemefula
    Дата: 07.03.15 07:07
    Оценка:
    Здравствуйте, jazzer, Вы писали:

    J>На типах пишется логика ограничений: когда, что и как может быть вызвано.

    J>И проверяет их компилятор за нас, автоматически, и не пропустит ни строчки кода, не соответствующего требуемым (или изменившимся) ограничениям.

    У тебя и проверка компилятором и проверки в рантайме. А хочется только первое без второго. То есть, что бы никаких проверок рантайме не было.
    Re[2]: Про типы и логику - С++
    От: AlexRK  
    Дата: 07.03.15 07:46
    Оценка:
    Здравствуйте, Ikemefula, Вы писали:

    I>У тебя и проверка компилятором и проверки в рантайме. А хочется только первое без второго. То есть, что бы никаких проверок рантайме не было.


    Компилятор проверяет не значения (которые он проверить не может), а то, написал ли программист нужную проверку в рантайме.

    I>То есть, что бы никаких проверок рантайме не было.


    Этого быть в принципе не может. Ну если только все входные данные известны на момент компиляции.
    Re: Про типы и логику - С++
    От: мыщъх США http://nezumi-lab.org
    Дата: 07.03.15 07:59
    Оценка: 3 (1)
    Здравствуйте, jazzer, Вы писали:



    J> Нет никакой реализации логики (т.е. реальных действий) на типах. На типах реализуются ограничения,

    J> которым должен удовлетворять код. При этом в коде реализации самих ограничений могут быть ошибки, естественно.
    не совсем понятно где нет реализации логики. у вас или вообще на типах. если вообще на типах то с этим сложно согласиться. давайте возьмем очень простой пример. a = b + c; пускай это строки. тут происходит неявное выделение памяти, которое на си мы бы делали руками. это -- логика.

    некоторые языки типа js могут вести себя неожиданно для си-программиста и вместо того, чтобы объединять строки -- переводят из строки в число (если переводится) и складывать их как числа. это уже некислый слой логики (хотя и неожиданный порой и для самих js программистов).

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

    тип "градусы ххх" (цельсия, фаренгейта, кельвина) так же может автоматически складывать c = a + b даже если а это по цельсию, а b -- по фаренгейту. более того, тип c, в котором записан результат, интересуется какой это именно градус только при выводе на экран. тогда мы уже явно указываем какие градусы нам нужны. как, впрочем, и при иницилизации (но тут как бы очевидно).

    идем дальше. конструкция for line in lines может читать файл по строкам. или массив по элементам. и где же эта логика? так она в типе и реализована. чудес не бывает. реализацию можно прятать, но она таки должна быть. однако, именно это позволяет одной и той же конструкцией и читать массив, и файл, и... да что угодно! хоть базу данных. при этом в последнем случае реализация "скрытой" логики может быть совсем уж нетривиальной. у меня такая конструкция с одинаковой легкостью выводит на экран строку по символам или дизассемблирует хоть x86, хоть jvm. дизассемблер (или парсер) имеет сложную реалиацию, но "каркас" об этом не знает. каркасу абсолютно все равно массив ли это байт (символов) или байт-код.

    так вот, если вчера было ноль по цельсию, а сейчас 14 по фаренгейту и я хочу узнать дельту и эту дельту выводит код вида a — b, то что же это такое как не реализация _логики_ на _типах_ ?! потому что а это один тип, b -- другой. при вычитании одного из другого создается третий тип — кельвина, в котором и хранится результат. а вот при отображении его на экране... ну просто print (a-b) выдаст ошибку. нужно писать что-то типа print "температура %Ц" % (a — b), где %Ц у нас задает формат вывода -- цельсия.

    мы получаем код, который легко расширять, который легко писать и легко сопровождать. если это не реализация логики на типах, то что это? это ни разу не есть ограничения операций над типами. это ни разу не более строгие проверки сложения целься с фаренгейтами. это как раз решение проблемы: что делать (элегантно) если у нас температура постоянно то в одном, то в другом, то вообще в третьем...


    вот, кстати, пример из жизни. ВРЕМЯ. оно же, сволочь, у всех нас разное. мой вариант решения проблемы (чтобы избежать ошибок). если мы реализуем _логику_ на различных типах времени, то мы только указаываем типы. и раз уж зашел разговор о времени -- когда отмечать сто пятьсот лет крещения руси? сколько с той поры было календарных реформ? грубо говоря, если летописцы говорят, что событие произошло 1 декабря 865 года в риме, а нам нужно сколько с той поры прошло до 1 декабря 1865 года в питере -- это СУРОВЫЙ квест. на самом деле нет. на самом деле это легко разрулить. пускай тип ДАТА все знает о календарных реформах разных стран во все времена. нам-то до этого какое дело? у нас задача решается простым вычитанием. и пускай за этим вычитанием не только 100500 строк кода, но еще и База Знаний.

    сорри за неровный стиль изложения. но, думаю, мысль моя понятна. семантика операции + и — может быть сколь угодно сложной и если она выполняется над разными типами -- это хороший способ спрятать под капот реализацию логики.
    americans fought a war for a freedom. another one to end slavery. so, what do some of them choose to do with their freedom? become slaves.
    Re[2]: Про типы и логику - С++
    От: Mamut Швеция http://dmitriid.com
    Дата: 07.03.15 21:46
    Оценка:
    М>сорри за неровный стиль изложения. но, думаю, мысль моя понятна. семантика операции + и — может быть сколь угодно сложной и если она выполняется над разными типами -- это хороший способ спрятать под капот реализацию логики.


    Это прекрасно в теории. На практике за overload +/- во многих местах бьют канделябром по рукам, и правильно делают. А иногда не бьют, и тоже правильно делают

    Более того, «семантика операции + и — может быть сколь угодно сложной» скрывает под собой толпу подводных камней, которую я, в принципе, выразил тут
    Автор: Mamut
    Дата: 30.01.15
    :

    Цитата: Any dev can build systems without worrying about implementation internals. Just fit types together

    Возражение: После чего в пятницу вечером сервер ложится и не поднимается, потому что какой-то новичок написал код, позволяющий создавать отчеты о продажах за целый год, что начало поднимать с диска терабайты данных в память. А что? API-вызов всего лишь требует типа TDate


    Так и со сложными +/-, которые прячут что-то под капотом. В результате строка a = b + c может спокойно положить сервер, потому что под капотом b делает full database lock, а c поднимает в память четыре терабайта данных


    dmitriid.comGitHubLinkedIn
    Re[3]: Про типы и логику - С++
    От: мыщъх США http://nezumi-lab.org
    Дата: 07.03.15 22:49
    Оценка:
    Здравствуйте, Mamut, Вы писали:

    M>Это прекрасно в теории. На практике за overload +/- во многих местах бьют канделябром по рукам, и правильно делают. А иногда не бьют, и тоже правильно делают

    хорошо, не надо overload. пускай будут функции типа delta(temperature1, temperature2). таки если temperature1 и temperature2 это разные типы -- то delta все равно будет вычислена правильно. ну или мы получим ошибку компиляции. на выбор. а вот если температура хранится как int, то мы тоже получим ошибку. но уже в вычислениях.

    и еще раз о строках. семантика "+" в отношении строк в том же паскале сильно отличается от семантики "+" в отношении int. но мы привыкли к этому и даже не замечаем. а питон может даже умножать строку на число. семантика понятна на интуитивном уровне и сильно упрощает код в определенных местах.


    M> Так и со сложными +/-, которые прячут что-то под капотом. В результате строка a = b + c может спокойно положить сервер,

    "что-то прячут под капотом" -- вы же сами говорите, что "А что? API-вызов всего лишь требует типа TDate". по вашему и API вызовов нужно воздержаться. разговор идет про возможность реализации логики на типах. +/- это чисто для примера. без них легко обойтись. речь про то, что если ожидается тип А, вместо которого передается тип Б, то операция преобразования это и есть логика на типах. если такая операция возможна вообще.

    хорошо, давайте поговорим о вашей задаче:

    # если заказ отправлен, нельзя увеличивать, можно уменьшать
    у нас есть тип "отправлен" и "не отправлен". у типа "отправлен" метод "уменьшить сумму" пускай всегда возвращает ошибку. пускай даже ошибку + id строки с текстом объяснения почему нельзя.

    я не знаток си++, но вы же не требовали си++, так? возьмем питон с его утиной типизацией. все работает просто великолепно. легко создать тип (в случае питона — класс или функцию) "отправлен" и "не отправлен". и все. остается только связать его с заказом. пускай заказ у нас это A. пускай A.status это статус заказа. в момент отправки A.status меняет тип и потому где бы ни располагалась операция изменения суммы -- она неизбежно обломается с попыткой уменьшения суммы после отправки. при этом транслятор не будет возвращать ошибку. ошибка возвратиться в рантайтме с описанием на человеческом языке (если это необходимо).

    M> потому что под капотом b делает full database lock, а c поднимает в память четыре терабайта данных

    я уже писал, но напишу еще. си (си++) ужасно противный язык. нет никакого способа (исключая грязные хаки) узнать сколько у нас осталось стека. и мы не знаем накладные расходы на создание кадрового фрейма. они очень сильно зависят от компилятора и ключей компиляции. в результате рекурсия это очень и очень плохо.

    вопрос: может ли уронить сервер код (2*2)? пускай этот код вообще в другом процессе. ответ — в конфигурации по умолчанию в win server/workstation очень даже может и порой роняет. почему роняет -- это уже другой вопрос ответ на который требует знания потрохов винды. умеючи можно изменить конфигурацию (штатными средствами) так, чтобы не ронял. но оно не так часто происходит, чтобы это было актуально.
    americans fought a war for a freedom. another one to end slavery. so, what do some of them choose to do with their freedom? become slaves.
    Re[4]: Про типы и логику - С++
    От: Mamut Швеция http://dmitriid.com
    Дата: 07.03.15 23:37
    Оценка:
    M>>Это прекрасно в теории. На практике за overload +/- во многих местах бьют канделябром по рукам, и правильно делают. А иногда не бьют, и тоже правильно делают
    М>хорошо, не надо overload. пускай будут функции типа delta(temperature1, temperature2). таки если temperature1 и temperature2 это разные типы -- то delta все равно будет вычислена правильно. ну или мы получим ошибку компиляции. на выбор. а вот если температура хранится как int, то мы тоже получим ошибку. но уже в вычислениях.

    А, ну с этим я вроде нигде и не спорю

    М>хорошо, давайте поговорим о вашей задаче:


    Не надо про мою задачу Пока что с ней никто, от слова вообще, не справился

    М>я не знаток си++, но вы же не требовали си++, так? возьмем питон с его утиной типизацией. все работает просто великолепно. легко создать тип (в случае питона — класс или функцию) "отправлен" и "не отправлен". и все. остается только связать его с заказом. пускай заказ у нас это A. пускай A.status это статус заказа. в момент отправки A.status меняет тип и потому где бы ни располагалась операция изменения суммы -- она неизбежно обломается с попыткой уменьшения суммы после отправки. при этом транслятор не будет возвращать ошибку. ошибка возвратиться в рантайтме с описанием на человеческом языке (если это необходимо).


    Охохонюшки. Опять только один статус и только одна проверка У нас в рантайме все хорошо и без типов и трансляторов И да, у нас можно изменять сумму после отправки И условий, при которых нельзя — с десяток. Создавать десяток классов?

    M>> потому что под капотом b делает full database lock, а c поднимает в память четыре терабайта данных

    М>я уже писал, но напишу еще. си (си++) ужасно противный язык. нет никакого способа (исключая грязные хаки) узнать сколько у нас осталось стека. и мы не знаем накладные расходы на создание кадрового фрейма. они очень сильно зависят от компилятора и ключей компиляции. в результате рекурсия это очень и очень плохо.

    Я про рекурсию даже не упоминал

    М>вопрос: может ли уронить сервер код (2*2)? пускай этот код вообще в другом процессе. ответ — в конфигурации по умолчанию в win server/workstation очень даже может и порой роняет. почему роняет -- это уже другой вопрос ответ на который требует знания потрохов винды. умеючи можно изменить конфигурацию (штатными средствами) так, чтобы не ронял. но оно не так часто происходит, чтобы это было актуально.


    Вооот. А у нас ситуация «люди запустили отчет за последний год и система начала терять производитльность вплоть до падения ноды» одно время происходило раз в месяц Оказался один отчет на одной странице, на которой не стояло «if DateRange > 1 месяца», не разрешать :D И это не решается никаким типизациями, а только волшебными пенделями программистам




    Offtop:

    Эта багофича имела вообще другое начало, которое было еще прекраснее.

    У нас в качестве основной базы данных используется (в плане abuse, а не use) mnesia, в которой по дизайну базы таблица не может содержать больше 2GB объектов. Как только таблица переваливает за 2 GB, ее нужно разбивать на части.

    И есть у нас архивные заказы. Текущая таблица архивных заказов висит в памяти, остальные (кусками по 2 GB) — на диске. И вот кто-то приходит и требует отчет по заказам.

    Я когда впервые увидел код, который это реализует, я ржал, наверное, полчаса.

    Псевдокод:
    
    orderIds = order_table.get_all_ids()
    
    foreach(table in archived_order_tables):
      ids = table.get_all_ids()
      orderIds.append(ids)
    
    sort(orderIds)
    
    filter(OrderIds, определенное_условие)


    и так — на каждый отчет (даже не годовые). На тот момент было 32 архивные таблицы, то есть на каждый отчет 64 гига поднимались в память, из них считывалось в память несколько миллионов записей, потом в памяти же они сортировались и фильтровались. А мы удивлялись, какого хрена постоянно mnesia нагруженная и один из процессов веб-сервера стабильно около 300 гигов в памяти занимает

    И да, этот код был написан одним из гуру Эрланга, к тому же

    И да, тут ничего не спасет, только живительная эвтаназия.


    dmitriid.comGitHubLinkedIn
    Re[5]: Про типы и логику - С++
    От: мыщъх США http://nezumi-lab.org
    Дата: 08.03.15 01:22
    Оценка:
    Здравствуйте, Mamut, Вы писали:

    М>>хорошо, давайте поговорим о вашей задаче:

    M>Не надо про мою задачу Пока что с ней никто, от слова вообще, не справился
    вы меня просто возбуждаете с ней справится. давайте попробуем, а?

    M>Охохонюшки. Опять только один статус и только одна проверка

    M>Создавать десяток классов?
    почему нет? что вас останавливает? я же тут пытаюсь на эмулировать машину состояний на том, что есть (типы). пускай каждый тип это узел, а его методы -- это функции перехода в другие состояния. ваша задача очень хорошо ложится на машину состояний. другой вопрос как ее реализовать? один из вариантов -- на типах. не знаю как элегантно наследовать классы в си++, но на питоне очень просто брать классы и плодить на основе их типы, при этом наследуются тонны кода и вы только правите поведенческую модель дерева в конкретных узлах. оно к тому же будет и быстро работать. что может быть быстрее машины состояний? если из состояния А возможен переход только в Б и Ц, то проверки на Е и Ф выбрасываются. меньше проверок -- больше скорость. да и машину состояний можно визуализировать, чтобы проверить насколько наша модель корректна.

    M>Я про рекурсию даже не упоминал

    а зря. потому что никогда не знаешь где она прячется.


    M> Вооот. А у нас ситуация "люди запустили отчет за последний год

    M> и система начала терять производитльность вплоть до падения ноды"
    по этой причине я не пользую си++. потому что глубоко убежден в непосильности компилятора решать мои проблемы. да, в си++ компилятор отлавливает какие-то ошибки еще на этапе трансляции. но это очень малая часть и без тестов не обойтись. и тесты нужно писать без оглядки на компилятор. короче говоря, я не вижу причин по которым бы стоит изучать ужасно сложный си++ стандарт на который больше чем академическое описание правил русского языка и при этом даже такой сложный инструмент как си++ не позволяет решать мои задачи равно как и ваши.

    давайте (если вам интересно) попробуем решить вашу задачу на базе машины состояний, представив ее узлы различными типами (классами). увы, я не знаю эрланг. только си (но он тут не подходит), js, питон, руби и немного java.

    M> Эта багофича имела вообще другое начало, которое было еще прекраснее.

    M> У нас в качестве основной базы данных используется (в плане abuse, а не use)
    про 2 гб есть другая история из моей практики. где на си для индексов использовали int, а не size_t. на 64 бит платформе на никсах int оказался 32 бита. а что? бывает. а size_t — 64 бита и можно выделять блоки памяти больше 4х гектар. вот только часть алгоритмов работала с ним некорректно. вот такая жопа. и никакая проверка типов не спасла, поскольку в си индекс вполне можно хранить в int и никто не ругается матом. хотя сишные библиотечные функции принимают size_t в качестве аргумента, но так получилось, что их не использовали.


    M> Я когда впервые увидел код, который это реализует, я ржал, наверное, полчаса.

    M> И да, тут ничего не спасет, только живительная эвтаназия.
    ээээх... помню как сервер в лабах однажды под конец рабочего дня начинал самопроизвольно уходить то в ребут, то на вешалку. а часам к восьми вечера все восстанавливалось. такая странная зависимость заставила искать код саботажа. искали его долго и нашли только после того как нашал вешаться уже второй сервер.

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

    а все это время -- искали ошибки в коде и пробовали откатываться к образу системы взад. о такой коварной ошибке как лопнувший трубопровод никто и не думал. железо тестировали конечно. ночью. когда все расходились по домам. ну так ночью сервер работал без воды и потому отрабатывал все тесты производителя железа на отлично.

    и раз уж мы заговорили за кондиционеры... вообразите себе картину. в одном Y образном кубике рядом сидят два человека. одна -- девушка, которая чуть ли не раздетая до гола и рядом с ней мужик в зимней куртке, ежится от холода. а все потому что на мужика сверху льется поток переохлажденного воздуха, но система построена так, что до других он не доходит и остальным жарко. а что? клево так. отъехал в кресле на метр и ты уже на юге. вернулся на метр обратно и попал на крайний север.
    americans fought a war for a freedom. another one to end slavery. so, what do some of them choose to do with their freedom? become slaves.
    Re[5]: Про типы и логику - С++
    От: AlexRK  
    Дата: 08.03.15 06:28
    Оценка:
    Здравствуйте, Mamut, Вы писали:

    M>Не надо про мою задачу Пока что с ней никто, от слова вообще, не справился


    Справедливости ради, я бы все же уточнил: ее просто никто не стал делать.
    "Не стал делать" и "не справился" это все же разное.
    Логически из первого второе не следует.
    Re[5]: Про типы и логику - С++
    От: jazzer Россия Skype: enerjazzer
    Дата: 08.03.15 08:33
    Оценка:
    Здравствуйте, Mamut, Вы писали:

    M>Охохонюшки. Опять только один статус и только одна проверка У нас в рантайме все хорошо и без типов и трансляторов И да, у нас можно изменять сумму после отправки И условий, при которых нельзя — с десяток. Создавать десяток классов?


    В моей версии класс для свойства — это строчка
    struct AnotherClass;
    Всё. Внутри даже не нужно ничего, никаких членов и прочая — нам нужно только уникальное имя. "Создавать десяток таких строчек" — вот ужас то
    jazzer (Skype: enerjazzer) Ночная тема для RSDN
    Автор: jazzer
    Дата: 26.11.09

    You will always get what you always got
      If you always do  what you always did
    Подождите ...
    Wait...
    Пока на собственное сообщение не было ответов, его можно удалить.