Pull to refresh

Comments 90

Сразу вспомнилась книга «Безопасно by design»: там много примеров, где пользовательские типы (вплоть до количества заказов в интернет-магазине) помогают писать более безопасный и менее подверженный ошибкам код.

книга «Безопасно by design»: там много примеров, где пользовательские типы (вплоть до количества заказов в интернет-магазине) помогают писать более безопасный

Начинается с

Как неизменяемость решает проблемы с безопасностью.

что сразу создаёт накладные расходы и ставит крест на скорости. :-(

А так часто нам нужна какая-то экстремальная скорость и сверхэкономия памяти?

Думаю те части, в которых это нужно, можно писать опасно, но это должно делаться осознанно.

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

А что без неизменяемости всё будет именно работать неправильно с ошибочными результатами?

А сама неизменяемость гарантирует, что всё работать правильно и без ошибок?

Ложная дихотомия. Никто не утверждает ничего подобного. Гарантий нет, но меньше риск допустить ошибку.

Пристегнутый ремень в автомобиле гарантирует вам выживание в любой аварии? А если вы его не пристегнете, то обязательно попадете в аварию и погибните?

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

Не серебряная пуля, просто один из полезных приемов.

Пристегнутый ремень в автомобиле

Неизменяемость - это не ремень безопасности, а "впереди автомобиля должен бежать специальный человек с флагом, фонарём и дудкой" (c) из правил дорожного движения The Locomotive Act 1865 года. :-)

Потому что неизменяемость - это есть ничто иное, как эмуляция ручных вычислений на бесконечном листике бумаги = всё что написано на бумаге неизменяемо!

Расплата за это получается вот такой:

Почему современное ПО такое медленное — разбираемся на примере диктофона Windows

Компьютеры быстры, но вы этого не знаете

Когда старый компьютер лучше нового

Неожиданные причины торможения программ и систем

Ни одна из перечисленных вами проблем не была вызвана неизменяемостью.

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

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

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

Что же касается скорости и компактности, то есть пара примеров.

Посмотрите на наш прекрасный интернет. Что не сайт, то тормоза и обжорство памятью. Спасибо фреймворкам.

Недавно понадобилось написать объединение двух таблиц по ключу с сортировкой. Ну лень было все это руками ваять. Использовал готовый хэшмэп и чего то ещё. Исходный файл 700мб + чуть-чуть. Так вот в первоначальном варианте это вообще не взлетело, т.к. Jvm отъедала 8 Гб оперативы и падала с ошибкой нехватки памяти. Пришлось один из этап делить на куски. И то это отьедало 5-6 гигов. Но функционал нужен был быстро, конкретно сейчас и без сюрпризов. Иначе бы написал все руками. Работало бы и быстрее и точно более экономно.

Ну и каким местом обсуждаемые типы касаются проблем фреймворков и прожорливости джавы?

UFO just landed and posted this here

Но функционал нужен был быстро, конкретно сейчас и без сюрпризов

Я правильно понимаю, что сами без фреймворков реализовать "функционал" быстро, конкретно сейчас и без сюрпризов вы не можете, но виноваты фреймворки?

Неизменяемость не обязательно ставит крест на скорости. Есть структуры данных, специально заточенные под это.

В функциональных языках неизменяемость очень распространена, поэтому можно, например, посмотреть книжку Криса Окасаки о чисто функциональных структурах данных.

Неизменяемость — это снаружи, то что видит программист. Под капотом интерпретатор/компилятор вполне может использовать под новую переменную ту же память, если проследил, что старая переменная в дальнейшем не используется.
UFO just landed and posted this here

Сейчас (2022) эту статью можно лишь включить в обучение, почему до этого дошли, а тогда, когда она была написана (в 2011 на на пост 2007), это возможно было обсуждаемым, но не сегодня.

UFO just landed and posted this here

Контроль ошибок ввода и обработки - единственный способ уменьшить количество неверных данных до приемлемого уровня!

UFO just landed and posted this here

Пример с логированием перс данных не решит контроль ввода

Пусть у нас будет второй тип с именем VerifiedEmailAddress. Если хотите, он даже может наследовать от EmailAddress

Понимаю, что это всего лишь пример, но тут классическая ловушка наследования. Мало того, что свойство Verified относится скорее к аккаунту, а не к адресу, так еще через пару итераций у нас будет что-то вроде:

  • VerifiedEmailAddress

    • VerifiedPersonalEmailAddress

    • VerifiedBusinessEmailAddress

  • NonVerifiedEmailAddress

    • NonVerifiedPersonalEmailAddress

    • NonVerifiedBusinessEmailAddress

А потом в эту иерархию понадобиться впихнуть IsActiveEmailAddress, IsSubscribedEmailAddress. Здесь, как мне кажется, более уместна композиция (псевдокод):

class Account:
    email : EmailAddress
    is_verified : bool,
    type : {Personal, Business},
    is_active : bool,
    is_subscribed : bool

"Verified" - не свойство сущности, а состояние целого приложения. Причём в зависимости от типа проверяемой сущности у нас могут быть два различных состояния "VerifiedUserEmail" и "VerifiedUser". Тоже самое касается извлечения данных. Шаг извлечения - переход из одного состояния приложения в другое. Но поскольку добрая половина людей не умеют в теорию графов, а computer science звучит для них как что-то сложное и крутое из Гарварда/MIT/Америки, то имеем, что большинство проектов забиты сущностями, выполняющими одни и те же шаги - извлечение данных, но с разными сущностями. А сущностей в бизнес логике может быть несколько сотен.

Не уверен, что правильно вас понял, но попробую ответить. Да, в теории очень здорово писать send_super_secret(email : VerifiedEmailAddress) и сама система типов будет гарантировать нам, что письма будут отправляться только верифицированным адресам. Во многих случаях это очень уместно и элегантно. Особенно если тип почты не нужно менять динамически. Но здесь мы начинаем подмешивать бизнес-логику. Например, нам может понадобиться разрешать отправку верифицированным адресам, но только тем, которые были активны в последний месяц лунного календаря. Завтра требования могут ослабнуть, и необходимо будет отправлять не только верифицированным, но и бывшим премиум-пользователям. Я к тому, что в send_super_secret вероятно добавятся различные условия и желательно предусмотреть возможность их появления заранее. (Возможно здесь помогут контракты, но я не силен в этом)

Это проблема решается поддержкой контрактов в языке, примерно так

send_super_secret (email : EmailAddress)
        require email.isVerified
{
   channel.send(email, message)
}

А с введение интерфейсов Verifiable и поддержкой дженериков можно безопасно переиспользовать такой код

<T: Verifiable> send_super_secret (token : T)
        require token.isVerified
{
   channel.send(token, message)
}

UFO just landed and posted this here

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

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

-- десятичное число из 12 цифр с фиксированной точкой
type Money is delta 10**(-4) digits 12;
-- целое число от 0 до 10 тысяч включительно
-- переполнение выбросит исключение
type Count is range 0..10000;
-- беззнаковый байт, для него допустимо переполнение
type Byte is mod 2**8;
-- подтип типа Count
-- может автоматически расширяться до родительского типа
subtype SmallCount is Count range 0..100;
-- стандартный тип с плавающей точкой
type Float is digits 6 range -3.40282E+38..3.40282E+38

Контрактами называют подход, при котором функции имеют пред- и постусловия, выраженные в виде логических утверждений.


Как оно там реализовано: через скрытые завтипы, символьные вычисления, в рантайме или вообще как комментарии — вопрос исключительно реализации.

UFO just landed and posted this here

И тем не менее, если контракт проверяется в рантайме или даже не проверяется вовсе — он не перестаёт быть контрактом.

Да нет, verified именно email, а не аккаунт. Аккаунт часто уже есть и верифицирован (по уникальному коду в смс, например), а теперь пользователь добавил email и надо верифицировать и его. В целом же согласен - громодить иерархию классов тут не самое лучшее решение

очешуеть, надо было еще немного подробностей реализации заложить в имена типов.

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

Sleep(1000) сделает ваш код намного безопаснее в этом плане.

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

Равномерно распределённая случайная задержка не поможет, а вот что-то вроде распределения Паскаля (с редкими большими значениями и частыми маленькими), как мне кажется, сильно затруднит атаку по времени.

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

>безопасное сравнение хэшей
Можно подробнее?

Сравнение, которое всегда выдаёт результат за одно и то же время.

Например, с использованием такого тождества (a, b — хэши длиной L, a_ii-ый байт соответствующего хэша):

a=b \Leftrightarrow \bigvee_{i=1}^L\left(a_i \veebar b_i\right) = 0

Если длина хэша кратна 8, можно соптимизировать на операции с uint64, так что ещё и работать оно быстро будет.

Вот, например, реализация этого тождества: https://github.com/php/php-src/blob/master/main/safe_bcmp.c

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

Обычно сейчас используются constant time алгоритмы вроде Argon2

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

"user@server.domain".split("@")[0] // хоть и сложно читается, но просто пишется
"user@server.domain".Domain() // пишется не очевидно, откуда-то большие буквы, домен и поддомен объединены в общем нужно где-то искать доку

Дальше больше поводов для холивара)

  • Использование только примитивов - проще читать. Просто потому что заучить примитивы можно, а расширяемую систему типов нет. Ее можно либо знать если писал сам, либо каждый раз подглядывать, ибо в другой фирме уже будут другие типы.

  • Несмотря на улучшение безопасности, на деле одни уязвимости переходят в другие. Тут все просто, кода без багов не бывает, поэтому чем известней кодовая база, тем больше в базах уязвимостей записей о ней. По факту, работа с безопасностью перекладывается на разработчиков либ, и админов их обновляющих.

  • Инкапсуляция это зло. Очень прикольно, когда для валидации имейла у тебя есть очень мощная и страшная либа, с кучей регулярок, учета доменов в стиле IPv6 и т.д.. Однако, что-то всегда меняется, а что-то нет, и единственный способ проверить имейл по настоящему это отправить на него письмо. Это не очень очевидно, но представим что вы пытаетесь зарегать пользователя, js проверил имейл, и тот подошел, а вот бек как ни странно, на такой имейл отправлять не умеет, хотя это и правда существующий и валидный имейл. В общем валидация на беке отличалась от валидации на фронте, они обе инкапсулированы, поэтому никто не залез смотреть.

  • Еще про инкапсуляцию. Со сложными типами невозможно работать без доки. Где-то нужно иметь список всех методов, что они принимают и возвращают. И работа в стиле, пишешь код, альт-таб в браузер, снова в код - это не нян. Порой проще вместо доки использовать исходники. А поглядев на исходники, зачастую возникает желание вообще не использовать эту либу) Да и хорошие доки встречаются редко. В общем все круто, когда сделано идеально, однако в реальности так бывает только когда работают за идею, а сделанным за деньги, пользоваться можно тоже только за деньги.

  • Еще про инкапсуляцию. Часто в либах зачем-то используют private и protected, еще конечно константы и прочий сахар, мешающий все поломать. Это конечно прекрасно для разработки либы, однако для публики лучше все делать публичным XD это кажется очень плохой идеей по началу, однако будет меньше людей которые скопировали исходники или применяли хаки, чтобы сделать публичными один два метода, которые им зачем-то все же нужно перегрузить.

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

Дальше уже не столь холиварные рассуждения.

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

  • За все нужно платить. Готовый код - менее гибкий, нужно изучать, содержит баги.

  • За все нужно платить. Не готовый код - менее универсальный, нужно писать, содержит баги.

  • Далеко не везде нужно учитывать разницу в написании количества денег в Германии и Великобритании.

  • Очевидных названий методов, классов, констант - не бывает. (Всегда найдутся люди которым не понятно)

  • Очевидного поведения, не требующего документации - не бывает.

  • Хорошей понятной документации - не бывает.

А теперь рассуждения о том когда все же пора.

  1. Если 0.1% ваших пользователей генерируют больше вашей зарплаты, то пора писать код и под них.

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

  3. Если всем в вашей компании привычно, то можно и не то что в языке, но тогда вы привязаны к команде, а команда к компании.

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

  5. А если все же услышали, то в вашей компании есть более важные проблемы, чем плохой код.

Использование только примитивов - проще читать.

Проще, если это в одном месте. Если же мест много, а их в больших проектах очень много, то легче и правильнее написать один раз класс или функцию и дальше использовать везде. Это же азы.

У вас приоритет на то чтобы код было легко писать или читать?

> Инкапсуляция это зло

Пока не захочется что-то поменять в большой кодовой базе.

> Со сложными типами невозможно работать без доки

А со сложными типами которые спрятанны внутри примитивов можно без доки?

А еще можно делать простые типы. В примере с деньгами методов намного меньше, чем у Decimal.

Имена и сигнатуры методов это тоже доки. Автодополнение и переход к исходному коду не занимают много времени.

> Порядок должен быть в голове

Я работал в больших проектах. Чтобы понять какая логика скрыта за базовыми типами приходится тратить время. У класса обёртки перед этим есть огромное приимущество, можно догадаться по API. И это логичное место для документации.

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

Тут не в инкапсуляции проблема.

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

Тут такой момент. От рефакторинга вы не выиграете в безопастности, всё давно уже оттестированно в том чиле пользователями. И рефакторинг будет в разы дороже, чем елси бы делать внятную систему типов под доменную модель. Это надо делать сразу это не дорого. На тестировании быстро отобьётся.

Есть еше бонус по тестированию.

email.Domain() От этого кода можно ожидать что емейл будет провалидирован и метод уже оттестирован. Тестируется юниттестами.

"user@server.domain".split("@")[0]
Кстати домен будет user. Если будут две собаки, то будет неверное значение без ошибки.

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

Да у меня просто накипело, и на очередной депрессивный приступ, вызванный изменениями внешнего мира, наложилось.

А если чуть более серьезно. То видел много примеров, где ооп фанатизм очень вредил.

  1. Код битры, в которой приходится работать. Это сразу чтобы словить все помидоры. В битре есть компоненты, они не так давно стали переходить на классы. Основной рекомендованный путь модификации стандартных компонентов, это пара файликов исполняющихся после них. У этого подхода есть проблемы с ajax, ибо компоненты в битре это нифига не контроллеры, хотя последнее время и пытаются таковыми стать. В общем с переходом компонентов из тупо кода в классы открыло новый путь расширения, а именно унаследовать оригинальный класс, перегрузить один из методов и все счастливы. Вот только деление на публичные и приватные методы было сделано, не лучшим образом. Настолько не лучшим, что лучше бы все сделали публичным, я серьезно. А еще лучше если бы доступ к методам указывал на вероятность их поломки обновлением битры.

  2. Как-то опять таки на битре захотели построить нормальную систему. Федерального уровня. В общем одни классы упаковывали в другие, нифига не выносили код в абстрактные классы и т.д. Про именование методов я вообще молчу. Оно было в стиле

$Users = generator["Users"]; // тут было правило, что название переменной должно быть именем сущности
$list = $Users->GetUsersListByIds($ids, [$select, $filter, $order, $limitOrPage, $whatever]);
/* зачем-то есть название сущности в названии метода,
порядок других полей - случайный,
даже когда написал абстрактный GetListByIds все равно делали методы GetЧто-тоListByIds
*/

В общем там было очень много фигни, которая пыталась походить на что-то нормальное, а в результате отнимало время. Ни читать ни писать в таком стиле не хотелось почти никому, поэтому проект на полгода для одного человека, затянулся на два года для команды из менее десятка людей.
3. Самый спорный пример, ваша позиция скорее выдаст степень подсказок вашей ide. Иногда просто хешмапы оборачивают в классы. Все по красоте, геттеры-сеттеры для каждого каждого свойства, легко расширять, дополнять методами если вдруг нужно. Все очень правильно, ибо с такими классами не придется переписывать код, если вдруг понадобится их расширить. Хешмапы для этого явно не подходят. Казалось бы не на что ругаться. Вот только, эти классы никогда не потребуются как классы. С ними даже проще работать как с ассоциативными массивами (опять таки php), у массива легко посмотреть список ключей и значений, пройтись по ним всем и это все что нужно для этой сущности. Конечно в какие-то языки и редакторы встроены очень хорошие инструменты для работы с классами. Они подскажут название методов и кому-то по этим названиям легко ориентироваться, вот только для массивов уже есть куча готовых методов. И это я не к тому, что мол переписывайте классы на массивы, это я к тому, что класс не всегда лучше. Он очень часто лучше, но не всегда и не везде.

То видел много примеров, где ооп фанатизм очень вредил

Я тоже видел много отвратительного кода. В статье как раз расписаны примеры когда классы уместны. С фанатизмом нужно бороться образованием, а не отказом.

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

Кроме слова классы ничего общего с тем, про что статья нет. С тестами и классами вечно проблема, что их используют неправильно и это больно. Вместо того, чтобы понять как правильно делать от них отказываются.

Иногда просто хешмапы оборачивают в классы.

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

Вот только, эти классы никогда не потребуются как классы.

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

То видел много примеров, где ооп фанатизм очень вредил.
Код битры, в которой приходится работать.

Код 1С-Битрикс это последнее, что надо использовать как пример применения ООП.

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

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

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

Использование только примитивов - проще читать.

Искренне желаю вам не столкнуться с доказывающим обратное кодом

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

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

Очень много крайне спорных (хотяи интересных) утверждений. Но за поинт про фальшивый порядок - аплодирую стоя! Как же это верно...

Кроме того, не у всех валют хранится только два знака после запятой. У некоторых валют, например, у бахрейнского или кувейтского динара, их три.

Странный тезис, количество "копеек" в той или иной валюте, весьма опосредованно связано с количеством знаков которые нужно хранить. Например, официальный курс ЦБ (а они-то уж точно знают, сколько копеек в рубле) доллара на сегодня составляет 60,9013 рубля.

Значения integer не страдают от проблем с округлением, свойственных типам float и double, поэтому они предпочтительнее, чем числа с плавающей запятой.

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

var x = Money.FromMinorUnit(100, "GBP"): £1

var y = Money.FromUnit(100.50, "GBP"): £1.50

Вот прям очень интуитивно же:

var z = Money.FromUnit(150.50, "GBP"): £2.00

Хранение знаков для валют должно быть = количество знаков после запятой у валюты * 2.

Хранение знаков для валют должно быть = количество знаков после запятой у валюты * 2.

Так с какими криптовалютами можно и за 64 бита вывалиться.
Эфир - 18 десятичных знаков, некоторые вообще 30
https://docs.nano.org/protocol-design/distribution-and-units/#

Например, официальный курс ЦБ (а они-то уж точно знают, сколько копеек в рубле) доллара на сегодня составляет 60,9013 рубля

Курс доллара - не валюта, а соотношение валют. Тщательнее.

Это уже вопрос терминологии, что считать денежным значением. Курс доллара это стоимость одного доллара в рублях, цена, можно сказать.

И тут надо учесть, что цена рубля в долларах тоже точное значение 0.0164 доллара за рубль, а не обратная величина стоимости доллара в рублях.

Опять же цена акции Россети сегодня 0.5778 рублей. Это уже денежное значение или отдельная сущность Цена?

Это не вопрос терминологии, а принципиальный вопрос. Валюта, курс и цена - суть разные единицы, у них разные размерности, разное логическое значение, разное использование.

Меня тоже это всегда удивляло. Все верно. Только вот почему 4 знака после запятой? Не 5, не 15, не 1, а именно четыре?
Какой в этом скрытый смысл?
Не говоря уже о том, что практической пользы эта информация не несет. Основная задача курса - обеспечить конвертацию "арбузов" в "патроны", и при всем своем желании вы не сможете получить или отправить с помощью банковской системы 90.13 копеек - будет округление.

Потому что на торги выставляют суммы, а не 1 рубль. То есть, условно, меняют 3 рубля на 4 доллара, ставки идут миллиардами, поэтому эти 3 рубля дают возможность менять частями (в данном случае должна быть возможность из этих трёх поменять один) , тогда сразу менять должны были 3 на 4.02, а обмен 3 на 4 тупо запрещён. В итоге, ставки динамически меняются и при вашем подходе градация 0.01 для миллионной ставки даёт весьма приличную дельту и малую предсказуемость получаемой суммы, а счёт идёт на секунды. Поэтому меняют всю сумму ставки на любую величину, а курс обмена, это посто коэффициент.

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

Становитесь председателем центробанка и сделайте 15 знаков после запятой. 😎

var y = Money.FromUnit(100.50, "GBP"): £1.50

Не понял, почему полтора-то?

И в том же прммере, почему x был 1 фунт, а когда стали распечатывать, оказалось полтора?

Код учитывает инфляцию по ходу выполнения кода :)

Понимаю, что перевод, но очень хочется сказать "Раз такой умный, возьми и сделай".

Ещё один замечательный пример — деньги! Просто куча приложений выражает денежные значения при помощи типа decimal. Почему? У этого типа так много проблем, что такое решение мне кажется непостижимым.

Разумеется, для моделирования такого типа Money придётся приложить усилия, но после его реализации и тестирования остальная часть кодовой базы будет в гораздо большей безопасности.

Тут ведь нет ничего нового, все та же дилема как писать меньше кода и получить меньше ошибок в рантайме. Хорошо бы свалить все на компилятор, чтобы ошибок в принципе не возникало- добро пожаловать в Haskell, с его "маниакальной" строгой типизацией. Для языков с динамической типизацией тоже есть свои библиотеки описания и проверки типов. И если надоело, что код то и дело падает в рантайме, то без строгого контроля за типами данных не обойтись. Только ведь вот какое дело, работы это только добавит. Тут все как в поговорке: "семь раз отмерь ....".

Только ведь вот какое дело, работы это только добавит.

Работы по написанию - да. Работы по отладке - в каких-то случаях убавит заметно сильнее.

Да как-то не заметил особо добавившейся работы от того что я пишу на Scala и Rust, наоборот, тот факт что весь код гарантированно работает исходя из типов, а тесты нужно писать только на критически важную логику здорово уменьшает количество работы по сравнению с динамическими языками где нужно писать 4-5 строк юнит тестов на одну строку кода только для того чтобы быть уверенным что код валиден (не говоря уже о логике)

Возможно, для указанных проблем подошла бы не классическая система пользовательских типов на базе ООП, а конструируемая логическая система типов на базе OWL (например).

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

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

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

Отличная статья.

Добавлю пример из своей практики. Когда-то у меня в коде цвет был представлен в виде примитивного типа float3. На первый взгляд проблемы не было, т.к. все значения примитивного типа (даже отрицательные) могли быть цветом. Но были такие вещи:

float3 color = getColorDocument(…);
color = colorSpace.convertColorDocumentToColorLinear(color);
color = colorSpace.convertColorLinearToColorLAB(color);
color = someColorLABFunction(color);
color = colorSpace.convertColorLinearToColorDocument(color);

Здесь в коде перед последней строчкой я забыл сконвертировать цвет из цветовой модели LAB в цветовую модель Linear, и в программе появился труднообнаружимый баг. Кроме того, не понятно было, цвет какой модели принимают и возвращают функции: приходилось указывать это в комментариях.

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

struct ColorDocument{float3 rgb;};
struct ColorLinear{float3 rgb;};
struct ColorLAB{float3 lab;}

Но появилась новая проблема: как выполнять над структурами математические операции, которые легко делались над примитивными типами?

Поначалу я хотел всё сделать грамотно, в соответствии с системой типов. Например, разность двух объектов ColorLinear порождает объект типа ColorLinearDifference, который цветом уже не является, но его можно прибавить к ColorLinear и получить снова ColorLinear. Закопался на десятки типов и тысячи строк кода. Получилась полная фигня и куча boilerplate-кода.

В итоге выкинул всё, кроме приведённых выше простейших структур, и везде, где нужна математика, напрямую лезу в .rgb или в .lab. Так понятнее, хоть и не полностью «типобезопасно».

Зачастую есть класс Color, и из него достаются разные репрезентации. И создается класс из разных репрезентаций. И так везде. Можно сделать класс кластер под капотом. А можно прост хранить одну репрезенатцию и гонять туда-сюда.

И класс-кластер, и класс с одной репрезентацией имеют накладные расходы во время исполнения. Мне это не подходит. Кроме того, смена репрезентации иногда происходит с потерями. Например, если ColorDocument — это sRGB, то он не поддерживает WideGamut, HDR, и отрицательные цвета.

Поэтому каждая смена репрезентации в моём случае должна быть явной и обдуманной.

И класс-кластер, и класс с одной репрезентацией имеют накладные расходы во время исполнения. Мне это не подходит.

Расскажите на чём пишете и что за проект.

Работаю в Pixelmator Team (графические редакторы). Делаю в том числе работу с цветом (настройки цвета, цветовые пространства, обработка Raw…). Программирую на С++. Код должен работать и на Intel и на Arm (M1/M2, iPhone, iPad).

У нас жёсткие требования по скорости исполнения кода. Используем и CPU (SIMD+многопоточность) и GPU (Metal+CoreImage); всё это асинхронно.

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

Примеры из статьи лично меня не убеждают. Оборачивать строки в обертки это скука и так можно делать, кажется, вообще везде, где есть статические типы. За последний год программирования у меня получилось уместно добавить обертку ровно 1 раз. Даже грустно. Вроде хочу с типами дружить, а толком не получается.

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

Думаю, что лучший друг программиста не на идрисе это все таки дебаггер =)

Дебаггер, это чтобы разгребать последствия.

Примеры из статьи лично меня не убеждают

А такие вариант работы со строками? https://docs.oracle.com/javase/8/docs/api/java/nio/file/Path.html https://docs.oracle.com/javase/7/docs/api/java/net/URL.html

Здорово, конечно, когда на уровне типов можно выразить какие-то полезные свойства программы.

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

Статья прям совсем для новичков, где объясняется нужность ValueObjects.
Придется прочитать довольно много литературы и написать много кода, прежде чем это все усвоится на хорошем уровне.

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

Во-первых, его можно просто переполнить, и функция вернет истину.

Как вы "просто переполните" стандартный массив байт? Особенно учитывая, что его не вводит пользователь, а возвращает хеширующий алгоритм?


Во-вторых, если вы где-то по пути запихаете нул и замените хеш на нул в базе, то вуаля вы тоже сломаете вход в систему.

Снова — как? Это ж не-nullable колонка в базе, как вы туда null запишете? (А если она nullable, значит null там предусмотрен)


И как хакер заменит выход алгоритма хеширования на null?


И, главное, даже если хакеру что-то из перечисленного вами удастся — что он получит с того, что поломает авторизацию себе?

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

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

Спасибо, действительно интересно было почитать. Мне порой очень не хватает типа "телефонный номер". После прочтения стал задумываться над созданием такого в каком-нибудь из своих проектов.

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

Хорошо бы стандартную библиотеку с такими фишками, чтобы не писать велосипед!

Замечательные типы - это хорошо, но и сама система типов языка должна предоставлять такую возможность - ADT, pattern matching, желательно HKT или хотя бы GAT's.

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

И такой тип уже есть, называется URL. Например, емейл там представляется как mailto:user@domain.tld. Его можно тупо поставить в href, и даже можно добавлять ?from=noreply@company.com&subject=Hello&body=Hi%20there


Как и с почтовыми адресами и индексами, номерами телефонов, персональной информацией и проч., емейлы лучше всего вообще никак не валидировать, а хранить и использовать "как есть", т.е. как пользователь ввёл. Практически все валидаторы упускают какой-то хитрый сценарий использования и усложняют жизнь, а не упрощают. Например, я как-то использовал трюк с "+" в username при регистрации аккаунта, а потом не мог ни залогиниться, ни удалить аккаунт, потому что валидатор на странице логина решил, что это невалидный адрес, при том что при регистрации он был валиден.


«деловые» взаимодействия тоже можно выразить через систему типов. Пусть у нас будет второй тип с именем VerifiedEmailAddress.

Однажды верифицированный адрес не имеет гарантии, что он будет оставаться валидированным вечно. Тип VerifiedEmailAddress тут даёт ложную гарантию, поэтому практически бесполезен. Его просто нельзя нигде использовать, не обманывая себя. Достаточно флага в базе данных и проверки этого флага в переходе workflow, чувствительного к наличию верификации (обычно это завершение регистрации аккаунта или изменения емейла). Не нужно тащить бизнес-логику в типы, типы должны гарантировать факты о самих данных (допустимый диапазон значений и набор операций).

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

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

Как итог - берите и пишите красиво свои пет-проекты в областях, не требующих производительности, покажите пример. И перестаньте требовать от других. На практике самыми продуктивными и великими обычно являлись очень плоские, простые и приземлённые программисты (вспомним создателей C, Unix и Linux, можно продолжать - список длинный). У них было мало концепций и типов. Но они хорошо работали благодаря идеям взаимодействия и комбинирования, а не развесистым типам.

С паролем может быть еще хуже, из личного опыта. В проекте которым сейчас занимаюсь случайно получилось так что при восстановлении пароля в базу писался чистый пароль а не его хэш (что, соответственно, означало что A) пользователь не смог-бы войти сменив пароль, и B) было потенциальной уязвимостью), к счастью наш доблестный QA это увидел до выкатки. Как это получилось? Как оказалось, когда все три (инпут с фронта, хэш этого инпута, и хэшированный пароль из базы) все имеют сигнатуру `smthPassword: String` - весьма несложно случайно запихнуть не то значение не в то поле по невнимательности. Решилась проблема использованием тонкой newtype обертки HashedPassword(String), благодаря которой теперь нельзя запихнуть "сырой" пароль String в поле (или функцию) тип которого HashedString, нельзя по-незнанию/наивности сравнить хэш и сырой пароль напрямую, нельзя случайно вывести пароль (точнее его хэш) из микро-сервиса через RPC или записать его в лог, ибо ждя этого типа соответствующие фичи не имплементированы за ненадобностью, etc.

В том что касается первого примера с email - нужно, конечно, подходить к этому без фанатизма, но тем не менее между

//if user.email_verified { // ой, забыл/не знал что такое поле вообще есть
  send_sensitive_data_to(user.email);
//}

и

enum UserEmail {
  NotVerified { email: String },
  Verified { email: String },
}

// send_sensitive_data_to(user.email);
// нельзя, ибо send_sensitive_data_to() ждет строку, а тут какой-то UserEmail

// send_sensitive_data_to(user.email.email);
// нельзя, ибо такое поле есть только у конкретных вариантов а не у всего типа

match user.email {
  NotVerified { email: not_verified_email } => send_sensitive_data_to(not_verified_email);
  //...
}

второй вариант выглядит куда более надежным, так как когда ты не можешь использовать email напрямую (при условии, конечно, что в языке есть средства позволяющие это сделать, как например enum в Rust или closed/sealed типы в Haskell/Scala) и должен руками написать что ты хочешь отправить секретную инфу на конкретно непроверенный email - явно большинство людей заподозрят неладное, даже если они этот проект до этого в глаза не видели и даже не подозревали что есть такая вещь как верификация email.

что такое email.email? каким образом Verified появляется на объекте? что мешает не вызвать проверку Verified когда функцию переиспользуют в новом месте?

Плохо как и необъявленная сущность, так и лишняя сущность.

Весь вопрос: как понять, что останется сущностью через месяц хотя бы...

Я согласен с посылом статьи, что типы-обёртки — это классно. Но про VerifiedEmailAddress, который наследует EmailAddress, я вообще не понял. Какого типа будет поле email у класса User-то?

  • Если VerifiedEmailAddress, то что если он ещё не Verified?

  • Если EmailAddress, то при использовании всё равно нужно

if( email is VerifiedEmailAddress )

что по сути то же самое, что и

if( email.IsVerified() )
  • Если два поля, то это вообще жесть, да и всё равно вылезает что-то типа

if( verified_email != null )
Sign up to leave a comment.

Articles