Марк Шевченко — Воркшоп «Практические задачи решаем функционально» (Часть 2)

Марк Шевченко — Воркшоп «Практические задачи решаем функционально» (Часть 2)57:24

Информация о загрузке и деталях видео Марк Шевченко — Воркшоп «Практические задачи решаем функционально» (Часть 2)

Автор:

DotNext — конференция для .NET‑разработчиков

Дата публикации:

05.04.2024

Просмотров:

467

Описание:

Классический REST API тестируют с помощью curl или Postman. Более новый gRPC тестировать сложнее, потому что на входе и на выходе у него бинарные данные. Нужна утилита, которая умеет сериализовать текстовые данные в бинарные и десериализовать их обратно. Задача кажется сложной, потому что языки описания схемы и данных Protobuf — достаточно развитые. Но решается она просто, если пользоваться правильным инструментом. Мы напишем утилиту на языке программирования F#, используя библиотеку FParsec. Научимся по описанию грамматики писать код и тесты для разбора, построим абстрактное синтаксическое дерево и разберёмся, как применять его для сериализации.

Транскрибация видео

Спикер 3

Дорогие друзья, с возвращением.

Мы продолжаем наш воркшоп, посвященный функциональному решению задач.

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

Марк, пожалуйста, тебе слово.

Спасибо, Филипп, спасибо.

Спикер 6

Ну что, давайте я кратко напомню.

на чем мы остановились.

А мы остановились на том, что мы немножко не успели там доделать.

И то, что нам нужно сделать, это несколько текстов перетащить из нашего вот этого файла с копипастами.

Так.

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

Схема состоит из сообщений, перечислений, намс и каких-то опций.

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

То, что надо было сделать, это нужно было перетащить все файлы Proto3, это наше описание структур.

Потом у нас есть парсеры, которые умеют это все разбирать.

И мы это все перетаскиваем в файл, который называется Proto3 Parser.

Спикер 4

Вот он.

И, наконец, у нас есть тесты, которые мы тоже очень любим.

Спикер 6

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

Так.

У нас все парсеры покрыты тестами.

Это, кстати, очень интересный момент.

Так, неправильно нажал.

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

Вы пишете тест, делаете какое-то предположение, пишете код и проверяете, правильно ли у вас распарсился файл.

Так.

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

То есть он весь состоит из таких вот, то, что мы называем proto-items, ну или отдельные элементы файлов.

Вот наш финальный парсер, который называется proto, выглядит так.

Он пропускает пробелы, если они есть в начале.

Потом он обязательно синтекс проверяет, что есть такая конструкция.

Помните, мы ее писали.

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

И потом сколько-то proto-items, элементов прототипа оно описывается, прото-буфа.

А эти элементы, это что?

Это у нас просто точка запятой, это пустой элемент, потом импорт, package, option, определение enum, определение message.

Вот эти только элементы могут встречаться.

Вот мы это описали.

Финальный тест.

Тест, как я говорил, он уже такой интеграционный.

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

После того, как я все скопировал, у меня должны тесты все пройти.

Стало гораздо больше.

Они проверяют все парсеры, которые мы дописали.

Так, у нас их много, и они все есть.

И давайте посмотрим на коды тестов сейчас.

Спикер 1

Угу.

Спикер 6

Он огромный.

Дело в том, что коды тестов, ну, тесты должны быть маленькие.

Обычно они должны быть маленькие, но проблема в том, что это юнит-тесты.

Иногда вы пишете интеграционный, то есть тест, который проверяет целиком все, что вы сделали.

В конце прошлой части я там вкратце это все попытался показать.

Давайте посмотрим уже точно.

Значит, мы описываем тестовый метод, помещаем его в категорию интеграционный.

Это вот здесь вот есть такая

Конструкция trade attribute.

И мы описываем внутри у нас просто обычным текстом.

Мы в виде строки, на самом деле, описываем message файл.

Это proto файл, один из таких, который можно найти на просторах интернета.

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

То есть в данном случае дерево состоит из отдельных элементов.

Из такой конструкции, как package, из такой конструкции, как option, из такой конструкции, как import.

И несколько месседжей будет.

В месседжах есть имя, например, mail request и fields, поля.

У каждого поля тоже есть имя.

И есть какой-то тип, на который оно может ссылаться.

Иногда это строка, иногда это целое число, иногда это другой тип.

тип, другое сообщение.

Вот как здесь, например, поле user ссылается на сообщение типа user.

У всех полей есть обязательный номер, он должен быть.

Мы в теме про сериализацию будем рассказывать.

В общем, то, что здесь у нас, это такая вот структура данных, которая создана там.

Message fields превращается в proto-месседж.

Или опшены какие-то тоже превращаются в proto-опшен.

огромный такой момент.

Вот этот код...

Один раз сложно написать, но потом он просто проверяет в целом, что у нас все нормально, что мы ничего не сломали.

Мы тесты, если будем эти запускать, у нас как раз это нам и покажет.

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

У вас готовый код, готовая программа.

Вы прочитали строку из файла до конца, отдали этому парсеру, и он вам вернул эту структуру.

И наша следующая задача – это с этой структурой что-то научиться делать.

То есть одно дело мы распарсили, хорошо.

Но все ли это из того, что нам нужно?

На самом деле нет.

На самом деле нет.

Это не все.

Сейчас поставлю еще раз.

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

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

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

Но для нас это неудобно.

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

У нас есть вот этот вот протофайл, в нем описывается схема, в нем какие-то есть элементы, например, месседж.

В месседже есть имя, а еще у него есть поля.

Это тоже вложенные какие-то узлы, ну а дальше за узлами ничего нет.

И вот это вот некая структура.

А нам реально для...

Работы нужно будет что-то вроде этих, как это, мэпов, dictionaries, хеш-таблиц.

То есть у нас есть хеш-таблица с названиями месседжей и потом описание этих месседжей.

И есть хеш-таблица с описанием энумераторов и этих перечислений, энамсов.

И описание каждого инамса.

И вот по идее нам в таком виде схема нужна для сервизации и десервизации.

Как из одного сделать другое.

Давайте мы прям на готовый ход посмотрим.

Спикер 4

Для этого нам сначала сюда надо вернуться.

Спикер 6

Так.

Еще два файла.

Мы просто должны... Три файла.

Мы должны скопировать непосредственно с сайта.

Дело в том, что мы описали структуру

Схемы, ну то, что описывает схему сообщений.

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

Мы не будем прямо вот с нуля это делать, мы просто возьмем готовые файлы и все, и их просто подставим.

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

Значит, мы их просто возьмем и вставим.

Текст-прота.

Если кто делает, вы просто заходите на GitHub,

Нажимаете кнопку row, вот у вас в текстовом виде представление весь файл целиком.

Спикер 7

Так, это четвертый.

Этот программ должен быть здесь.

Спикер 4

Это абстрактное синтаксическое дерево для описания текстовых данных.

Спикер 6

Потом мы добавляем парсер к этому делу.

Спикер 4

Он будет называться TextProtoParser.

Спикер 7

Вот он здесь у нас в коде.

Спикер 6

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

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

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

Можно использовать один и тот же код, ну, как бы,

Чтобы его не дублировать.

Но я этого пока не делал.

Про проект хочу сказать, что он еще не завершен.

Он как бы в работе.

Все важные вещи, которые нам нужны, они в нем готовы.

Мы все это посмотрим.

Но всегда есть что дописывать.

Можно, скажем, порефакторить, попрощать код.

И не факт, что некоторые решения здесь останутся в том виде, в котором вы сейчас все это видите.

Тестовые файлы я всегда называю так же, как, ну то есть одному модулю в исходном коде соответствует один модуль в тестовом файле.

И он называется точно так же, и добавляю слово tests.

Это позволяет очень легко понимать, к какому модулю какие тесты относятся.

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

Нет, что-то ему...

Спикер 7

Вот здесь вот забыл поставить на свое место.

Спикер 6

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

Вы можете потом по исходному коду походить.

Он не очень большой.

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

Так.

Спикер 4

Следующий момент.

Это.

Вот это.

Мы скопировали тест экспрот.

Это у нас какой был?

Оу.

Сколько мы уже прошли всего.

Спикер 6

Так.

Давайте сообщение, текст прото и серилизация, десерилизация.

Да сколько ж можно-то, я все мотаю, мотаю, все никак не могу домотать до нужного места.

Схема.

Вот.

Значит, история со схемой.

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

Ну что это может быть?

Это может быть байт-код какой-то, это может быть...

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

Мы сейчас их просто отсюда скопируем.

Спикер 4

Схема builder, схема builder tests.

Спикер 7

Угу.

Спикер 4

По папочке protos ищем файл, который называется схема builder.

Спикер 7

Мы на него сейчас посмотрим.

Спикер 6

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

Так, он должен быть повыше здесь.

Спикер 4

И тесты к нему.

Без тестов никуда.

Хотя на самом деле, ладно, давайте без тестов.

Нам там что-то интересное будет.

Я вот не уверен.

Ага, ну в целом просто проверяем, что все работает.

Спикер 6

Ладно, давайте эти тесты пока не будем тогда использовать.

Нам хватит только схема билдера.

Посмотрим, что это такое.

Схема билдера это такая штука, которая умеет строить, сейчас, вот так включим, умеет строить схему.

Схема, она описывается вот как такой тип из трех полей, ну это как структура или как класс.

Там есть сообщения, в которых поля доступны по имени.

Поле называется named messages.

Потом сообщения, в которых поля доступны по номеру.

При сериализации нам нужны по имени, а при десерилизации по номеру поля внутри сообщения.

Поэтому мы создаем и тот, и другой вариант.

И enums, которые были описаны.

И билдинг происходит достаточно просто.

Например, у нас есть...

Метод, который называется buildNamedMessages.

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

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

У нас используется функция, которая называется ListChoose, она из стандартной библиотеки.

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

И результатом работы функции будет что-то такое с...

Кортеж, тюпл из двух элементов.

Из имени, месседж, сообщения.

И из списка полей.

Точно такой же.

А, внизу потом у нас мэп есть используется.

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

В общем, это самая структура данных.

Точно так же у нас строится совершенно аналогичным образом...

Сообщения, где доступны по номеру поля.

Только мы сохраняем не имя, а номер.

Вот здесь вот.

Отличие буквально в двух строках.

Ну и с иномумами то же самое.

Теперь у нас общий построитель схемы.

Это такая функция, которая...

Пробегается по всем сообщениям, всем элементам файла внутри и отбирает из них сообщения.

Это вот MessageChooser, а вторая отбирает enum.

И у нас структура заполняется так.

Мы берем все элементы из файла.

Сначала выбираем из них все сообщения.

Потом для каждого сообщения вызываем buildNamedMessage.

То есть у нас из той структуры данных строится новая структура.

Потом точно так же buildNumberedMessage и buildEnum.

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

И вот у нас готовая схема.

Код получается совсем небольшим, 83 строки.

Это наша компиляция.

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

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

Тут опять-таки можно посмотреть, насколько удобно функционально писать такие штуки.

То есть смотрите, у нас три, в общем-то, достаточно небольшие функции.

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

Но это ломается в течение двух недель.

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

Это читается сверху вниз довольно легко.

То есть у нас есть функция, и вот эти штуки, которые стрелочки здесь, это так называемые пайпы, это тоже то, что есть в функциональном программировании, то, что потом перетащили в такие языки, как C-sharp, Java и C++, и там это называется Fluent API, то есть такая точечная аннотация, когда у нас метод, точка, следующий метод, точка, следующий метод, вот так вот.

Спикер 4

Идею взяли вот как раз отсюда.

Спикер 1

И

Спикер 6

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

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

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

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

Тоже большой плюс.

Так.

Но я тесты не скопировал для схемы BuilderL.

Ничего, значит, запускать не будем.

И следующий этап у нас, то, чем мы должны будем заниматься.

Вот хорошо, мы построили нашу схему.

Это у нас была компиляция на самом деле.

Вот сейчас то, что мы сейчас делали, три функции билда, это компиляция.

И у нас остается финальная часть, реализация.

Значит, смотрите, мы построили эту схему для...

Для файлов, которые называются proto, а для текст-proto не нужно никакую схему строить.

Почему?

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

Это просто список полей, а нам как раз это и нужно.

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

Там слишком много данных ненужных.

Хорошо.

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

И тут я вам открою страшную тайну.

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

Для сервера, для клиента на разных языках.

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

То есть это не то, что мы сейчас пишем.

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

В общем, она находится вот там вот, где...

В вашем домашнем каталоге есть папочка New Git Packages, gRPC Tools, там какая-то номер версии, Windows 64, там находится файл протаси.

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

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

Спикер 4

На вот эту страницу.

Спикер 6

Это тоже из гугловской документации.

И здесь вот как раз расписаны примеры, как мы сервизуем и десервизуем данные.

Вот мы сейчас будем с этим работать.

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

Все.

Теперь у нас сервизация и десервизация.

Вот что нас волнует.

Так.

Мы подходим уже там в финишный прямой.

Спикер 4

Схема сервизации.

Ага.

Спикер 6

Значит, я вот этот файл Proto.c запустил и получил вот эти бинарные данные.

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

Это вот такое сообщение distance request, в нем два поля from и to, а в каждом из полей latitude и longitude.

Это все с данными, которые реально координаты центра Питера и центра Москвы.

После серилизации вот в такую форму, то есть это 40 байт.

И у нас вот эти 40 байт должны получиться.

Я их взял, и они у нас будут в тесте использоваться.

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

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

И вот их там шесть штук.

И вот эти шесть штук мы их описываем как enum.

Она называется wire type, типа способ, тип телеграфирования, наверное, это можно было бы если в прямой переводить, ну или тип сериализации.

Значит, есть такой varint, это переменные длины для сохранения маленьких целых чисел в маленьком количестве байт.

I64, I32 это вот такой фиксированный, то есть ровно 64 бита или ровно 32 бита.

LAN это для чего-то такого, что похоже на, у чего есть длина.

То есть это строки, это другие сообщения, это массивы байт.

И S-групп, E-групп, их уже не нужно, они деприкейт, это абсолют, в общем, это устаревшая штука.

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

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

И давайте посмотрим, как у нас работают разные типы.

Вот varint, как он сериализует и десерилизует.

Вот у нас есть число 20 тысяч.

Снизу вы видите бинарное представление.

Это 15 битовое число.

Мы разбиваем его на кусочки по 7 бит.

Вот видите, если вы посмотрите, как раз правых 7 бит, потом следующих 7 бит.

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

Три полноценных семибитовых байта, так нельзя сказать, да?

Семибитовых значений.

Сохраняется это в порядке little and then, то есть самые младшие значения должны храниться в начале, а старшие в конце.

Это вот фиксировано так.

Поэтому мы просто все меняем местами.

Видите, у нас младший 7-бит переехали справа налево, а старший слева направо.

Вот.

И теперь мы в каждое значение добавляем старший бит.

У нас три числа.

Значит, все, которые имеют продолжение, у них в начале единица.

А у финального в начале ноль.

Поэтому у нас получается, видите, они у меня не залезли все три в одну строку, ну так вот, не получилось.

Поэтому второй, он немножко ниже.

Но вы имейте в виду, что это вот подряд идущие все-таки три.

Это уже три байта.

У первого и у второго в начале единичка.

Ну вот красным цветом она обозначена.

То есть старший бит, единичка.

Это значит, что дальше есть еще число.

У последнего ноль.

И таким образом мы в 3 байта можем 20 тысяч сохранить.

Ну такой вот не очень простой вариант.

Как это все выглядит на F-sharp?

Ну на самом деле не очень сложно.

Мы используем...

Ну, во-первых, здесь момент тонкий у нас есть.

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

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

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

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

Но меня это не так пугает, потому что не так страшно.

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

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

Ничего страшного, если у нас там выделится лишний 200 килобайт.

Никто от этого не умрет.

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

Поэтому здесь код с побочным эффектом.

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

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

Следите за изменениями в этом файле, подключайтесь, если вам интересно.

Теперь рассмотрим, как это работает.

Очень интересный момент.

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

То есть в данном случае что мы делаем?

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

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

Второе название у нее это fold.

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

У нас есть начальное значение, некий state, у нас есть функция генератор, и эта функция генератор должна генерировать следующий byte и следующий state.

В принципе, код тоже не очень сложный.

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

И как только у нас на очередном этапе следующее число, следующее...

Остаток становится равным нулю, значит мы дошли до конца, то есть мы все разделили.

И на каждом этапе вот это то, что 128 мы получили, мы смотрим, а это у нас финальный байт или не финальный, финальный это когда мы следующее число равно нулю, next reminder равен нулю.

И вот значит мы таким образом просто генерируем новые состояния.

В одном случае мы добавляем старший байт, вот здесь операция 0x80 UY.

Это так описываются байтовые литералы в F-Sharp.

Она добавляется, а в других случаях не добавляется.

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

Но в принципе он получается короткий, и он делает именно то, что нам нужно.

Так, и давайте мы с вами сейчас это просто все... Создадим эти два файла и туда скопируем их с... Этот модуль называется SERD.

И, наверное, вы так вот сталкивались регулярно, это было...

Сериализация, десериализация.

Она называется в самых разных языках программирования именно так.

Даже не в языках, в библиотеках, где угодно.

Так, вот он у нас написанный есть.

Это у нас самый крупный большой модуль.

Нет, наверное, даже не так.

Мы сделаем row.

Потому что в нем реализована и сериализация, и десериализация.

Там на самом деле довольно много логики.

Поэтому он у нас такой большой получился.

Сколько?

342

Две строки.

Но, в принципе, если подходить по уму, его можно разбить на две части.

То, что относится к сервизации, и то, что относится к десервизации.

Будет где-то там пополам.

Спикер 7

И точно так же мы тесты скопируем.

Тесты нам, наверное, все-таки потребуются.

Спикер 6

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

Сейчас разобраться осталось только с...

Спикер 7

Мне нужны тесты.

И мы их должны вставить на свои места.

Здесь и здесь.

Пробуем запустить, убедимся, что все нормально.

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

Так.

Спикер 4

Так, тесты прошли с успехом.

Ну, давайте мы будем смотреть все-таки, что у нас, как работает сервизация и десервизация.

Мы описываем вот этот вот var type.

Спикер 6

Так, наверное, нам лучше будет сделать вот... Вот смотреть в режиме презентации.

То есть тип сервизации.

Вот у нас, пожалуйста, serialize вариант, который получает число какое-то.

Очень часто мы можем не писать...

типа переменных, а он выводит вверху, видите, написано uint64 stream unit, это значит, что функция получает два параметра uint64 stream, а возвращает unit, ну то есть ничего.

Описываем вот этот генератор внутри, функция, функция, которая на каждом шаге один следующий байт генерирует и уменьшает вот этот вот остаток remainder, все время делит на 128, пока он не достигнет нуля.

Мы передаем этот генератор в функцию unfold, это библиотечная функция.

Вот она какая.

И начальное состояние false и n. Значит, вот у нас будут записываться байты.

Хорошо, если мы познакомились, что у нас с тестами на этот счет?

У нас же ведь тест должен быть.

Давайте посмотрим.

Так, мы давайте уберем все ненужные файлы, которые мы сделали.

Но мы про них уже поговорили, и нам они больше не интересны.

Мы оставим только сериализацию и десерилизацию.

Спикер 7

Поехали.

Вот, здесь интересный новый способ.

Спикер 6

Именование тестов или создание тестов.

Вот раньше у нас были тесты, это отдельные методы.

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

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

Следующий ретрит у нас будет в ноябре, пока непонятно где.

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

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

Вот, она мне рассказала, что сейчас чуть более другой способ именования тестов используется, то есть они на Java это делают, они описывают класс на Java, который называется что-то-что-то, ну там какой-то другой класс или другой метод, должен, should

А название уже отдельных методов, они там, должен сохранять байт 50 для числа 80, для исходного числа.

И таким образом у нас внутри этого модуля одного класса несколько тестов, которые относятся к нему к...

к функции serialize var int.

Вот она мне это рассказала.

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

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

Я не знаю, делает ли так кто-нибудь еще.

Но мне понравилось.

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

Хорошо, значит, у нас должен...

Если у нас будет число 80, оно должно сохраниться как 5, 0.

А если будет число 150, то оно должно сохраниться как 96, 0, 1.

Почему?

Я сейчас покажу просто.

Нет, это не здесь.

Это вот здесь.

Вот этот пример, который у нас в самом начале мы разбираем...

Берем число 150 и пытаемся его, вот 150 они пишут, а оно должно в результате превратиться в 0.8, 96, 0.1.

Но 0.8 это тег, это не значение поля, про теги мы чуть попозже поговорим.

Значит реально оно должно превратиться в 96, 0.1.

Если мы будем использовать этот метод в INS,

И вот, значит, мы это заранее знаем.

А 80, оно почему просто будет в 50 превратиться?

Потому что 50 это и есть 80.

То есть для маленьких чисел число само себе равно.

Ну, то есть 80 это десятичная система, а 50 это шестнадцатеричная.

Ну, по сути, вот числа меньше 128, они будут записаны как есть.

А больше они, значит, соответственно, превратятся в 2 байта.

Ну, давайте мы просто проверим вот этот модуль, все модули, все, да, все три метода сработали.

То есть сериализацию мы сделали правильно.

Несмотря на то, что код сложный.

Вот, кстати, интересный момент.

Отрицательное число, если мы пытаемся минус 2 сохранить, то оно сохраняется аж в 10 байт.

Спикер 4

Кто знает почему?

Это вопрос к залу, чтобы вы могли там как-то участвовать.

Спикер 6

Ну, я просто расскажу.

Это число, если оно в дополнительном коде хранится, оно вот такую двоичную форму имеет.

минус 2 с основанием 10 равно вот огромному числу с основанием 2.

И, естественно, это не очень удобно, поэтому есть еще способ

Цивилизация называется signed integers.

Вот здесь описано в таблице.

0 это 0, минус 1 это единица, а 1 это 2, а минус 2 это тройка.

То есть видите, у вас слева идут подряд отрицательные, соответствующие положительные, следующие отрицательные, следующие положительные.

А кодируется это просто последовательными натуральными числами.

Вот для этого нам тоже нужен код.

Так, сейчас.

Спикер 7

И он у нас, конечно, написан в соседнем файле.

Спикер 4

То есть нам сейчас придется скакать между исходным кодом и тестами.

Так, а почему... А, ну вот, serialize as integer 32.

Спикер 6

Если число меньше нуля, тогда мы берем абсолютное значение, умножаем на 2, вычитаем единицу.

Черт.

Если больше нуля, то мы просто умножаем на 2.

Это такой способ кодирования.

И вызываем затем serialize var int.

Тогда у нас, скажем, минус единица превратится просто в единичный байт.

А здесь у нас serialize bool, оно в булевские значения записывается как 1 и 0, соответственно.

А serialize так мы чуть попозже сейчас обсудим.

Так, смотрим тесты.

Все ли у нас хорошо?

Значит, код тестов, он довольно простой в этих случаях.

Видите, он... Так, нет, не видите.

Спикер 7

Вот он.

Спикер 6

Значит, мы, если сохраняем минус единицу, у нас должно получиться байт 0.1.

А если мы сохраняем единицу, то должен получиться байт 0.2.

Значит, здесь мы вызываем assert.equal, это стандартный метод для тестов.

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

неоднозначность.

Он может сравнивать и значения, и коллекции значений, и как бы он не может выбрать, он говорит, скажите точно, что вы хотите.

Здесь у нас expected, первое значение expected, это способ описать массив в шарпе, скобка, черта, черта, скобка.

А здесь у нас используется memoryStream, такой поток, который в памяти хранится.

И у memoryStream есть метод, который называется toArray, и он все, что мы записали, он преобразует в массив байт.

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

Ну, в эти байты.

Запускаем тесты.

Вот они у нас проходят.

Спикер 4

Так, а я...

Я же вот только вот эти три хотел, да?

Спикер 7

Ну ладно.

В общем... Последний это tag.

Спикер 6

Значит, tag это то, что в начале каждого филда должно идти, то есть поля.

Там сохраняется номер числа и тип, вот этот wire type.

И вар-тайп у нас может быть от 0 до 5.

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

В общем, он в 3 бита помещается.

Поэтому номер поля сдвигает на 3 бита или умножает на 8.

Прибавляет вар-тайп.

У нас вот такая табличка получается.

Вар-инт это 0, например.

Если поле номер 1, а номера полей всегда с 1 нумеруются.

Не обязательно подряд, кстати.

Не обязательно у вас должно быть первое поле.

Вы можете в любом порядке их описывать.

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

То есть разные.

И это будет закодировано как число 8.

Байт 8.

А если это будет третье поле, и тип будет и 32, а у него значение 5, то это будет 29.

То есть это 3 умножить на 8 плюс 5.

И подобный код мы тоже пишем.

Да.

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

Ну, понятно, в общем, мы поняли смысл, как это все делается.

Сериалайз так, он очень простой.

То есть вот ту функцию, которую мы написали, вот мы ее и делаем.

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

Вот у нас таких функций сериализации, у них их достаточно много.

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

Значит, это очень простые примитивы для сохранения разных типов.

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

В одном случае так, в одном вот так.

Флоуты отправляются как есть, но они обязательно должны быть в порядке, байты в

от младших к старшим в порядке, который называется little-endian.

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

Если у вас, кстати, будут на собеседовании спрашивать, как реверсировать алгоритм, вы говорите, есть такой метод array-reverse.

Ничего особенно делать не надо.

Все функции стилизации у нас простые, кроме функции, которые начинают стилизовать поля.

Вот она.

SerializeColorValue.

И она прям реально большая, прям огромная.

Почему?

Потому что там как раз много разных типов.

У нас, по-моему, 16 или даже больше типов описано, которые поддерживает протобув.

И мы должны все их реализовать.

Фактически вот здесь мы все используем.

В разных вариантах нам приходится.

И мы в каждом случае вызываем свою функцию сериализации.

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

Стерилизуем double, стерилизуем float, стерилизуем целые числа, стерилизуем длинные целые числа, стерилизуем те самые signed числа вот этим другим алгоритмом, который чередует позитивные и негативные.

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

И соседняя функция – это…

рекурсивная сериализация месседжа целиком.

Она сама себя вызывает.

Почему?

Потому что внутри одного месседжа может быть другой.

Ей нужна схема для работы.

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

То есть мы даем имя месседжа, с которым мы сейчас будем работать.

Список полей – это те поля, которые у нас в файле текст-прота, то есть они подряд идут.

У нас есть схема и у нас есть стрим.

Вот так вот я показываю, что там справа.

И в действительности мы просто скачем по этим полям подряд.

Если мы видим, что это скалярное поле, мы берем его и вызываем функцию SerializeScalarValue.

А если это лист, то я это еще не написал на самом деле.

Вот у меня написано ImplementedException, но я это обязательно допишу.

Следите за изменениями в файле.

Значит, то есть там очень много всяких вариантов, и не все я еще даже реализовал.

Но в основном уже там какие-то вещи можно реализовать, децерализовать.

А если у нас встречается месседж, месседж внутри месседжа, если вы там поете, у нас был distance request, а внутри point.

Point – это тоже месседж.

Значит, мы рекурсивно сами себя вызываем, переключаемся на новый месседж.

И у нас эти две функции, они вместе дают…

Возможность реализовать данные.

И давайте мы посмотрим, что у нас получается.

Так, нет, я не туда переключаюсь.

Спикер 4

Я хотел показать тесты.

Спикер 6

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

Я, честно говоря, когда готовился, я не думал, что это будет вот так.

То есть все время туда-сюда, туда-сюда.

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

Так, вот.

Serialize scalar value.

Мы ему передаем...

что это у нас целое число, значение у него 150, это первое число, и это целое, да.

У нас должно сохраниться 0.8, 0.96, 0.1.

То есть все как в том файле в документации.

Вот здесь.

0.8, 0.96, 0.1.

То есть мы полностью вот этот сценарий проверяем.

Мы описываем одно поле.

У него название произвольное фу.

Это целое поле.

У него порядковый номер единица.

И мы сохраняем 150.

И мы убеждаемся, в конце концов, что у нас правда сохраняется 0.8, 96, 0.1.

То, что мы и хотели.

Это, кстати, интересный момент.

Смотрите, вы когда читаете документацию какую-нибудь по...

Ну вот поэтому, как кодировать в протобуфе?

Вот у вас есть готовые примеры, они вам дают.

Вот это перейдет в это, это в это.

Вы их в качестве сэмплов и можете использовать.

Они дают вам уверенность, что вы правда все делали в соответствии со спецификацией.

Потом, значит, вот я описал большую схему.

Она правда большая, огромная.

Это все схема, схема, схема.

Теперь я описал запрос.

Это откуда и куда.

И я описал те самые 40 байт, помните, я вам показывал, что сервизация, вот они, эти 40 байт, вот они здесь.

Это нам нужно будет для следующих тестов.

И вот serialized message должен, это тоже интеграционный тест, мы берем и записываем вот этот вот наш distance request и передаем ему...

Наши поля.

Это вот это имеется в виду.

Поля, которые должны быть описаны в файле текст прота.

И в результате у нас должен появиться массив из вот этих 40 байтов.

Если мы все правильно сделали, то у нас вот это все так и должно быть.

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

И это значит, что мы на самом деле сервизацию написали.

Это очень круто.

Мы молодцы.

И нам осталось разобраться с десериализацией.

Она фактически парная, точно такая же.

Не забываем про тесты.

Я вам еще раз говорю, что здесь я вам сейчас просто показываю все как есть.

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

С десериализацией то же самое.

К каждому методу мы вставим соответственный метод сериализации.

Deserialize varint, deserialize tag, deserialize bool.

Вот это вот все.

Я их написал в начале, и все эти функции, как сам начальник говорил, они возвращают какое-то значение 0.

Просто для того, чтобы IntelliSense у нас работал в идее, чтобы у нас не ругалось.

Я не знаю, вы пишете тест на deserialized variant, а что это такое, я не знаю.

Мы описываем в начале функцию, потом мы описываем...

тесты, которые должны ее проверить.

И у нас они падают.

Потом мы делаем реализацию, которая будет нормально работать.

Именно в таком порядке я это делал.

И у нас, давайте, последний момент сейчас.

Это десерилизация, это тесты на десерилизацию, они простые тоже.

Как вы там видели, мы берем 150, сохраняем, должно получиться 9601.

Обратный пример точно такой же.

Вы на вход получаете 9601, диссерилизуете, должно получиться 150.

И они все простые.

И в самом низу у нас есть такой же точно интеграционный тест.

Ага, вот строки.

Со строками интересный момент.

Если вы сохраняете один смайлик, то это на самом деле 8 байт.

И это отдельная история.

Я с этим намучился.

Мне пришлось написать тест отдельный, чтобы это дело работало.

Так, диссерилизация, сервизация.

Вэрью – это отдельная строка, отдельный филд.

Он просто диссерилизуется.

И месседж.

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

Ну и последний момент, на самом деле, у нас…

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

То есть вот это в текстовом виде, то, что я вам вначале говорил.

Текстовое представление... Сейчас, сейчас, сейчас.

Ну да, почти оно, почти оно.

Вот этот.

Вот этот файл мы должны получить в самом конце.

То есть хорошо, нам пришли бинарные данные, мы десервализовали, у нас получились филды, но эти филды — это наши переменные в нашей программе.

Теперь мы должны их напечатать, сгенерировать фактически текст.

И что мы делаем?

Мы пишем функцию, которая называется printFields, и она, да, бежит по этим филдам, генерирует их текстовые представления, а потом мы пишем интеграционный тест, который это все проверяет.

Значит, вот мы передаем ей эти филды, поля,

И у нас должен появиться вот такой текст в результате.

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

Во всех языках, наверное, кроме растов.

Только в расте более-менее нормально сделали, и то за счет макроса.

Вы ставите специальную программу, специальный макрос, который позволяет вам нормально написать

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

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

Мы вызываем equal.

Да, все.

Я уложился 45 минут.

Прекрасно.

С диссерилизацией у нас, ну по сути мы все то же самое повторили и добавили еще один метод, который просто в строковое представление это все превращает.

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

Это мне показалось.

Вот он.

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

Так, ну не, ну вообще-то на самом деле короткий.

Вот он со всеми этими идентациями, со всеми типами, вот он в такое вот представление, в текстовое представление вам и сделает.

И на этом, наверное, все.

То есть мы с вами все сделали, что хотели.

Мы научились реализовать бинарные данные, десерилизовать из бинарных и даже кодегенерировать текст.

Я теперь делаю... Вопросы по второй части.

Это то, о чем мы говорили.

Можно вопросы задавать, а сюда подглядывать.

Спикер 3

Да, давайте.

Марк, спасибо.

Ребята, у кого есть вопросы, поднимайте руки.

Спикер 7

Так вот, отлично, есть рука.

Спикер 2

Здравствуйте.

Очень интересно.

Ну, лекция заставила меня выйти из зоны комфорта, потому что я все-таки ООП.

И первый вопрос, наверное, философский о том, как научиться мыслить функционально, то есть как именно перейти в этот клуб.

Спикер 6

Да, это, кстати, хороший вопрос, потому что, правда, порог входа такой высокий, но на самом деле он берется, потому что

Ну, во-первых, чтение литературы, да, хорошо помогает.

То есть какой-нибудь Скотт Влашин, который сидит на сайте F-Sharp for Fun and Profit и пишет статьи, просто если даже почитать, очень много интересных там видно у него приемов.

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

Я вот изучил, например, что-то в Хаске или в FF Sharp, и думаю, надо попробовать.

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

Решил, и после того, как решил, мне показывают другие решения на этом языке.

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

Один из самых крутых способов, там правда такие...

Это очень просто, я думаю, вау, как круто.

Спикер 2

И еще вопрос, наверное, с точки зрения именно операторов, потому что очень много операторов, там скобка, скобка, скобка, там какие-то открывающиеся, закрывающиеся.

Я так понимаю, их можно самому создавать?

Спикер 6

Да, их можно самому, это особенность даже не F-Sharp, это особенность вообще всей этой ветки языков программирования, которая происходит от ML.

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

По названию функции вы можете что-то понять.

Но здесь для F-sharp, F-particle это считается приемлемым, потому что, ну, вы один раз это выучили, как библиотеку, и потом у вас это постоянно встречается.

Спикер 5

Спасибо большое.

Спасибо.

Это еще вопросы?

Из чата ничего не задают?

Спикер 3

Тогда, Марк, спасибо.

Вот появился вопрос.

Спикер 1

Привет, спасибо за доклад.

Хотел узнать, какие дальнейшие планы по реализации этой программы?

Будешь идти в тайп-провайдеры, чтобы можно было из криптов это запускать, либо это будет просто консольная утилита?

Спикер 6

Пока речь идет о консольном утилите, наверное, и Type Provider можно сделать.

В общем, проблема в том, что у меня возникла задача, которую я хотел решить.

Я ее решил.

А сейчас планов громаден.

По меньшей мере, это висит в GitHub.

Любой человек может зайти и сказать, а сделайте вот это.

И я такой, ну да, наверное, надо сделать.

Единственная проблема.

В прошлый раз, когда я сделал такой проект, это фильтр Калмана, он тоже на F-Sharp написан, он тоже лежит в GitHub.

И там единственное issue, которое мне попросили, оно звучало так.

Можете переписать на Java?

Ну не, не единственное, но одно из самых таких смешных.

Я его запомнил.

Боюсь, что следующий попросит.

Перепиши на C Sharp.

Спикер 1

Спасибо.

Спасибо.

Спикер 3

Олегий, спасибо большое.

На этом мы заканчиваем.

Вы можете поймать нашего спикера в дискуссионной зоне.

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