Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

К вопросу о front-end’ах

with 4 comments

Да, я обещал написать про front-end’ы, так что вот. Для начала расскажу что это такое. Если коротко — можно писать код как plain text, а можно его писать с помощью редактора a la Microsoft Equation Editor. Именно это делает Mathematica, и получается неплохо.

Но мы начнем с простого, например с того факта что * не умножение а звездочка и было бы логичнее использовать \times или хотя бы \cdot. Я понимаю что кого-то нервирует что произведение x и y будет выглядеть как x×y, но это пожалуй единственный кейс который приходит в голову, и тут можно написать x \cdot y и все довольны.

Вы заметили что я начал использовать \LaTeX? Его Дональд Кнут изобрел чтобы математику верстать. Выглядит сугубо приличнее чем plain text Вот например хочу я, допустим, деление: конечно x/y выглядит более менее сносно, но \frac{x}{y} как-то солиднее. Или foo->bar() все же не так круто как \text{foo}\to\ bar().

Вообще идентификаторы символов тоже хочется иметь погибче, например почему я не могу назвать меременную C^1_k или же например \widetilde{abc}?

Все те условности к которым мы привыкли навязаны plain text представлением. И если отойти от него – да, нужно иметь специальные форматы файлов, но структурированный как дерево код легче обходить, так что плюшки тут очевидны. Это как раз то, что делает MPS, но можно сделать ещё круче.

Вообщем такие вот мысли…

Written by Dmitri

18 июля 2016 at 0:33

Опубликовано в Technology

Плохие и хорошие идеи в языках программирования

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

На самом деле нет. Как 5 лет назад было нормой 2-4 ядра, так и сейчас. И да, я знаю что есть процы подороже и посерьезнее, только вот косяк: подавляющее большинство людей нынче пользуют скорее макбуки нежели десктопные РС, а на них сколько ядер? 12? 16? Древнее индейское поселение фигвам.

Поэтому в этом посте немного троллинга “современных” языков. При этом я не жалуюсь, у меня все работает. Просто мысли в слух.

Дорогая, мы продолбали операторы

Даже не операторы. Вы на раскладку клавиатуры смотрели? Что это за !@#$%^&*()_+ мля? Я вообще не понимаю почему у нас на главном органе компьютера такой вагон легаси. Ладно, я вас не прошу с QWERTY следать, но хоть мусор-то можно было убрать?

Я сотственно вот о чем:

  • Единственный вменяемый оператор в ЯП это +. Всё. Кроме плюса все остальное – бред.

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

  • Деление и умножение тоже продолбано. С какого перепугу “звездочка” это умножение? Умножение это ×. Насчет деления конечно можно поспорить, может / без полноценного фронт-енда a la Mathematica — это приемлимо, но я не уверен.

  • Оператор = никаким образом не означает присваивание. Коротко, Delphi, Mathematica и прочие – правы, нужно разделять присвоение и определение, если вы уж решили использовать = для записи значения в переменную. Я конечно склонен думать что x := 2 это первичное определение переменной (даже auto/var не нужен) а для записи значения подойдет x <- 3 или 3 -> x (это кстати подход в F#, VHDL).

  • Использование = для сравнения — абсолютно нормальная практика (т.е. if (x = 3)) если бы не одно но — числа с плавующей точкой так сравнивать нельзя т.к. нужно указывать погрешность. Я бы, может, и не отказался вместо abs(x-y) < tol писать что-то вроде x =1e-10= y хоть это и выносит порядочно мозг.

  • Вот эта галочка ^ это либо оператор возведения в степень (ну или **), либо ничего. XOR из нее сделали безумцы.

  • Скобки продолбали все и полностью. Вы понимаете, что в математике разные скобки ([], {}, ()) все имеют право на жизнь и используются чтобы не запутаться в сложных выражениях? Так тогда зачем квадратные скобки узурпировали для массовов а фигурные — для scope? А представьте ЯП где можно использовать любые для выражений, плюс компилятор проверяет согласованность.

  • Вообще в математике оператор × можно опускать, и я склонен думать что выражение 3x в том же С++ или C#, при желании, нельзя мизинтерпритировать как ничто кроме 3×x. Также сюда можно приплести units of measure, чтобы можно было писать 52m вместо 52 mm. (И да, я понимаю что непонятно xy это переменная или x*y, но можно же из контекста выводить? Или опасно?)

  • F# узурпировал prime mark ' как символ, но я бы его взял как транспозицию, как делает MATLAB.

  • А еще я бы разрешил произвольные названия переменных, только не как в F# (ВОТ ТАК) а что-то вроде:

    struct Values { #1, #2:i32 }
    Values v; print(v.2)
    

    Кстати, писать v.2 можно в Rust, тоже неплохая возможность.

Косяки с именами типов в C++ и не только

С++ ставит рекорд по полному факапу системы типов. Что такое unsigned long long? Никто не знает, поэтому мы и используем <cstdint>, но можно лучше (спасибо Rust):

  • Целочисленные типы = знаковость (i/u) + размер (сколько вешать в битах), например i32, u64 итд.

  • Для чисел с плавающей в луже точкой просто делаем f32/f64 (спасибо за вынос мозга, F#). Эй SG14, где там 16-bit float?

  • Если вам вдруг нужны пакованые SIMD типы, их можно добавить как p128/256/512. Но это экзотика. Придется вводить векторные операторы (.+, .*, и так далее) как это сделал MATLAB.

  • Использование этих коротеньких типов как постфиксы (var z = 3.0f32) — гениально, спасибо Rust.

  • Целочисленные переменные isize/usize с размером, равным слову проца — это очень умно, опять же спасибо, Rust. И что еще более гениально, так это то, что в Rust индекс массива — unsigned. А в С++ вполне можно написать x[-1] т.к. это всего лишь смещение указателя.

Поддержка векторно-матричных операций

Если все так пекутся о многопоточности и векторизации, почему нигде кроме MATLAB никто не напрягся впаять вектора/матрицы в язык? Более того, я бы предложил:

  • Улучшенную инициализацию, то есть

    X = [1 2; 3 4]; // instead of {{1,2},{3,4}
    X = [ 1 2
          3 4 ]; // also works, kind of
    

  • Вменяемые скалярные и векторные произведения, как сделал MATLAB — X*Y и X.*Y как векторное и Адамарово произведение соответственно.

  • Маски для определенно-нулевых элементов матрицы чтобы сократить кол-во операций если у вас, например, треугольная матрица.

  • Статические и динамические размеры. Очевидно но все же, хочется хороший API. То, что сейчас в С++ это адъ.

И ещё…

Ладно C# запилил observer pattern как event (немного неуклюже, конечно), но было бы неплохо получить binding operator =:= который жестко связывает два значения. А там и до резолва сложных выражений недалеко. Вообщем в современном мире это актуально.

Да, и неплохо было бы оставить операторы < и > в покое и не использовать их для шаблонов. Хотите шаблоны? Используйте « и » и мне плевать что этих символов нет на клавиатуре, т.к. клавиатуры ущербны по определению. И нет, я не предлагаю адъ вроде APL, я скорее предлагаю просто делать уже языкам графические front-end’ы как Mathematica.

Но об этом в следующем посте. ■

Written by Dmitri

4 июля 2016 at 20:40

Опубликовано в Programming

Tagged with

Задача про 2008

Этот пост — репост одного моего поста который был на Hexlet до того как Hexlet решил изобрести себя снова и сдвинул мой курс в «песочницу». Он немного технический, и я не хочу его терять. Надеюсь вы поймете. К сожалению, комментарии оригинального поста потеряны, но с этим ничего не поделать.

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

Как с помощью 13 (тринадцати) нулей и простых математических операций получить число 2008?

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

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

Вещественность

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

Самое очевидное решение — это 0^0 = 1 (это кстати спорно и вызвало много дебатов), но это в результате дает нам \left \lfloor{\frac{13}{2}}\right \rfloor = 6 нулей из которых, как вы понимаете, 2008 никаким образом не сделать. Должны ведь быть и другие варианты? Попробуйте угадать, какой оператор делает из нуля единицу?

Конечно же это факториал. Факториал — это обычно произведение всех чисел от 1 до n, так что например 3!=3\times2\times1, но у факториала есть одна особенность: 0!=1, а следовательно у нас теперь 13 единиц, а с ними уже можно работать. И кстати, пока не поздно, обращу внимание на то, что в условии задачи я упомянул простые математические операции. Если бы можно было использовать, например, логарифмы, то задача уже решена т.к. любое натуральное число можно выразить через логарифмы, корни, и три двойки (доказательство), а соответственно решение нашей задачи выглядело бы вот так:

\displaystyle 2008 = - \log_2\left(\log_2\left(\underbrace{\sqrt{\sqrt{...\sqrt{2}}}}_{2008}\right)\right)

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

Возведение в степень

Итак, нужно получить 2008. Самое очевидное — это найти близлежащее число 2^n. В данном случае, это 2^{11} и соответственно мы можем подобраться к решению вот так:

\displaystyle \begin{aligned}  2008 &= 2^{11} - 40 \\  &= 2^{2^3+3} - 5\times2^3  \end{aligned}

К сожалению, числа 2, 3 и 5 идут «по себестоимости» (т.е. чтобы сделать 5 нужно 5 нулей), и соответственно в 13 нулей мы ну никак не уложимся: для решения выше нужно аж 20 нулей!

Соответственно нашей первоочередной задачей становится поиск более «дешевых» чисел, и тут на помощь приходят…

Факториалы

Факториалы дают нам не только единицы с которыми можно работать, но также дешевые крупные числа. Например, 3!=6 а это значит что за три нуля мы можем получить не только 3 но также 6=3!, 720=(3!)!, и так далее. Если форма записи X_Y обозначает что число X требует для записи Y нулей, то мы получим следующие значения:

\displaystyle \begin{aligned}  0_1 =& 0 \\  1_1 =& 0! \\  2_2 =& 0! + 0! \\  3_3 =& 0! + 0! + 0! \\  4_4 =& 3 + 0! = 2^2 \\  5_4 =& 3! - 0! \\  6_3 =& 3! \\  7_4 =& 3! + 1 \\  8_5 =& 3! + 0! + 0! = 2^3 \\  9_5 =& 3^2  \end{aligned}

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

Тем временем, мы можем начать подбираться к результату: например, разложив 2008 на множители, мы поймем что

\displaystyle \begin{aligned}  2008 &= 2^3 \times 251 \\  &= 2^3 \times (216+36-1) \\  &= 2^3 \times (6^3 + 6^2 - 1) \\  &= \underbrace{2^3 \times \left(6^2(6+1)-1)\right)}_{14}  \end{aligned}

Увы, решение выше требует на один ноль больше, чем у нас имеется.

Другой подход к решению задачи — это разложить на множители не 2008 а, скажем 2008\pm10. Вот что нам дает MATLAB:

1998=2   3   3   3  37
1999=1999
2000=2  2  2  2  5  5  5
2001=3  23  29
2002=2   7  11  13
2003=2003
2004=2    2    3  167
2005=5  401
2006=2  17  59
2007=3    3  223
2008=2    2    2  251
2009=7   7  41
2010=2   3   5  67
2011=2011
2012=2    2  503
2013=3  11  61
2014=2  19  53
2015=5  13  31
2016=2  2  2  2  2  3  3  7
2017=2017
2018=2  1009

Из интересных чисел тут, пожалуй, 2000 и 2016. Также соблазнительно число 1998 (у него есть множитель 37=3!^2+1). Возьмем сначала 2000:

\displaystyle \begin{aligned}  2000 =& \underbrace{2^4\times5^3}_{12} \\  =& \underbrace{2\times\left(2^{3}+2\right)^3}_{11}  \end{aligned}

Это решение нам явно не подходит т.к. 2008 = 2000+8_5 т.е. для решения нужно 16 единиц. Попробуем 2016:

\displaystyle \begin{aligned}  2016 =& \underbrace{2^3\times6^2\times7}_{14} \\  =& \underbrace{6^4+(3!)!}_{10}  \end{aligned}

Но даже так, у нас остается три нуля, а 2008=2016-8_5, то есть для этого решения нам всяко не хватает двух нулей. Что же, подход с обычными (намек!) факториалами почти себя исчерпал, давайте еще попробуем что-нибудь сделать с числом 1998:

\displaystyle \begin{aligned}  1998 &= \underbrace{3!^2\times\left(3!^2+1\right)}_{11}  \end{aligned}

Это достаточно бессмысленное занятие т.к. двумя нулями лишние 10 не набрать, но, по крайней мере, мы выловили тот факт, что возможно стоит попробовать 3!^4:

\displaystyle \begin{aligned}  2008 &= 3!^4 + 712 \\  &= \underbrace{3!^4 + (3!)! - 2^3}_{15}  \end{aligned}

Увы и ах. Как я уже говорил в самом начале, число 2008 выбрано неслучайно — это число вставляет максимальное число палок в колеса. Нужен какой-то трюк чтобы снизить «стоимость» простых чисел вроде 8. К счастью, такой трюк имеется, и это…

Двойной факториал

Двойной факториал n!! — это тоже произведение всех чисел от 1 до n, но с шагом 2, т.к. например 5!!=5\times3\times1=15. Само по себе это кардинально меняет картину, и я хочу обратить ваше внимание на два равенства:

  • 8_4 = 4!!, то есть восьмерка стала «дешевле» на один ноль.

  • 48_3=(3!)!!

Итак, делим 2008 на 48 и получаем 41.8(3), а поскольку 42=6\times7, мы наконец-то можем попробовать получить ответ:

\displaystyle \begin{aligned}  2008 &= 48\times42-8 \\  &= \underbrace{(3!)!!\times\left(3!\times(3!+1)\right)-4!!}_{14}  \end{aligned}

Но что-то тут не так: 42 нам далось адским трудом, мы заплатили за него аж 7 нулей. На один ноль поменьше и все получится. На самом же деле, 42=48-6=(3!)!!-3! и вот, ура, у нас готово первое решение:

\displaystyle 2008 = \underbrace{(3!)!!\times\left((3!)!!-3!\right)-4!!}_{13}

Субфакториал

Разных факториалов бывает много и субфакториалы — это такой особый тип факториала, который определяет количество беспорядков порядка n. Высчитывается он так

\displaystyle !n=n!\sum_{k=0}^n \frac{\left(-1\right)^k}{k!}

Прелесть этого факториала в том, что он в очередной раз дает нам другие равенства, например 265_3 = !(3!). А это в свою очередь заставляет нас в очередной раз посмотреть на разложение 2008 на множители:

\displaystyle \begin{aligned}  2008 &= 4!! \times 251 \\  &= 4!! \times (!(3!) - 14) \\  &= \underbrace{4!! \times \left(!(3!)-5!!+1\right)}_{13}  \end{aligned}

Оптимизация задачи

Кому-то может показаться, что 13 нулей — это предел мечтания, но нет — вот например пара решений, где используются только 12 нулей:

\displaystyle \begin{aligned}  2008 &= \underbrace{(!5)^2 + \sqrt{\left((3!)!+1\right)} + 1}_{12} \\  &= \underbrace{\left(!(4!!)-1\right)\times2+5!}_{12}  \end{aligned}

Предлагаю игру в стиле code golf — оставляйте в комментариях решения, которые используют 11 нулей и меньше, посмотрим чье кунг‑фу лучше. No cheating! ■

Written by Dmitri

15 февраля 2016 at 0:39

Опубликовано в Mathematics

Tagged with

Runtime Compiled C++

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

166 of 3863 functions ( 4.3%) were compiled, the rest were copied from previous compilation.
4 functions were new in current compilation
335 functions had inline decision re-evaluated but remain unchanged

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

Вообщем, рассказ-то у меня не про это, а про runtime-компиляцию, про которую я уже часто говорил под термином ‘динамическое прототипирование’ — вот тут у меня .NET-ное видео есть, можно посмотреть как это делать на C#-е.

Вообще этот подход хорошо иллюстрирует зачем вообще нужен Dependency Inversion Principle — да-да, именно inversion а не injection. Если попытаться объяснить этот принцип словами, получится что-то вроде этого:

Делайте Extract Interface на любом классе, по поводу и без повода. Замените все типы с Foo на IFoo. Если кому че не нравятся, пусть пишут официальную жалобу, в дубликате и с ксерокопией паспорта.

Вот как-то так. А вы думали, зачем Extract Interface? Конечно чтобы в рантайме заменить один тип на другой. Как? Объясняю:

  • Вы сделали объект типа Person.

  • В рантайме вы поменяли объект типа Person, скомпилировав еще один такой же но с пофикшенной багой.

  • Теперь у вас два объекта типа Person, но это разные типы!

Никакого вам duck typing’а! Следовательно, вы в принципе не можете зависеть от Person, а только от некого интерфейса IPerson который, кстати, менять нельзя если только вы не абстрагировались еще выше (но боюсь что это непрактино в 99% ситуаций).

А что в C++?

Да по сути то же самое, только еще проще т.к. ничего не надо делать, умные дяди уже все сделали. Естественно, что этот подход сугубо intrusive, т.к. это «плюсы» и тут нету reflection и иже с ним.

  • Файлы которые нужно подменить помечаются специальным макросом REGISTERCLASS.

  • В момент запуска вашей проги, система строит табличку того какой класс где определен.

  • Классы, которые хочется менять в рантайме есессно не положено инстанцировать «в лоб», так что

    IObjectConstructor* pCtor = m_pRuntimeObjectSystem->GetObjectFactorySystem()->GetConstructor( "RuntimeObject01" );
    if (pCtor)
    {
    	IObject* pObj = pCtor->Construct();
    	pObj->GetInterface( &m_pUpdateable );
    	if( 0 == m_pUpdateable )
    	{
    		delete pObj;
    		m_pCompilerLogger->LogError("Error - no updateable interface found\n");
    		return false;
    	}
    	m_ObjectId = pObj->GetObjectId();
    }
    

На этом этапе у кого-то может политься кровь из глаз, но я напоминаю, что это C++. Выше происходит следующее: мы через нашу runtime систему находим фабрику которая инстанцирует объект и вызываем конструктор, а потом пытаемся подкрутить его к интерфейсу — если получилось, сохраняем также id объекта чтобы его потом можно было правильно удалить.

Значит теперь к объекту мы можем достучаться только через его интерфейс. Интерфейс — это просто структура с pure virtual функциями, наподобии вот этой:

struct IUpdateable : public IObject
{
  virtual void Update(float deltaTime) = 0;
};

IObject — это важный базовый класс инфраструктуры. Ну так вот, что там дальше?

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

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

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

Вообщем, все это очень интересно и, резюмируя, является еще одним козырем в рукаве, наряду с Cling, обертыванием C++ либ в Питон, и прочими извращениями. Enjoy!

Written by Dmitri

2 февраля 2016 at 2:01

Опубликовано в С++

Tagged with

Заметки про Rust

Содержание

Я не знаю как с экономической точки зрения в России можно не работать треть месяца, но ладно — для многих водка решает, а тех кому нечем заняться ждет обзор языка Rust с точки зрения человека который пишет в основном на C++/C# (хотя я теперь модный и знаю сколько-то Kotlin’а, что тоже полезно, т.к. Java трогать ну совсем не хочется).

Так вот, давайте по порядку и с заголовками, чтобы красиво было. (Да, подсветка синтаксиса тут во F#-у, т.к. wordpress.com пока не разродился на Rust.)

Примитивные типы

Начнем с целочисленных типов: тут все хорошо и стандартизированно: вместо всяких unsigned long long которые в тех же плюсах вообще непонятно что значат, в Rust тип u8 значит ‘unsigned и 8 битов’ что очень понятно, в принципе, ну и по аналогии i32 это 32-битный int.

Есть также usize/isize – это биты размера системы, то есть если вы на 64-бит (я надеюсь), и что хорошо, так это то что в отличии от C++, где есть весьма существенные непонятки с типом size_t, а также языками C++/C#/Java, где индекс массива может быть signed, в Rust-е индекс массива это строго usize. Это же здорово!

Да, насчет чисел с плавающей точкой — тут f32/f64, все просто.

Операторы

Все как в С-образных языках, но жестко выпилены операторы ++/--, а логические операторы можно применять только к булевым переменным и, наоборот, побитовые – только к целочисленным (то есть к f32/64 не получится). Это хорошо т.к. снижает кол-во возможных непоняток.

Переменные

Объявление переменной в Rust несложно, например:

let mut a:i32 = 2+3*4;

Тип, как видите, идет после названия. В примере выше он не нужен т.к. type inference в Rust очень умен и умеет делать вывод на этапе вызова (я позже покажу какого это).

Переменные в Rust — хитрые твари потому что:

  • Примитивы выделаются на стэке если только ты их вручную не задвинешь в кучу через Box<>
  • By default немутабельны, но могут быть сделаны таковыми с помощью ключевого слова mut
  • Владеют той памятью на которую ссылаются. Об этом позже т.к. идея сложная

Помимо локальных есть еще глобальные переменные:

  • Константы которые просто инлайнятся и сами адреса не имеют
    const MEANING_OF_LIFE:u8 = 42;
    
  • Статические переменные, которые можно даже делать мутабельными, но если ты так сделал – то придется ващевеськод оборачивать в unsafe т.к. система не может мониторить что ты правильно работаешь с глобальной переменной, в которую вообще все могут писать
    static mut Z:i32 = 123;
    

И да, язык хочет чтобы глобальные переменные были константами. И вообще, code style inspections встроены в сам компилятор что, имхо, дурной тон и так вообще не надо делать.

Control flow

If

Тут много интересного. Во-первых, нет тернарного оператора ?: — всесто этого if возвращает значение:

let day = if temp > 20 {"sunny"} else {"cloudy"};

Где return, спросите вы? А вот… в старой доброй традиции терминального ввода, MATLAB’а и прочих, правило Rust такое – если после чего-то нет точки с запятой ;, то это – return value. По мне так достаточно косячно т.к. ну очень плохо читается. Вариант использовать return тоже есть, конечно, но уверен что растаманы будут ругаться.

Да, эти if-ы всегда требуют фигурные скобки вокруг блоков, но зато не требуют круглых скобок вокруг проверки условия.

While/loop

Что касается while, то тут все то же самое, continue и break работают. А вот do-while не сделали, зато сделали loop который чем-то аналогичен while(true) или for(;;). И это в языке который печется о безопасности.

For

Цикл for работает как-то вот так:

for x in 1..11 
{
  // skip 3
  if (x == 3) { continue; }
  // stop at 7
  if (x == 8) { break; }
  println!("x = {}", x);
}

Как вы догадались, 1..11 это range, то есть набор чисел от 1 до 10 включительно. Такой вот «обходной» подход к for сравним с обычным IEnumerable, то есть ничего особенного, просто очередная редукция энтропии. Кстати, 11..1 не получится и приведет к косякам, ну и шаг хождения (как 1:101:2 в MATLAB) тоже указать нельзя.

А да, и если нужен индекс итерируемого элемента, есть специальный метод который вернет вам не просто элемент, а кортеж индекс-элемент:

for (pos,y) in (30..41).enumerate()
{
  println!("{}: {}", pos, y);
}

Match

Теперь насчет switch — его нет, есть match, и он раз в 100 мощнее. Ну вот например он умеет обрабатывать range’ы:

let country_code = 999;
let country = match country_code 
{
  44 => "UK",
  46 => "Sweden",
  7 => "Russia",
  1...1000 => "unknown",
  _ => "invalid" // try commenting this out - must cover all cases!
};

И да, он тоже, как и if возвращает значение, которое можно присвоить. Только вот тут есть один косяк… приглядитесь! Видете диапазон 1...1000? Там три точки. А ранее было две! И ранее 1..11 означало от 1 до 10 включительно, а тут — от 1 до 1000 включительно. Такой вот когнитивный диссонанс. И причем авторы языка это специально сделали, «чтобы люди не путались». Ну-ну.

Структуры данных

Массивы

Начнем с массивов. Ну вот как-то так:

let mut a/*:[i32;5]*/ = [1,2,3,4,5];

Тип массива я, опять же, закомментил — вывод типов и тут работает на ура. Массив на стэке. Можно менять по индексу, при неправильном индексе получим панику. Есть метод len() для длины, т.е. ходить по массиву можно так:

let b = [1u16; 10];
for i in 0..b.len()
{
  println!("{}", b[i]);
}

Не знаю как вам, а мне форма записи 0..b.len() как-то не очень. Зато синтаксис выше — [1u16; 10] — это для заполнения десяти элементов массива одним и тем же значением. Удобно.

Многомерные массивы тоже реальны как массивы массивов:

let mtx:[[f32; 3]; 2] = 
[
  [1.0, 0.0, 0.0],
  [0.0, 2.0, 0.0]
];
println!("{:?}", mtx);

Слайсы

Слайсы – это как бы ссылки на куски массива, т.е. диапазоны памяти. То есть, если у нас есть массив, можно его кусок одолжить и че-то с ним поделать:

fn use_slice(slice: &mut [i32])
{
  println!("first elem is {}, len = {}", slice[0], slice.len());
  slice[0] = 4321;
  // will crash
  //let z = slice[10];
}
fn slices()
{
  // a slice is part of an array
  // its size is not known at compile time
  let mut data = [1,2,3,4,5];
  
  // start w/o mut, borrow as a slice
  use_slice(&mut data[1..4]);
  use_slice(&mut data); // entire array
  println!("data after slice use = {:?}", data);
}

В отличии от массивов, слайсы могут быть хз какого размера.

Вектора

Вектор — дженерик тип Vec — это динамический массив, причем в куче. Можно добавлять и удалять данные.

let mut a = Vec::new();
a.push(123);
println!("a = {:?}", a);
let idx:usize = 0;
println!("a[0] = {}", a[idx]);

Из кода выше должен возникнуть резонный вопрос: где название типа данных? Почему мы можем вообще вызывать Vec::new() если нигде нет аннотаций типа? Ответ на этот вопрос – очень умный вывод типов Rust’а.

Да, поскольку в Rust есть Option<T>, доступ к элементам безопасен и может быть сделан вот так:

match a.get(5)
{
  Some(x) => println!("a[5] = {}", x),
  None => println!("error, no such element")
}

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

Да, насчет итерации вектора, тут все просто:

for x in &a { println!("{}", x); }

Вот этот амперсанд & перед названием ветора перед операции означает «одолжить». То есть на момент итерации, вектор «одолжен». Да, и варианта менять вектор во время итерации тут нет, спасибо Rust’у с его безопасностью.

Что еще? Вектор ведет себя как стэк (т.е. имеет push/pop методы). Вот если нужно например его опустошить в обратном порядке, можно так:

while let Some(x) = a.pop()
{
  println!("{}", x);
}

Выше — немного магии. Как это у нас в while внезапно не true/false а Option<T>. А вооот! Rust поддерживает if let/while let, которые проверяют на None/Some<T>. Очень удобно!

Строки

Тут все хорошо и плохо одновременно. Хорошо потому что Rust гарантирует что строки это всегда валидные UTF-8 последовательности, а плохо потому что есть два типа строк — String и &str.

Начнем с &str — это т.н. ‘string slice’, но не в классическом смысле: слайс строки как Vec<u8> крайне бессмысленнен т.к. это байты Юникодной последовательности, а &str – это особный тип, который дает доступ к строке, которую можно разбить на последовательность буков и делать с ними что-то:

let s:&'static str = "hi there!";
// s = "bar"; // cannot reassign immutable
//let a = s[0]; // cannot index
  
for c in s.chars().rev() // reversed! also as_bytes()
{
  println!("{}", c);
}

В примере выше, мы статически аллоцируем текст и берем его как &str а дальше с помошью chars() разбираем на буквы и делаем что хотим.

А теперь про String — вот этот тип как раз для того чтобы менять, т.е. это мутабельный Vec<u8> в который можно аппендить и вообще:

let mut letters = String::new();
let mut a = 'a' as u8;
while a <= ('z' as u8)
{
  letters.push(a as char);
  letters.push_str(","); // note the _str
  a = a+1;
}
println!("{}", letters);

Вообщем, со строковыми типами в Rust много чехорды, люди пишут умные блог-посты на эту тему, и толком непонятно что юзать – зависит, конечно, от того будете ли вы вашу строчку «шарить».

Option<T>

Это специальная структура данных (аналог boost::optional или F#‘ному Option<'t>) которая является перечислением, и имеет два возможных значения

  • None — значит «данных нет» и тут нечего ловить.
  • Some(T) где T — это то значение что вернули.

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

let x = 3.0;
let y = 0.0;
let result:Option<f64> =
  if y != 0.0 { Some(x/y) } else { None };
match result {
  Some(z) => println!("{}/{}={}", x, y, z),
  None => println!("cannot divide {} by {}", x, y)
}

Ну и опять же, растаманская магия дает сразу выписать результат, если он есть, вот так:

if let Some(z) = result { println!("result = {}", z); }

Кортежи

Не, ну а как же без них? Хотите вернуть из функции и сумму и произведение двух чисел? Пожалуста:

fn sum_and_product(x:i32, y:i32) -> (i32, i32)
{
  (x+y, x*y)
}

Теперь можно их выписать например вот так:

let x = 3;
let y = 4;
let sp = sum_and_product(3, 4);  
println!("sp = {:?}", sp);
println!("{0} + {1} = {2}, {0} * {1} = {3}", 
  x, y, sp.0, sp.1);

Не нравится индексиция через точку? Вы еще С++ не видели. Ну ладно, ладно, давайте тогда деструктурируем:

let (a, b) = sp;
println!("a = {}, b = {}", a, b);

Кортежи-кортежей тоже в принципе реально, но написать foo.1.2 вам никто не даст:

let sp2 = sum_and_product(4,7);
let combined = (sp, sp2);
println!("last element is {}", (combined.1).1); 

Зато деструктуризация кортежа-кортежей выглядит красиво:

let ((c,d),(e,f)) = combined;

Что еще: а, да, кортеж это ж где разные типы можно хранить. Вот, пожалуйста. И да, одноэлементные кортежи немного кривовато делаются:

let foo = (true, 42.0, -1i8);
let meanings = (42,);

Структуры

Старый добрый сишный struct, никакого буллшита:

struct Point
{
  x: f64,
  y: f64
}
struct Line
{
  start: Point,
  end: Point
}

Методы в его тело не добавляются, а конструкторов-деструкторов как таковых нет как феномен, инициализация идет как-то вот так:

let p = Point { x: 3.0, y: 4.0 };
println!("point p is at ({},{})", p.x, p.y);
let p2 = Point { x: 5.0, y: 10.0 };
let myline = Line { start: p, end: p2 };

Для страктов, как и для кортежей (и всего остального) тоже работает деструктуризация. Как это выглядит мы увидим когда посмотрим на…

Перечисления (enum-ы)

Старый добрый… а, что, не такой? А, ну да. Enum в понимании Rust – это совокупность просто лейблов, кортежей и struct’ов, типа используйте что хотите (и да, generic enums тоже возможны, смотрите на Option). Вот например как можно описать цвет:

enum Color
{
  Red,
  Green,
  Blue,
  RgbColor(u8,u8,u8),
  CmykColor{cyan:u8,magenta:u8,yellow:u8,black:u8},
}

Ну а для разбора такого счастья можно использовать всю мощь pattern-matching’а:

let c = Color::CmykColor{cyan: 0, magenta: 128, 
  yellow: 0, black: 255};
match c
{
  Color::Red => println!("r"),
  Color::Green => println!("g"),
  Color::Blue => println!("b"),
  Color::RgbColor(0,0,0) 
  | Color::CmykColor{black:255,..} 
    => println!("black"),
  Color::RgbColor(r,g,b) => println!("rgb({},{},{}", r, g, b),
  _ => ()
}

Заметили .. выше? Это не range, конечно, это описание того что «пофиг чему равны другие поля».

Функции

Тут все, вообщем-то, просто:

Просто функции

Функция начинается с fn, далее берет один или несколько аргументов (тип, как всегда, после имени):

fn print_value(x:i32)
{
  println!("value = {}", x);
}

Функции могут также возвращать значения: тип возврата пишется через стрелочку:

fn product(x: i32, y: i32) -> i32 // return value
{
  let z = x * y;
  z // no semicolons
}

Возврат, как уже говорил, идет того значения, у которого нет ;.

Аргумент можно передать «по ссылке» – для этого используется & как на типе аргумента так и в функции, а для «дереференса» ссылки используется *. То есть поведение совсем уж в разрез с С++.

fn increase(x: &mut i32) // start with i32
{
  *x += 1;
}
let mut z = 1;
increase(&mut z); // lend z

Методы

Помните я сказал, что у struct’ов как таковых нету собственных функций? Но их можно добавить! Делается это вот так:

struct Point
{
  x: f64,
  y: f64
}
struct Line
{
  start: Point,
  end: Point
}
impl Line
{
  fn len(&self) -> f64
  {
    let dx = self.start.x - self.end.x;
    let dy = self.start.y - self.end.y;
    (dx*dx+dy*dy).sqrt()
  }
}

Ключевое слово impl позволяет определить реализацию того или иного метода для структуры. Заметьте как, выше, функция sqrt() вызывается на типе f64! Хотя я конечно же мог бы вызвать ее как f64::sqrt(), тут как кому удобнее.

Замыкания

Ну вот есть у вас функция:

fn say_hello() { println!("hello"); }

Вы можете взять и сделать из нее переменную. И потом вызвать ее как функцию:

let sh = say_hello;
sh();

Это никого не должно удивлять. Как и вариант создания таких переменных прямо в месте где хочется их использовать. Тут у нас Ruby синтаксис! Серьезно, вот:

let plus_one = |x:i32| -> i32 { x + 1 };
let a = 6;
println!("{} + 1 = {}", a, plus_one(a));

То есть plus_one это вполне себе функция. Сразу вопрос: а что с захватом окружения? А вот тут включается весь злой арсенал проверок Rust: если что-то из окружения захвачено, мы не отпустим пока сама лямбдочка не будет удалена. Как? А вот так:

let mut two = 2;
{
  let plus_two = |x|
  {
    let mut z = x;
    z += two;
    z
  };
  println!("{} + 2 = {}", 3, plus_two(3));
}
let borrow_two = &mut two;

Выше — искусственно сделанный scope, который гарантирует что plus_two будет удалена и, тем самым, «отпустит» two, которую она захватила.

Вот как-то так: передали контроль над переменной: все, пиши пропало. Поэтому лучше одалживать.

Время жизни

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

Владение

Каждая переменная «владеет» контентом, но поведение отличается в зависимости от того, на стэке она или в куче. Вот если она в стеке, например

let v = vec![3,2,1];

то это значит что такое вот невинное, казалось бы, присваивание как

let v2 = v;

означает фактически, что v2 забирает управление памятью, и что отныне переменную v использовать нельзя, т.к. у нее «забрали» управление. Это значит что вот это не скомпилируется:

println!("{:?}", v);

То же самое происходит если лямбду написать как

let foo = |v:Vec<i32>| ();
foo(v);

Поскольку функция foo забрала вектор, использовать его пока лямбда в scope — нельзя.

Это поведение на языке C++ называется move semantics, т.е. объект как бы «перемещается», а старая его копия больше не валидна. Работает это, правда, только для тех типов которые не определили трейт Copy (о трейтах позже). Для примитивов работает этот трейт, и у нас копирование вместо move’а:

let u = 1;
let u2 = u;
println!("u = {}", u); // компилится без проблем!

Как вернуть управление из функции? Ну, можно вот так:

let print_vector = |x:Vec<i32>| -> Vec<i32>
{
  println!("{:?}", x);
  x
};

Но это гемор, поэтому Rust вводит такое понятие как…

Одалживание (borrowing)

Итак, мы с вами договорились до того, что в любой момент только одна переменная «владеет» доступом к данным, а все другие — в пролёте. В терминах C++, можно сказать, что передача такого параметра — это передача unique_ptr.

Что делать если хочется просто поработать над объектом (т.е. shared_ptr)? Тогда можно передать ссылке на него, и эта ссылка означает «одалживание» (borrowing) объекта:

let print_vector = |x:&Vec<i32>| // take a reference
{
  println!("x[0] = {}", x[0]);
};
let v = vec![3,2,1];
print_vector(&v);
println!("v[0] = {}", v[0]);

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

Lifetime

Вы не видите lifetime’ов точно так же, как на выводе типов вы не пишете вручную аннотаций. А они есть. Например, типичная функция выглядит вот так:

fn bar<'a>(x: &'a mut i32) -> &mut i32 // lifetime elision
{
  x
}

Вот это вот <'a> может показаться type parameter’ом из F#, но нееет, это время жизни, и оно подчиняется разным хитрым правилом. Есть время жизни 'static, которое обозначает жизнь программы. Другие определения — это на ваше усмотрение.

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

Всякая всячина

Крейты (ящики)

Rust поставляется с хитрой системой сродни NuGet’у. Проектным файлом служит файл в формате TOML, который описывает не только то, что мы строим (не беспокойтесь, файлы руками описывать не нужно), а также зависимости которые взяты из репозитария crates.io или откуда-то ещё.

Соответственно, помимо компилятора, есть еще crate.exe, который может как собрать ваш проект, так и запустить его или, например протестировать.

Модули

В Rust все делится на модули (ключевое слово mod) которые можно либо держать в одном файле либо распихать по файлам и папкам. Вот например тут

pub mod greetings
{
  pub mod english;
  pub mod french
  {
    pub fn hello() -> String { return "bonjour".to_string(); }
    pub fn goodbye() -> String { return "au revoir".to_string(); }
  }
}

описан файл lib.rs (значит будет собрана DLLка), и хоть модуль french включен прямо тут, модуль english лежит в файле greeting/english.rs. Convention over configuration, однако!

Ключевое слово pub определяет, что видно наружу, а что нет. Импортируется и ипользуется это очень просто:

extern crate phrases;
use phrases::greetings::french;
fn main() {
    println!("English: {}, {}", 
      phrases::greetings::english::hello(), 
      phrases::greetings::english::goodbye()
    );
    println!("French: {}, {}", 
      french::hello(), 
      french::goodbye()
    );
}

Ключевое слово use — что-то вроде using namespace в C++.

Да, забыл сказать — пакеты компилируются на стороне пользователя, т.е. поставляются как сорцы. Упс!

Тестирование

Как и язык D, Rust просто влепил тестирование прямо в суть языка на уровне набора атрибутов. Вот например

#[cfg(test)]
mod tests
{
  extern crate phrases;
  #[test]
  #[should_panic]
  #[ignore]
  fn english_greeting_correct()
  {
    assert_eq!("helloo", phrases::greetings::english::hello());
  }
}

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

Документация

Специальные комментарии /// для кусков кода и //! для всего модуля целиком дают rustc.exe сгенерить красивую документацию по вашему коду. Highly recommended!

Заключение

Я тут постарался как-то описать Rust по сравнению с C#/C++. Не могу сказать что хочется вот прямо взять и начать на нем писать, но некоторые умные идеи я из него почерпнул. ■

Written by Dmitri

4 января 2016 at 23:02

Опубликовано в Technology

Tagged with

Отсутствие итогов 2015 года

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

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

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

Насчет гаджетов всяких и программулек. Ну что тут сказать? Я записал сколько-то видосов для SpbDotNet (.NET сообщества в Питере) на Sony α7Rii, это лучший фотоаппарат на сегодняшний день и еще долго таким будет. И вообще я много на него уже пофоткал и очень доволен, хотя нет предела совершенству.

Насчет языков программирования — на D в уходящем году выло написано ровно ничего. На F# — только фиксы в MathSharp по требованиям пользователей. Покупки прог в этом году шли прятным бонусом, Брайан Даунинг даже сделал обзор X2C у себя в видео, заодно простимулировал меня пофиксить несколько проблем с разным легаси вроде Excel функций про которые я вообще не знал.

В технологическом плане в ушедшем году интересного было очень мало. Вышел iPad Pro который я не купил (но собираюсь), так что я периодически покупал книги по финмату и ножи. Да, я люблю ножи.

Да, все-таки удалось попробовать Rust, но выводов пока нет. Я тяготею к тому что приносит деньги, так что мне сильно отъезжать от канонов негоже. К тому же, С++ радует, я посетил ряд конференций — это C++ Russia, ACCU, CppCon, Meeting C++ и на двух последних даже сделал доклады. Ну были и другие конфы, всего по мелочи.

Что ещё? Блин, все-таки хочется влиться в финмат тусовку по полной, но мы уже вошли в какую-то зону апатии, когда мы на поезде с которого уже не сойти, а последняя остановка называется retirement, причем не в смысле «пенсий» — у меня ее нет, и у вас скорее всего тоже — а скорее в смысле «лежать на пляже на канарах». Канары хорошие, да, годное место.

Чё-то как-то сумбурно получилось, и даже удивительно что вы дочитали досюда. Наверное в новом году нужно делать какой-то reboot и начать писать про финмат, правильное использование статистики, и прочее добро. Только дойдут ли у меня руки? Вот не уверен.

Вообщем… всех с новым 2016! Не знаю как вам, а мне кажется что все будет просто супер!

Written by Dmitri

31 декабря 2015 at 23:59

Опубликовано в Life, Technology

Tagged with

Монада Maybe на языке C++

Монада – это вообщем-то просто паттерн, но из функционального программирования. А поскольку в C++ есть и лямбды и всякие функциональные объекты, почему бы не построить монаду? Не обещаю что будет красиво, но все же.

Сценарий

Монада Maybe – это такой подход при обработке присутствия или отсутствия того или иного поля у класса. Вот например допустим мы моделируем человека и у него есть адрес:

struct Person
{
  Address* address = nullptr;
};

Сам по себе адрес – это там всякие номер дома, улица и так далее, но если вы мажор (или Enya) и вы купили замок, то у него вместо улицы и номера доме есть просто имя. Соответственно имеем:

struct Address
{
  string* house_name = nullptr;
};

Тут и выше, использование raw pointer’ов – оно намеренное. Можно было бы вместо них использовать shared_ptr или boost::optional, об этом поговорим позже.

Итак, допустим мы хотим написать функцию которая, если ей скормить указатель на Person, печатает имя дома если таковое имеется. Простая реализация будет выглядеть как-то так:

void print_house_name(Person* p)
{
  if (p!= nullptr && p->address != nullptr && p->address->house_name != nullptr)
    cout << *p->address->house_name << endl;
}

Думаю проблема ясна – все эти проверки на nullptr утомительны и хотелось бы че-то попроще. Ну как сказать попроще… поумнее что ли.

Реализуем Maybe

Итак, мы будем строить экскаватор, т.е. класс который умеет копать вглубь объектов и при этом проверки на nullptr в него встроены, т.е. их не нужно делать самому. Начнем с того, что сделаем просто класс с указателем:

template <typename T> struct Maybe
{
  T* context;
  explicit Maybe(T* const context)
    : context{context}
  {
  }
};

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

Person p;
Maybe m{p}; // не сработает 
            // придется писать Maybe<Person> m{p};

Это как-то уныло, в связи с чем мы реализуем helper function который займется собственно выводом типов:

template <typename T> Maybe<T> maybe(T* context)
{
  return Maybe<T>(context);
}

Немного грустно писать подобные вещи, но что поделать. Теперь мы можем написать Person p; maybe(p) а дальше вызывать функции на свежеиспеченном объекте. Что за функции? В вот тут все самое интересное.

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

maybe(p)
  .With([](auto x) { return x->address; })
  // а что тут - потом узнаете

Тут правила такие:

  • Если текущий контекст не nullptr, то можно исполнять лямбду, и возвращать Maybe<Address> с установленным контекстом
  • Если текущий контекст уже nullptr, то вызывать лямбду нельзя, но возвращать контекст все еще нужно

Печаль состоит в том, что нельзя просто вернуть текущий контекст, т.к. Maybe<Person> и Maybe<Address> – это разные типы. В связи с этим мы получим что-то вроде

template <typename Func>
auto With(Func evaluator)
{
  if (context == nullptr)
  {
    // а вот тут будет интересно
  }
  else
  {
    return maybe(evaluator(context));
  }
}

Итак, вроде как возврат значения в случае если «все ОК» идет через функцию, которая тут шаблонным параметром представлена (были попытки использовать std::function, но оно в контесте шаблонов с лямбдами не дружит). Так вот, тут все как бы ОК, но что насчет возврата контекста если у нас уже nullptr? По идее нужно вернуть всего лишь Maybe<T>{nullptr} где T – это тип контеста, который использует скормленная нам лямбда. Проблема в том, что

  • У нас нет T в явном виде
  • Зато мы знаем что функция, которую нам скормили, возвращает T*
  • Следовательно, удаляем указатель, и вперед:
  
template <typename Func>
auto With(Func evaluator)
{
  if (context == nullptr)
  {
    return Maybe<typename remove_pointer<decltype(evaluator(context))>::type>(nullptr);
  }
  else
  {
    return maybe(evaluator(context));
  }
}

Как видите, получилось немного адово, но оно работает. Теперь можно писать

maybe(p)
  .With([](auto x) { return x->address; })
  .With([](auto x) { return x->house_name; })

и углубление в структуру произойдет только если текущий контекст не nullptr.

Icing on the Cake

Обычно в монаду Maybe добавляют еще всякого мусора для просто обработки, if-ов и всякого такого. Например, добавим в нашу монаду функцию Do(), которая будет просто выполнять некую лямбду:

template <typename Func>
auto Do(Func action)
{
  if (context != nullptr) action(context);
  return *this;
}

Выше, как видите, мы как раз можем возвращать *this потому что контекст мы в этой функции не меняем. Теперь мы можем дописать нашу оригинальную функцию:

void print_house_name(Person* p)
{
  maybe(p)
    .With([](auto x) { return x->address; })
    .With([](auto x) { return x->house_name; })
    .Do([](auto x){ cout << *x << endl; });
}

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

Дискуссия

Я тут очень синтетично использовал nullable конструкты, что в реальной жизни используют разве что в языке С, в котором все равно подобную штуку не построить. В реальной жизни можно строки очень часто просто держатся by value, то есть ну будет пустая строка, но никак не null, и соответственно придется уже разбирать тот факт что она пустая.

То же самое насчет просто объектов, которые часто не T* а shared_ptr<T>/unique_ptr<T> или даже boost::optional<T> и соответственно проверять их уже нужно по-другому.

Вообщем вот как-то так. Спасибо коллегам из R++ за помощь с конструированием этой монстрятины. Да, с «методами расширения» да более краткими лямбдами было бы еще круче, я знаю! ■

Written by Dmitri

24 декабря 2015 at 14:01

Опубликовано в С++

Tagged with ,

Отслеживать

Настройте получение новых записей по электронной почте.

Присоединиться к ещё 146 подписчикам