Структурные и Именованные типы. Поведение или Представление?

Lionishy

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

fixxxer

К.О.
Партнер клуба
В функциональщине - да. Там и паттерн матчинг, и вот это все. А в ООП так делать не надо никогда, есть исключения для этого.
 

Lionishy

Новичок
А в ООП так делать не надо никогда
Этого делать не следует не в ООП, а в именованной системе типов. Вот в Java так делать, действительно, категорически нельзя, потому что нет механизма контроля случайных ошибок, нет защиты от дурака для таких конструкций. Но Java -- это очень убогий ООП, на уровне древних C++, Oberon и Simula. Возможно, даже одна из худших ООП реализаций (даже хуже С++), это тема отдельной дискуссии.

Но для той же JVM есть ООП Scala, здесь уже всё можно и нужно.
Нужно за тем, чтобы локализовать внутри функций выбор, чтобы полностью определить программу заданием связей, а не выбором ветвей потока исполнения.

Я смотрел, как выглядят средние PHP коды. Они пестрят выборами и сменой управления. Эти "повороты" не локализованы. Частенько можно в Java встретить даже управление на уровне Exception или Null Object Pattern. А ведь этого можно было бы избежать, если бы у нас был в руках механизм Maybe и Either.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
@fixxxer, функциональщина ни при чем. Когда твой алгоритм сводится к конечному автомату, то даже в конфиге nginx тебе нужен error_page/try_files, и никакая религия этого не изменит.

Lionishy, Пример кода на scala в студию, пожалуйста. Talk is cheap. Подозреваю, что о приведении типов в scala ты узнал по ссылке из моего сообщения выше.
 
Последнее редактирование:

Lionishy

Новичок
@grigori
Уже стало совершенно обычным, что ваши замечания не имеют отношения к обсуждаемому вопросу. Это мой последний ответ на Ваше сообщение.
Код:
sealed trait TF
case object T extends TF {
}
case object B extends TF {
}
 

fixxxer

К.О.
Партнер клуба
Scala - это хитрый замес ООП с ФП. Те же твои Maybe - это монады, чистейшая функциональщина. Классический ООП - это контракты и методы/сообщения, там система типов именованная (как ты говоришь) по определению, и там просто не надо такого хотеть. :) Чтобы не обсуждать Java с Симулой, предлагаю считать каноничным ООП Smalltalk, там вообще интерфейсы неявные ;). Что-то промежуточное между Java и SmallTalk есть в типах Go, но там именование никуда не девается, оно просто как бы автоматическое.

Что касается верификации кода - понятное дело, что сама такая задача на ФП ложится намного проще.

Я смотрел, как выглядят средние PHP коды
Средняя программа на любом широко используемом языке выглядит как говно. Для flow control в ООП есть известные паттерны, они работают, а то, что они многословны - я недостатком не считаю.
 
Последнее редактирование:

grigori

( ͡° ͜ʖ ͡°)
Команда форума
вероятно, потому что все твои сообщения не имеют отношения к разработке, только ad-hoc для твоих личных задач, с попыткой притянуть их за уши к общей логике, что в виде код становится очевидной глупостью, как мы видим из твоего примера выше

sealed trait - это реализация enum для типов, т.е. представление списка допустимых значений входного параметра конечного автомата.
Попытка расширения списка возможных значений сторонним разработчиком аналогично act(new P()) приведет к ... surprize ... исключительной ситуации! :)
 
Последнее редактирование:

Lionishy

Новичок
@fixxxer,
Между ООП и ФП нет границы, это нечто взаимно перпендикулярное (или близкое к тому). То есть, ООП -- это механизм абстракции. ФП использует определённый механизм абстракции. У меня есть личное мнение, что ООП стало таким популярным именно в императивных языках, потому что в самых популярных из них, например С, вообще было сложно с абстракциями и замкнутостью (процессы не могли быть объектами первого рода). В то же время, у ФП были большие сложности с созданием таких компиляторов, которые бы позволили использовать нормальную стратегию редукции. Вот и получилось, что в конце восьмидесятых -- начале девяностых конъюнктура как раз способствовала появлению C++ и Java, и т.п. в том виде, в котором сегодня они у нас есть.

каноничным ООП Smalltalk
Предлагаю считать каноничным императивным ООП вообще untyped. Luca Cardelli :)
 

fixxxer

К.О.
Партнер клуба
Между ООП и ФП нет границы, это нечто взаимно перпендикулярное (или близкое к тому)
Я и не спорю. Скала тому пример. А если ширше смотреть, можно и процессы в Erlang можно вполне себе считать объектами, вполне по smalltalk-овски.

Вот только непонятны твои претензии к ООП.

1) Сама по себе парадигма ООП - это "объекты меняются сообщениями в соответствии с контрактами", ничего более. Реализуй как хочешь. Если считать, что ООП - это Страуструповская ерунда про наследование и так далее, тогда другой вопрос, но это же ерунда )

2) Если говорить о конкретной реализации в Java (ну и в PHP - несмотря на рантайм, суть почти 1 в 1, только дженериков ужасно не хватает), которые по сути являются гибридными процедурно-объектными языками, как я и сказал, все вопросы решаются паттернами - да, иначе и многословнее, но решаются; если придерживаться принципов SOLID, никаких проблем не возникает. Качество среднего кода на PHP, конечно, ужасное, но это следствие распространенности среди начинающих программистов, низкого входного порога и исторического роста из языка "для скриптиков, встроенных в html". А так, все design patterns из Java-мира применимы.
 

Lionishy

Новичок
Вот только непонятны твои претензии к ООП.
К концепции объектов у меня претензий, конечно, нет.

Но во многих случаях Java просто превозмогает саму себя... Всё объект понимается буквально (и используется только как пустой тип)... Соорудили строгую типизацию, а про то, что она плохо работает без метапрограммирования забыли (выстреливают рефлексии и EJB)... Отказались от записей и код наводнился ValueObject'ами... Забросали всё исключениями достаточно бездумно (программисты вынуждены использовать исключения для управления потоком)... И т.д.

А PHP на опыте оказался, по моему мнению, лучше без типов. Впрочем, я так понимаю, что PHP в липких руках Oracle, так что его будут "незаметно" подталкивать под Java... Думается...
 

fixxxer

К.О.
Партнер клуба
Ну, насчет record... В value object может быть валидация, всякие там assert-ы, можно, конечно, сделать record с геттерами-сеттерами, но тогда уж с точки зрения бритвы Оккама достаточно классов. Против records/structures ничего не имею, но это при наличии классов по сути сахарок.

Вот без first-class functions действительно не очень хорошо, через анонимные классы они эмулируются довольно уродливо. То, что недавно добавили - костыль (впрочем, это везде так, где изначально их не было - и в C++, и в PHP).

Метапрограммирование - это очередное слово, которые все понимают по своему. Generics тоже метапрограммирование, темплейты в С++ - тем более. А рубисты этим называют возможность насрать в рантайме куда угодно чем угодно :)

Если уж про value objects и вернуться к твоему вопросу изначальному - меня вот тут всегда больше беспокоило, что я могу безболезненно сложить два инта weight и length. ;)
 
Последнее редактирование:

Lionishy

Новичок
В value object может быть валидация, всякие там assert-ы
С валидацией не всё очевидно... Если это предметная валидация (строка не длиннее 30 символов, число строго больше нуля и т.д.), то может быть лучше определить типы с нужными ограничениями и присваивать эти метки внутренним полям ValueObject?

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

У ValueObjct есть одно важное преимущество: они могут представлять активные данные -- потоки, ленивые связи и т.п. Только ValueObject чрезвычайно редко используются в таком виде. Да и реализуются мутабельным образом. Необходимость в более простом инструменте, который бы представлял не поведение, а простую структуру есть. Собственно, в Scala есть CaseCalasses. Они представляют простые данные и их структуру легко проследить, потому они могут использоваться в pattern matching периода компиляции.

Тут всё очень запутанно. В принципе ООП легко может справиться со стрелочными объектами.
PHP:
interface Nat {
    public function succ();
    public function pred();
    public function is_zero();
}

class NonZero implements Nat {
    public function succ()
    {
        return new NonZero(this);
    }

    public function pred()
    {
        return $this->pred;
    }

    public function is_zero()
    {
        return false;
    }

   public function __construct($pred)
   {
       $this->pred = $pred;
   }

   private $pred;
}

class Zero implements Nat {
    public function succ()
    {
         return new NonZero(this);
    }

    public function pred()
    {
        throw new Exception("Operational semantic violation");
    }

   public function is_zero()
   {
       return true;
   }
}

class Summ implements Nat {
    public function succ()
    {
        return NonZero(
            this->summ());
    }

    public function pred()
    {
        return this->summ()->pred();
    }

    public function is_zero()
    {
        return this->summ()->is_zero();
    }

    /**
     * @param I Nat
     * @param I Nat
     */
    public function __construct($lha,$rha)
    {
        $this->lha = $lha; $this->rha = $rha;
    }

    private function summ($a,$b)
    {
        if ($a->is_zero())
            return $b;
        return $this->summ(
            $a->pred(),
            new NonZero($b));
    }

    private $lha;
    private $rha;
}
Для нашей очень простой операционной семантики class Summ будет стрелочным объектом.
А общее правило для построения стрелочных объектов f: A -> B такое:
class f implements B {
F(A: a);
}

Типы стрелочных объектов -- это фабрики стрелочных объектов
Код:
interface F {
    f(A: a) -> B: b;
}
Код:
class F implements F {
    f(A: a) -> B: b
        b = f(a);
}

При таком подходе вам специальные стрелочные объекты нужны не будут. Такой подход требует большей аккуратности, чем подход полноценно функциональный, но он реален.
Введение методов как объектов первого рода связано с тем, что ООП действительно сложно, сложнее функционального подхода. Потому для более быстрой разработки лучше явно выделить более простые абстракции -- явные стрелочные объекты.

я могу безболезненно сложить два инта weight и length
Да -- это ужасно!
Потому в первом сообщении я написал слово "иногда". Иногда очень удобно определить псевдоним. Иногда удобнее знать внутренню структуру и применять методы соответственно именно внутренней структуре, иногда удобно проследить внутреннюю структуру. Но не "всегда".
Потому хорошо, когда есть возможность и так, и так работать. По этой причине лучше всего отделять записи от классов в отдельную, иную конструкцию языка.

P.S.
Длинно получилось... Извиняйте...
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
С валидацией не всё очевидно... Если это предметная валидация (строка не длиннее 30 символов, число строго больше нуля и т.д.), то может быть лучше определить типы с нужными ограничениями и присваивать эти метки внутренним полям ValueObject?
Эти типы и будут value object'ами. И да, что значит «предметная валидация»? Ты так называешь поддержание внутренних инвариантов объекта?

Если это доменная валидация данных (данные обязательно введены пользователем, переданный e-mail не содержится в базе адресов и т.д.), она, как мне кажется, обязательно должна быть отделена от ValueObject.
Всё наоборот. Доменная валидация VO должна быть в самом VO. Но ты приводишь в качестве примера «переданный e-mail не содержится в базе адресов», то время как этот инвариант просто не является внутренним для VO Email, поэтому проверять это внутри него абсурдно. Собственно, я не улавливаю разницы с твоей «предметной валидацией», есть только семантическое различие. Это те же самые проверки инвариантов на уровне «строго больше нуля», просто это уже бизнес-инварианты, поскольку речь идет про модель.
 

Lionishy

Новичок
Эти типы и будут value object'ами.
Нет. Это будут стрелочные объекты, которые преобразуют объект ValueObject в ValidValueObject. Если это сделать не получается, значит есть ошибка операционной семантики (ошибка уровня логики кода) и программу нужно немедленно завершить. Это что-то вроде проверки ranges`ами в Ада. Не прошёл -- ошибка компиляции (если попытался подсунуть напрямую ValueObject вместо ValidValueObject) либо рухнул с логическим исключением. Как попытка выполнить pred() на Zero. Чтобы этого не было, в ValueObject поле должно требовать не Nat, а NonZero.
Comment: правильнее говорить, что ValidValueObject -- это стрелочный объект из ValueObject в ValueObject. Агрегатный объект не будет проверять корректность строки, он запросит корректную строку, но при обращении get, например, будет обычную строку возвращать.

поддержание внутренних инвариантов объекта?
"Поддержание" -- это неверное слово. Объект просто не может быть создан с нарушением инварианта.

Доменная валидация VO должна быть в самом VO
Доменная валидация -- это управление потоком. То есть, нет нарушения операционной семантики, поступление неверных данных -- штатная ситуация. Например, если пользователь ввёл неверные данные -- переспроси. То есть, этим занимается модель процесса, а не модель данных.

А если это ситуация неприемлемая, то опять создаём стрелочный объект проверки e-mail, который обрушит программу, если e-mail будет найден в базе.
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
Нет. Это будут стрелочные объекты
Что такое «стрелочный объект»? Ты про сравнение по ссылке, а не по значению? Эм, выше прочитал и нифига не понял. Ты можешь понятный пример по аналогии с AvailableMana ниже?

, которые преобразуют объект ValueObject в ValidValueObject
Что ты имеешь в виду? Зачем тебе иметь VO, который не поддерживает свои собственные инварианты?

"Поддержание" -- это неверное слово. Объект просто не может быть создан с нарушением инварианта.
У тебя есть более удачный термин?

Доменная валидация -- это управление потоком.
Капитан очевидность говорит, что доменная валидация — это проверки domain-инвариантов. На уровне модели эти проверки должны быть жесткими и падать с исключением. Примерно то же самое ты подразумеваешь под нарушением операционной семантики, насколько я понимаю.
PHP:
namespace Blizzard\Hearthstone\GameDomain;

final class AvailableMana
{
    public function __construct(int $mana)
    {
        Assertion::range($mana, 0, 10);

        $this->mana = $mana;
    }

    // ...
}
В игре Hearthstone у игрока не может быть больше 10-ти кристаллов маны. Стало быть, 11 и более кристаллов — это бессмыслица с точки зрения нашего домена и нам нужно это контролировать. Мы моделируем понятие «Доступная Мана», без сохранения этого инварианта модель будет неполной.

Например, если пользователь ввёл неверные данные -- переспроси. То есть, этим занимается модель процесса, а не модель данных.
Я не понимаю какое отношение ввод имеет к домену. Это имеет смысл, если мы моделируем, скажем, клавиатуру. В остальных случаях это представление. То, о чем ты сейчас говоришь, называется клиентской валидацией и может быть на стороне клиента (iOS-приложение, JavaScript, etc.), само по себе это исключает того факта, что модель должна поддерживать свою внутреннюю непротиворечивость.
 
Последнее редактирование:

Lionishy

Новичок
Что такое «стрелочный объект»?
Принято говорить, что объект стрелочный, если этот объект есть функция.
Вот я нарисовал пример с натуральными числами. Там экземпляры класса Summ -- это стрелочные объекты.

Что ты имеешь в виду?
Тут я что-то невразумительно написал. Комментарием попытался исправить, всё равно не то. Это не надо читать.
Я поясню на AvailableMana

PHP:
namespace Blizzard\Hearthstone\GameDomain;

final class AvailableMana
{
    public function __construct(int $mana)
    {
        Assertion::range($mana, 0, 10);

        $this->mana = $mana;
    }

    // ...
}
Если AvailableMana может принять решение, что делать, когда $mana > 10 или $mana < 0, то можно и нужно эту проверку делать внутри AvailableMana. В этом случае у нас ветвится бизнес-процесс.
Но если AvailableMana вообще не может ничего редуцировать для этих значений -- это как раз ошибка операционной семантики. У нас в макроязыке для объекта AvailableMana нет ни одного правила редукции, которое позволили бы продвинуться абстрактному исполнителю.
Вот эту ситуацию можно и нужно проконтролировать на уровне типов.

PHP:
namespace Blizzard\Hearthstone\GameDomain;

final class AvailableMana
{
    public function __construct(IntRange<0,10> $mana) //если бы PHP были типы, параметризуемые значениями
    {
        $this->mana = $mana;
    }

    // ...
}
А IntRange мог бы иметь рекурсивный конструктор, который как ни крути, ничего кроме 0 .. 10 выдать не может.
Либо можно было бы создать преобразование, частично определённую функцию F: Int -> IntRange<0,10> | Error
В Java/PHP вместо Error просто исключение -- сгорел предохранитель и всё.

А вот принятие решение в бизнес-процессе -- это уже не исключение.

Ситуации, когда есть нарушения операционной семантики или предусловий, можно разрешить на уровне проверки и преобразования типов.
Ситуации, когда есть ветвление бизнес-процесса -- на уровне объектов ветвления.


Согласен, что первый случай всё же можно отнести к ValueObject. Но если вынести подобные проверки на уровень типов, то можно оперировать кортежами из этих valid data objects.
 

Вурдалак

Продвинутый новичок
Я не понимаю твоей претензии к исключениям. На уровне бизнес-логики (domain) решение приниматься не будет, поскольку это значение просто не имеет смысла для этого domain. Грубо говоря, геймдизайнер говорит: «может быть максимум 10 кристаллов», а ты такой: «а что все-таки будет 11? Нужно принять решение в бизнес-процессе!». Геймдизайнер тебе еще раз повторит, что этот кейс просто не имеет смысла и принимать каких-то бизнес-решений он не собирается.

А IntRange мог бы иметь рекурсивный конструктор, который как ни крути, ничего кроме 0 .. 10 выдать не может.
Т.е. эта конструкция у тебя молчаливо принимает какое-то из значений, если туда передать 11?

> IntRange<0,10>

Я вижу обычный сахар для частного случая. Там могут быть и более сложные проверки инвариантов, включающие регулярные выражения и прочее. Также VO могут содержать нормализацию. Это всё к вопросу о необходимости value object'ов.
 
Последнее редактирование:

Lionishy

Новичок
Если нет правила редукции для AvailableMana, когда туда попало число 11, то этот вопрос можно и нужно решить на уровне типа. И это не просто "сахарок". Это строгое разделение ответственности, которое позволяет разбить формальное доказательство корректности на аккуратные леммы. Я уже не буду лазить в AvailableMana и проверять, а не получилось ли так, что там int принял какое-то значение неверное. Я точно знаю, что этого быть не может -- у меня есть лемма IntRange<0,10>.

Как устроен IntRange<0,10> AvailableMana плевала с высокой башни. Выкидывает ли IntRange<0,10> исключение при конструировании или его строго невозможно сконструировать для других чисел (например, рекурсивно определить конструктор любого положительно целого cons<0> = 0 | cons<10> = 10 | cons<a>0> = cons<10> - (1+cons<a-1>). Тогда выражение cons<11> просто вызовет ошибку компиляции, потому что возникнет нередуцируемое выражение cons<-1>).

Я вижу обычный сахар для частного случая. Там могут быть и более сложные проверки инвариантов, включающие регулярные выражения и прочее.
Можно создать, конечно, и сколь угодно сложный тип.
Нужно ли? Другой вопрос.
А вот что нужно -- избавить AvailableMana от необходимости проверять свои параметры.
 

Вурдалак

Продвинутый новичок
Я уже не буду лазить в AvailableMana и проверять, а не получилось ли так, что там int принял какое-то значение неверное. Я точно знаю, что этого быть не может -- у меня есть лемма IntRange<0,10>.
<...>
Как устроен IntRange<0,10> AvailableMana плевала с высокой башни.
<...>
Можно создать, конечно, и сколь угодно сложный тип.
Ты всего-навсего предлагаешь декомопозировать один VO на несколько более мелких VO без какой-либо разумной аргументации. Я точно знаю, что AvailableMana не может быть вне 0..10, потому что я вижу эту строку:
PHP:
Assertion::range($mana, 0, 10)
Ты предлагаешь то же самое, записанное по-другому.

То, что ты тут называешь «типом» — это тот же VO.

Т.е. ты против VO, но за VO. OK.
 
Сверху