Pull to refresh
26
0
Павел Осипов @PavelOsipov

Разработчик iOS/C++

Send message

Не подскажите, по какому пути вы пошли в попытке автоматического исключения?

Видно, что сделано с душой)
Был бы очень рад узнать, учитываются ли прошлые и актуальные аномалии в процессе обучения модельки? Немного раскрою вопрос. Допустим, мы решили сделать baseline на очередной период, но у данной метрики наблюдались аномалии в прошлом, а то и вовсе она «болеет» прямо сейчас. Как решается вопрос исключения проблемных периодов из обучающей выборки? Помнит ли сама система о сбоях в прошлом или их выкусывание – это ручной процесс и таким образом построение прогноза всегда требует оператора?

Я нашёл 2:

Остановил свой выбор на первом и после некоторых модификаций и портирования на OpenCombine успешно воспользовался.

Action – прекрасная вещь. Пользовался этим инструментом ещё в ReactiveCocoa. Не знаете, есть ли реализация этой абстракции в Combine?

Опыт интересный. Подскажите, пожалуйста, насколько остаётся работоспособным разного рода инструментарий iOS разработчика, в частности:

  1. Работает ли Xcode Memory Debugger? Увижу ли я котлиновские объекты, если поставлю процесс на паузу? Увижу ли я колтиновкие объекта в визуальном графе состояния? Увижу ли я котлиновский стек, если активую опцию Malloc Stack Logging?

  2. Адекватно ли работают ли Xcode Instruments? В частности Allocations, Time Profiler? Нормально ли там отображаются котлиновские объекты и стектрейсы?

  3. Насколько хорошо работает плагин TouchLab для Xcode? Удобно ли в нём браузиться по структуре объектов, все ли они отображаются?

Разницы нет в случае RVO/NRVO, но как вы же сами выше и продемонстрировали, есть ситуации, когда эти оптимизации не применяются. В случае же с rvalue ссылкой мы гарантируем отсутствие копирований временного объекта.

Нельзя не заметить, что определённую оптимизацию, позволяющую избежать копирований временного объекта Widget, сделать всё-таки можно. Для этого мы вместо копирования продляем жизнь временному объекту (подробности), которая вернула функция doSomeVeryComplicatedThingWithSeveralArguments, а затем перемещаем его.


void someFunctionV3() {
    Widget&& complicatedThingResult =
        doSomeVeryComplicatedThingWithSeveralArguments(123, "hello");
    consume(std::move(complicatedThingResult));
}

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


Обвяз вокруг базовой структуры данных любопытен (интеракторы, сохранение, нотификации), однако корневая проблема, заявленная в начале статьи, выглядит нерешённой. Её суть, как Вы правильно отметили, в неудовлетворительном контракте, предоставляемой объектом «Заявка». Цитирую: «Конечно, все эти данные (разные куски заявки, собираемые на разных шага визарда – прим.) стоит упаковать в один объект заявки. Работая с таким объектом, мы обрекаем наш код покрыться лишним ненужным количеством проверок null. Например, такая структура данных никак не гарантирует, что поле educationType уже будет заполнено на экране «Образование»:


class Application(
   val name: String?,
   val surname: String?,
   val educationType : EducationType?,
   val workingExperience: Boolean?
   val education: Education?,
   val experience: Experience?,
   val motivation: List<Motivation>?
)

С интересом хотелось узнать, что же будет предложено в замен, но… в по факту итоговая структура получилась с ещё более слабым контрактом, чем исходная:


/**
 * Черновик заявки
 */
class ApplicationDraft(
   val outDataMap: MutableMap<ApplicationSteps, ApplicationStepOutData> = mutableMapOf()
) : Serializable {
   fun getPersonalInfoOutData() = outDataMap[PERSONAL_INFO] as? PersonalInfoStepOutData
   fun getEducationStepOutData() = outDataMap[EDUCATION] as? EducationStepOutData
   fun getExperienceStepOutData() = outDataMap[EXPERIENCE] as? ExperienceStepOutData
   fun getAboutMeStepOutData() = outDataMap[ABOUT_ME] as? AboutMeStepOutData
   fun getMotivationStepOutData() = outDataMap[MOTIVATION] as? MotivationStepOutData

   fun clear() {
       outDataMap.clear()
   }
}

Мало того, что мы по-прежнему не можем быть уверены в наличии полей (таков контракт словаря MutableMap), так ввиду потери информации о типе из-за приведения всего к ApplicationStepOutData мы вдобавок без юнит-тестов ещё и не можем быть уверены, что они правильного типа. Это красноречиво демонстрирует следующий код из статьи:


override fun resolveStepInData(step: ApplicationStep): Single<ApplicationStepData> {
   return when (step) {
       PERSONAL_INFO -> ...
       EXPERIENCE -> ...
       EDUCATION -> Single.just(
           EducationStepData(
               inData = EducationStepInData(
                   draft.getPersonalInfoOutData()?.info?.educationType
                   ?: error("Not enough data for EDUCATION step") // <==== THIS
               ),
               outData = draft.getEducationStepOutData()
           )
       )
       // ...
   }
}

Корень зла на мой взгляд в недостаточном моделировании самой сути предметной области. Под катом предлагаю своё решение корневой проблемы (на более близком мне Swift).


Steps model (in Swift)
import Foundation

struct PassedStep<Input, Output> {
    let input: Input
    let output: Output
}

extension PassedStep where Input == Void {
    init(output: Output) {
        self.input = ()
        self.output = output
    }
}

struct ActiveStep<Input, Output> {
    typealias Result = PassedStep<Input, Output>

    let input: Input

    func pass<NextOutput>(with output: Output) -> ActiveStep<Result, NextOutput> {
        ActiveStep<Result, NextOutput>(input: Result(input: input, output: output))
    }
}

extension ActiveStep where Input == Void {
    init() {
        self.input = ()
    }
}

struct A {}
struct B {}
struct C {}

struct Result {
    let a: A
    let b: B
    let c: C
}

typealias StepA = ActiveStep<Void, A>
typealias StepB = ActiveStep<StepA.Result, B>
typealias StepC = ActiveStep<StepB.Result, C>
typealias FinalStep = ActiveStep<StepC.Result, Void>

// Flow implrmrnted in some scenario class
// Asynchronous step completions replaced with synchrounous ones for clarity
let stepA = StepA()
let stepB: StepB = stepA.pass(with: A())
let stepC: StepC = stepB.pass(with: B())
let finalStep: FinalStep = stepC.pass(with: C())
let result = Result(
    a: finalStep.input.input.input.output,
    b: finalStep.input.input.output,
    c: finalStep.input.output
)

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

Вот именно, что одна версия переименовывается, но остаётся тем же самым файлом, а не выкачивается как новый файл.

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

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

Если речь про файлы с одинаковым именем внутри одной папки, то сервер такое сделать не даст. Если про разные, то нет проблем. В составном ключе есть не только имя, но и суррогатный идентификатор родительской директории.
Существенная ваша ошибка — использовать естественные ключи, а не суррогатные. Это приводит к тому, что переименовывание файла выливается в поиск и замену всех вхождений ключа во всём дереве. Это крайне сложно и медленно.


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

Но тут всё дело в нашем API. К сожалению у нод в дереве нет никакого идентификатора, который всегда существует и никогда не изменяется. Единственно на что можно рассчитывать – уникальность имени внутри одной папки. Таким образом процедура вычисления изменений внутри папки, о которой я упоминал в первой главе, строится на сравнении сортированных по имени списков дочерних элементов из локального папки и её серверного аналога. Поскольку эта процедура крайне частая и при полном слиянии двух деревьев (локального и серверного) в ней участвуют все элементы основной таблицы, то именно она должна быть максимально перфомансной, и именно под неё у нас и заточен первичный ключ. Ну а переименование – это суперредкая процедура, в рамках которой трогается перезаписывается крайне небольшое количество записей.
В этом сомнений, конечно, нет никаких. Но я больше размышляю о flatbuffers в контексте value-части записи. Меня в свое время очень порадовала статья об оптимизации Android приложения Facebook на основе flatbuffers. С тех пор думаю, а что если положить приехавшие объекты сразу в LMDB as is а потом читать их напрямую оттуда, благо формат провернуть такой фокус позволяет. Такая экономия времени была бы…
Прям заинтриговали готовящимся C++ API от души!

Пользуясь случаем, хотел спросить ещё следующее. В мобилках отсутствие места на диске является вполне себе заурядной ситуацией, особенно в контексте облачного приложения, которое и устанавливают, чтобы это место освободить. Насколько сложным вам видится поддержка работы хранилища в in memory режиме, без привязки к файлу. По идее поддержать mmap поверх анонимной памяти не должно быть сильно сложной задачей. Много ли внутри LMDB или сильно продвинувшейся вперед MDBX завязок на то, что под mmap обязательно лежит именно файл?
Спасибо, посмотрю. Вообще, если фантазировать вокруг да около, у меня еще витает мысль попробовать flatbuffers для сериализации, благо согласно их бенчмаркам она не так уж сильно проигрывает голым структурам.
Звучит интригующе. Было бы здорово хоть ссылкой, хоть как подсветить как код из FoundationDb помогает в деле генерации ключа.
Спасибо за ссылки, Юра, и вообще за вклад в развитие LMDB!

Пользуясь случаем, хочу поинтересоваться, автоадаптация размеров хранилища не приводит к инвалидации разного рода сущностей, полученных ранее от движка. Вот получил я указатель на данные в MDB_val, какой-то кусок кода продолжает им пользоваться и тут бац и база поресайзилась. Как транзакции переживают этот момент? В этом свете, предполагает ли работа с MDBX обязательное копирование ранее полученных из неё данных?
Спасибо, что поделились своим опытом. По прочтении данной статьи у меня возникло 2 вопроса:

  1. Вы пишете, что создали описанный процесс, чтобы сделать процедуру найма эффективнее. Можете раскрыть тезис, в чём измеряется эффективность? Когда непонятно «зачем», сложно оценить полученное «как». Есть ли циферные метрики, подтверждающие повышение эффективности? Например, что время найма уменьшилось на 20%, или текучка снизилась на такое-то количество?
  2. Какие есть возможности у команд разработки кастомизировать процесс под себя?

Ниже пояснение, почему у меня вообще появились эти вопросы.

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

Насколько я понимаю суть гибких методологий, она состоит в том, что каждая команда является самоорганизующейся единицей. Она самостоятельно настраивает свои процессы, принимая во внимание поставленные перед цели и имеющиеся ресурсы. Любой регламент сверху, касающийся любого аспекта её деятельности, эту гибкость отнимает, поскольку априори не учитывает её особенности. Представим себе, что в некой компании X есть линейка продуктов: какой-нибудь сервис-«дойная корова» (поиск, почта, объявления и т.п.), написанный на Java, внутренний инкубатор стартапов, пишущийся на Node.js, и инфраструктурные сервисы (портальной авторизация, middleware, стораджа), написанные на C. На мой взгляд будет контрпродуктивным гонять кандидатов по плюс-минус одинаковому флоу при найме в каждую из обозначенных команд.

Позволю себе провести ещё одну аналогию, которая роднит близкую нам предметную область проектирования ПО и рассматриваемую тему проектирования процессов. Подход к разработке с использованием микросервисной архитектуры родился в том числе потому, что получается неоправданно дорого реализовывать разные части системы с использованием одинакового стека технологий, несмотря на то, что это безумно красиво и технологически эффективно. Предъявляемые атрибуты качества к разным компонентам отличаются, и если у меня поиск написан стеке с голым C в связке с in-house базой данных и обязан работать 24x7, то это не повод навязывать его команде разработки соц. сети для любителей котиков, которая находится на этапе проверки гипотезы и может себе позволить небольшие даунтаймы. Как руководитель конкретной команды я предпочитаю иметь свой процесс найма и отбирать людей исходя из своих потребностей. Почему моя команда «соц. сети для любителей котиков» должна полгода набирать сотрудников, из-за того, что руководитель поиска добавил в процесс найма фильтр в виде требования написать алгоритм обхода бинарного дерева без использования рекурсии? Но, постойте, в отличие от команды разработки поиска, моей через полгода из-за невыполненных KPI вообще уже может и не существовать… Данный процесс видится рациональным только в том случае, если с точки зрения внутренней политики компании при закрытии одного проекта люди могли безболезненно переходить в другой. В этом случае действительно получается, что любой бекендер должен быть настолько хорош, чтобы в любой момент он мог перейти в самый технологически сложный проект. Но тут опять страдает команда любителей котиков – средненьких ей набрать не дают, а звезды к ней не идут. Возвращаясь к аналогии с проектированим ПО, описанный процесс найма сродни такому подходу к разработке, где технологический стек унифицирован таким образом, чтобы каждый компонент в монорепозитории в любой момент можно было бы взять и начать использовать в сервисе поиска. Он должен быть написан на голом C и обезбажен на 99,999%, пройдя многоступенчатое code review и иметь 100% code test coverage. Эффективно ли это?

Лично мне импонирует процесс найма в Artsy. Он адаптивен и отталкивается от имеющегося опыта кандидата. Они пытаются свести к минимуму проявления элитаризма в нашей профессии и неожиданным образом отвечают на вопрос «What's wrong with typical hiring practices»:

  • In-person coding challenges
  • Whiteboard interviews
  • Sample code
  • Take-home challenges.
А я в минуты дебаг-уныния перечитываю старый пост на RSDN «Путь химика в программирование»: часть 1, часть 2, часть 3.

Это правда. Все мы больше UIKit-программисты, чем Swift или Objective-C.

1

Information

Rating
Does not participate
Location
Москва, Москва и Московская обл., Россия
Date of birth
Registered
Activity