У тех, кто успевает сделать учебное задание ("спроектировать web API по выбранной им доменной области с простыми CRUD-операциями"), сколько остается времени на реальные задачи? Есть ли учебный этап в стажировке?
Собственно, не раскрыта тема статьи: как взаимодействует разработчик с практикантом на этапе реальных задач, какие требования к soft skills такого разработчика?
Почему каждая библиотека конфигурации на Go пытается делать всё сразу, притаскивая код или зависимости для форматов, которые не нужны в конкретном проекте? Явно же есть две задачи: 1) прочитать ключи и значения из env/file/whatever, 2) разложить значения по полям структуры конфига в соответствии с ключами. Достаточно определить что-то вроде type KV interface { Get(string) (string, bool) }, который возвращает библиотека, решающая первую задачу, и принимает библиотека, решающая вторую. Тогда для каждого источника достаточно было бы одной библиотеки, которую можно стандартным образом совместить с библиотекой-раскладывателем и другими библиотеками-источниками.
Важное отличие записи через конвейер от записи в файл в том, что конвейер запускается в initial namespace, и поэтому работает для дампов из контейнера, а путь к файлу интерпретируется в mount namespace процесса, дамп памяти которого пишется. Для контейнер настройка с файлом позволит получить дамп, только если каталог настроенного пути примонтирован как том контейнера.
Прослушать лекции, изучить материалы и выполнить типовое задание, к которому только что подготовили и которое нужно для проверки — это типичный опыт курсовой работы в вузе или тематической "летней школы". Это не похоже не рабочий процесс, а практика должна быть похожа.
Ты приходишь в проект, который решает задачи, понятные лишь в общих чертах, с помощью технологий, про многие из которых ты спасибо, если слышал. Где-то есть место твоей задачи, мотивация её сделать и одновременно настоящий критерий успеха: изменит твое решение в проекте что-то или нет. Чтобы хоть как-то начать, надо базово разобраться с пачкой инструментов. Потом вникать в то, что надо сделать, изучать, не бояться задавать вопросы, предлагать что-то. Внятно отчитываться на стендапе, code review проходить.
Новичку будет страшновато, поэтому всё в контролируемой среде. Естественным путем охватить весь проект и место задачи в нем не хватит времени практики, поэтому будут рассказывать и показывать. Задачи поставлены продуманно, чтобы было посильно, но и чтобы было, с чем столкнуться. К ним написаны указания, как их примерно можно выполнить, полезные ссылки. Руководитель всегда подскажет, если что-то не получается, и направит процесс.
Практикантов у нас единицы. В этом году было трое, собственно, три задания я описывал. По информации с кафедры, где я преподаю, это среднее количество на типичную базу практики. До ваших 150 наш подход не масштабируется, у нас сотрудников-то в разы меньше.
P. S. Вы круто сделали. Просто я считаю, что производственная практика должна быть иной.
В нашей компании получается находить к лету несколько задач, которые сравнительно изолированы, поэтому их можно дать практикантам, не открывая доступа к чуствительным частям проекта. Например, этим летом:
написать Prometheus-экспортер для метрик AMD uProf (практикант пишет весь код сам);
доработать генератор тестового трафика в части протокола sFlow (вспомогательный код не жалко дать);
покрутить определенные настройки ClickHouse, чтобы оптимизировать работу одного компонента (код практиканту особо не нужен).
В прошлые годы получалось давать задачи на DM/ML, выгружая образец данных. Разумеется, все эти задачи ставятся не в вакууме — практикант должен послушать, понять и уточнить, если надо, зачем и как это будет использоваться, развернуть среду разработки и для тестов.
Куда технически запускать практикантов, зависит от компании. Мы делаем изолированную "площадку молодняка" с копией GitLab, CI, трекера задач, необходимым для практики наполнением и железом. Студенты работают удаленно.
Вместо практики у вас получились летняя школа. Еще один учебный курс с лекциями про полный цикл разработки и игрушечным заданием. Производственная практика должна давать то, чего вуз не может: реальные задачи с большим контекстом, необходимость включиться в процессы и освоить актуальные инструменты, взаимодействие с коллегами. Студент после практики в идеале должен понять, зачем знать то, чему учили в вузе, и чего не хватает, чтобы за последний год подтянуть.
Если квартальные премии формируются по итогам продаж, которые могут быть только при своевременно и качественно сделанных фичах (конкурентный рынок), то на длительном промежутке каждому выгодно поддерживать "здоровье кода", иначе хорошо не будет никому. Это же отбивает охоту к непродуктивным замечаниям.
Скорее, руководство (manual/guide?) состоит из множества howto, сгруппированных по темам. Иногда в нем есть и reference, и explanation. То есть руководство и не должно входить в классификацию, это формат издания.
Техническая документация не только для кода. Например, есть система защиты от атак. Есть howto, как настроить автоматическую защиту от атак определенного вида, в ходе которого настраиваются конкретные алерты. Есть reference со всеми доступными алертами и их видами (в подсказках к UI не развернешься). Есть explanation логики, по которой организованы алерты.
А вот для долгосрочных объектов работает банальная parent-child древовидная модель (ну... ок, можно сказать RAII + классический malloc()/free()). Т.е. просто деаллоцируются куски дерева объектов/ресурсов рекурсией. Ну или какой вариант key-value in-memory базы данных со своим отдельным менеджером памяти (LMDB/MDBX).
В эту схему не укладывается случай, когда долгоживущие объекты 1) используются и даже создаются на fast path — in-memory database слишком медленные; 2) должны освобождать свои ресурсы, как только стали не нужны — они дефицитные; 3) могут уничтожаться вместе с родителем в иерархии, но держит их живыми не родитель, а использующие. Ядро не буду приводить в пример, так как там всё особенное. Но вот userspace драйверы в DPDK именно такие, и там масса багов от того, что написаны они на C с самопальными счетчиками ссылок. Высокопроизводительный и экономный системный софт, в общем.
объект класса-функтора с конструктором от необъектной лямбды
В этом объекте понадобится сохранить то, что захвачено. В C++ по функции нельзя получить нужную информацию, получив указатель на нее в конструкторе, поэтому "необъектная лямбда" не может быть функцией. Ниже вы пишете о "простом теле функции или лексическогом замыкании", но таких сущностей в C++ нет, и тем более их нет как объектов (хранимых типизированных сущностей), чтобы можно было их передать в конструктор и скопировать.
А вот с функциями высших порядков дело обстояло бы проще.
Для этого нужно функции сделать объектами (в терминах стандарта C++). Но зачем, если C++ позволяет иметь объекты с поведением, как у функций, и даже с более общим, чем у функций.
Вы утверждали, что всё захваченное лямбдой хранится в каком-то месте, не связанном с лямбдой, и поэтому лямбды могли бы быть не объектами (у которых есть место хранения), а функциями (у которых нет места хранения). Мой пример опровергает это: 1) захваченные лямбдой по значению переменные не существуют в момент вызова лямбды, 2) захваченные копии хранятся внутри std::function, но для создания такого std::function нужен объект с этими данными внутри. Следовательно, лямбды не могут быть функциями в общем случае, потому что приведен случай, где они должны быть объектами.
https://wandbox.org/permlink/Z2TVJjyUW6Hnr00G Никаких бесхозных значений. Лямбда в make_request() захватывает this по ссылке и request_id по значению. Затем она сохраняется в std::function. Но поскольку std::function конструируется на основе лямбды, это значит, что захваченное содержалось внутри лямбды, то есть, она не была функцией. Никакого "лексического" захвата быть не может, потому что std::function реализуется чисто средствами C++, и реализация её конструктора не может проинспектировать, что из make_request() использует лямбда — конструктор видит только объект-функтор и знает его размер.
Где будет храниться захваченный контекст, если лямбда вызывается вне того стека вызовов, где была создана? Практический пример уже приводил: запускается асинхронная операция, куда передается лямбда, которая должна быть вызвана про завершении операции. И функция, запускающая операцию, и функция, которая создала лябмду и передала ее, завершились, их стека больше нет.
Речь идет про объекты в терминах стандарта C++, а не принципов ООП. Объект принципиально отличается от функции тем, что имеет тип и хранится где-то (*). Это нужно лямбдам, чтобы хранить захваченный контекст. Захватывать контекст требует типичное применение лямбды, когда она вызываются не из той функции, которая лямбду создает (например, создается при старте IO, а вызывается из event loop).
Лямбда сделана именно объектом класса, а не объектом какого-то нового для C++ вида, чтобы не умножать сущности. Про инкапсуляцию у такого объекта, кстати, можно и говорить: интерфейс фиксирован, все данные-члены скрыты.
(*) Да, технически код функции хранится в памяти, но с точки зрения стандарта C++ можно только получить указатель, через который можно вызвать функцию, но нельзя оперировать её кодом как находящимся где-то блоком байт.
Лямбды позволяют сделать больше, чем вложенные функции, благодаря захвату контекста, как объясняет KanuTaH в соседней ветке. Если добавлять захват вложенным функциям, как вы там же предлагаете, получатся те же лямбды, только всегда именованные. Но в реальном C++ функция и объект с operator() — сущности разных категорий. Зачем запутывать язык особым типом функций, вложенными, которые на самом деле являются такими объектами? Сделали просто синтаксический сахар для объектов, не вводя новых сущностей.
Сколько по времени длится практика и стажировка?
У тех, кто успевает сделать учебное задание ("спроектировать web API по выбранной им доменной области с простыми CRUD-операциями"), сколько остается времени на реальные задачи? Есть ли учебный этап в стажировке?
Собственно, не раскрыта тема статьи: как взаимодействует разработчик с практикантом на этапе реальных задач, какие требования к soft skills такого разработчика?
Почему каждая библиотека конфигурации на Go пытается делать всё сразу, притаскивая код или зависимости для форматов, которые не нужны в конкретном проекте? Явно же есть две задачи: 1) прочитать ключи и значения из env/file/whatever, 2) разложить значения по полям структуры конфига в соответствии с ключами. Достаточно определить что-то вроде
type KV interface { Get(string) (string, bool) }
, который возвращает библиотека, решающая первую задачу, и принимает библиотека, решающая вторую. Тогда для каждого источника достаточно было бы одной библиотеки, которую можно стандартным образом совместить с библиотекой-раскладывателем и другими библиотеками-источниками.Важное отличие записи через конвейер от записи в файл в том, что конвейер запускается в initial namespace, и поэтому работает для дампов из контейнера, а путь к файлу интерпретируется в mount namespace процесса, дамп памяти которого пишется. Для контейнер настройка с файлом позволит получить дамп, только если каталог настроенного пути примонтирован как том контейнера.
Прослушать лекции, изучить материалы и выполнить типовое задание, к которому только что подготовили и которое нужно для проверки — это типичный опыт курсовой работы в вузе или тематической "летней школы". Это не похоже не рабочий процесс, а практика должна быть похожа.
Ты приходишь в проект, который решает задачи, понятные лишь в общих чертах, с помощью технологий, про многие из которых ты спасибо, если слышал. Где-то есть место твоей задачи, мотивация её сделать и одновременно настоящий критерий успеха: изменит твое решение в проекте что-то или нет. Чтобы хоть как-то начать, надо базово разобраться с пачкой инструментов. Потом вникать в то, что надо сделать, изучать, не бояться задавать вопросы, предлагать что-то. Внятно отчитываться на стендапе, code review проходить.
Новичку будет страшновато, поэтому всё в контролируемой среде. Естественным путем охватить весь проект и место задачи в нем не хватит времени практики, поэтому будут рассказывать и показывать. Задачи поставлены продуманно, чтобы было посильно, но и чтобы было, с чем столкнуться. К ним написаны указания, как их примерно можно выполнить, полезные ссылки. Руководитель всегда подскажет, если что-то не получается, и направит процесс.
Практикантов у нас единицы. В этом году было трое, собственно, три задания я описывал. По информации с кафедры, где я преподаю, это среднее количество на типичную базу практики. До ваших 150 наш подход не масштабируется, у нас сотрудников-то в разы меньше.
P. S. Вы круто сделали. Просто я считаю, что производственная практика должна быть иной.
О каком компоненте речь?
В нашей компании получается находить к лету несколько задач, которые сравнительно изолированы, поэтому их можно дать практикантам, не открывая доступа к чуствительным частям проекта. Например, этим летом:
написать Prometheus-экспортер для метрик AMD uProf (практикант пишет весь код сам);
доработать генератор тестового трафика в части протокола sFlow (вспомогательный код не жалко дать);
покрутить определенные настройки ClickHouse, чтобы оптимизировать работу одного компонента (код практиканту особо не нужен).
В прошлые годы получалось давать задачи на DM/ML, выгружая образец данных. Разумеется, все эти задачи ставятся не в вакууме — практикант должен послушать, понять и уточнить, если надо, зачем и как это будет использоваться, развернуть среду разработки и для тестов.
Куда технически запускать практикантов, зависит от компании. Мы делаем изолированную "площадку молодняка" с копией GitLab, CI, трекера задач, необходимым для практики наполнением и железом. Студенты работают удаленно.
Вместо практики у вас получились летняя школа. Еще один учебный курс с лекциями про полный цикл разработки и игрушечным заданием. Производственная практика должна давать то, чего вуз не может: реальные задачи с большим контекстом, необходимость включиться в процессы и освоить актуальные инструменты, взаимодействие с коллегами. Студент после практики в идеале должен понять, зачем знать то, чему учили в вузе, и чего не хватает, чтобы за последний год подтянуть.
Если квартальные премии формируются по итогам продаж, которые могут быть только при своевременно и качественно сделанных фичах (конкурентный рынок), то на длительном промежутке каждому выгодно поддерживать "здоровье кода", иначе хорошо не будет никому. Это же отбивает охоту к непродуктивным замечаниям.
Вместо "программной смеси":
Из недостатков — не показывает, для каких компонент порядок не важен.
Кстати, если подать на вход приведенный пример, tsort находит в нем цикл:
Скорее, руководство (manual/guide?) состоит из множества howto, сгруппированных по темам. Иногда в нем есть и reference, и explanation. То есть руководство и не должно входить в классификацию, это формат издания.
Техническая документация не только для кода. Например, есть система защиты от атак. Есть howto, как настроить автоматическую защиту от атак определенного вида, в ходе которого настраиваются конкретные алерты. Есть reference со всеми доступными алертами и их видами (в подсказках к UI не развернешься). Есть explanation логики, по которой организованы алерты.
Автор развил изложенные на сайте Divio идеи: https://diataxis.fr/.
В эту схему не укладывается случай, когда долгоживущие объекты 1) используются и даже создаются на fast path — in-memory database слишком медленные; 2) должны освобождать свои ресурсы, как только стали не нужны — они дефицитные; 3) могут уничтожаться вместе с родителем в иерархии, но держит их живыми не родитель, а использующие. Ядро не буду приводить в пример, так как там всё особенное. Но вот userspace драйверы в DPDK именно такие, и там масса багов от того, что написаны они на C с самопальными счетчиками ссылок. Высокопроизводительный и экономный системный софт, в общем.
Как это объясняет поломанность modern C++ или самого RAII?
Можно пояснить или указать, где почитать об этом?
В этом объекте понадобится сохранить то, что захвачено. В C++ по функции нельзя получить нужную информацию, получив указатель на нее в конструкторе, поэтому "необъектная лямбда" не может быть функцией. Ниже вы пишете о "простом теле функции или лексическогом замыкании", но таких сущностей в C++ нет, и тем более их нет как объектов (хранимых типизированных сущностей), чтобы можно было их передать в конструктор и скопировать.
Для этого нужно функции сделать объектами (в терминах стандарта C++). Но зачем, если C++ позволяет иметь объекты с поведением, как у функций, и даже с более общим, чем у функций.
Вы утверждали, что всё захваченное лямбдой хранится в каком-то месте, не связанном с лямбдой, и поэтому лямбды могли бы быть не объектами (у которых есть место хранения), а функциями (у которых нет места хранения). Мой пример опровергает это: 1) захваченные лямбдой по значению переменные не существуют в момент вызова лямбды, 2) захваченные копии хранятся внутри
std::function
, но для создания такогоstd::function
нужен объект с этими данными внутри. Следовательно, лямбды не могут быть функциями в общем случае, потому что приведен случай, где они должны быть объектами.https://wandbox.org/permlink/Z2TVJjyUW6Hnr00G
Никаких бесхозных значений. Лямбда в
make_request()
захватываетthis
по ссылке иrequest_id
по значению. Затем она сохраняется вstd::function
. Но посколькуstd::function
конструируется на основе лямбды, это значит, что захваченное содержалось внутри лямбды, то есть, она не была функцией. Никакого "лексического" захвата быть не может, потому чтоstd::function
реализуется чисто средствами C++, и реализация её конструктора не может проинспектировать, что изmake_request()
использует лямбда — конструктор видит только объект-функтор и знает его размер.Где будет храниться захваченный контекст, если лямбда вызывается вне того стека вызовов, где была создана? Практический пример уже приводил: запускается асинхронная операция, куда передается лямбда, которая должна быть вызвана про завершении операции. И функция, запускающая операцию, и функция, которая создала лябмду и передала ее, завершились, их стека больше нет.
Речь идет про объекты в терминах стандарта C++, а не принципов ООП. Объект принципиально отличается от функции тем, что имеет тип и хранится где-то (*). Это нужно лямбдам, чтобы хранить захваченный контекст. Захватывать контекст требует типичное применение лямбды, когда она вызываются не из той функции, которая лямбду создает (например, создается при старте IO, а вызывается из event loop).
Лямбда сделана именно объектом класса, а не объектом какого-то нового для C++ вида, чтобы не умножать сущности. Про инкапсуляцию у такого объекта, кстати, можно и говорить: интерфейс фиксирован, все данные-члены скрыты.
(*) Да, технически код функции хранится в памяти, но с точки зрения стандарта C++ можно только получить указатель, через который можно вызвать функцию, но нельзя оперировать её кодом как находящимся где-то блоком байт.
Лямбды позволяют сделать больше, чем вложенные функции, благодаря захвату контекста, как объясняет KanuTaH в соседней ветке. Если добавлять захват вложенным функциям, как вы там же предлагаете, получатся те же лямбды, только всегда именованные. Но в реальном C++ функция и объект с
operator()
— сущности разных категорий. Зачем запутывать язык особым типом функций, вложенными, которые на самом деле являются такими объектами? Сделали просто синтаксический сахар для объектов, не вводя новых сущностей.