Как стать автором
Обновить
0
Uma.Tech
Uma.Tech создает IT-инновации для медиабизнеса.

Аккуратно и системно облегчаем понимание кода

Время на прочтение5 мин
Количество просмотров2.6K

Читаемость кода упрощает как процесс написания программ, так и последующие действия – от отладки и оптимизации до тестирования и сопровождения.


image


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


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


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


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


protocol Monoid {
  init() // нейтральный, пустой элемент
  func +(Self, Self) -> Self // правило комбинирования двух элементов такого же типа
  func concat([Self]) -> Self // комбинирование нейтрального с другим элементом
}

Функтор – метод, позволяющий отобразить один тип моноида в другой. Это знакомый всем, простой и понятный метод map.


Таким именем и назовем протокол, реализованный на Swift.


protocol Mappable {
  associatedtype Element
  func map<InType, OutType>(transform: InType -> OutType, input: Self<InType>) -> Self<OutType> // Отобразить текущий тип элементов InType в новый тип OutType
}

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


protocol Bindable: Mappable {
  init(element: Element)
  func bind<Out>(input: Self<In>, transform: Int -> Self<Out>) -> Self<Out> // Связать In в Out с помощью особого преобразования transform, которое знает как работать с In.
}


Для понимания описанных сущностей рассмотрим несколько практических примеров.


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


В данном случае можно воспользоваться монадой Maybe, позволяющей идентифицировать наличие или отсутствие значения в переменной.


enum Maybe<T> {
    case some(T) // значение существует
    case none // значение отсутствует
}

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


К примеру, определив оператор связывания (bind) как >>-, принимающий монаду Maybe, и операцию, которую мы хотим выполнить только в том случае, если объект существует:


var link: String? = “http://}{abr.com”
link // к монаде Maybe, она же Optional в Swift
>>- { URL(string: $0) } // если строка существует, то применим преобразование в URL
>>- { try? Data(contentsOf: $0) } // если объект URL на предыдущем шаге получен, то попытаемся загрузить данные по ссылке
>>- { UIImage(data: $0) } // если данные загрузили, то создадим картинку из этих данных

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


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


Рассмотрим еще один пример работы с ошибками. В данном случае при возникновении ошибки создаем объект Result.failure и передаем его в колбэк completion, а в случае успешного получения данных – создаем Result.success и передаем его туда же, в колбэк completion:


...
error >>- Result.failure >>- completion
data >>- Result.success >>- completion

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


Аппликативный функтор (<*>) удобен при работе с делегатами или с функциями обратного вызова. В последнем случае мы видим слабую ссылку на функции, в которые надо передать результат выполнения асинхронной операции. Например, комбинируя его с предыдущим примером, можно получить элегантное решение такой задачи: вызвать callback только в том случае, если ссылка на него актуальна и картинка загружена по ссылке, составленной из переданной ранее строки.


var link: String? = “http://}{abr.com”
…

callback <*> link
>>- { URL(string: $0) }
>>- { try? Data(contentsOf: $0) }
>>- { UIImage(data: $0) } 

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


xhrRequest(url, null, handleRequest).promise
.then(handleResultData)
.then(loadManifest)
.then(handleFragments)
.finally(triggerLoaded);

Представив выполнение сетевого взаимодействия через Promise (промисы) мы сохранили читаемость и – что важнее! – оперировали понятиями не как делать, а что делать.


  1. Загрузить данные и обработать результат запроса;
  2. Обработать полученные данные;
  3. Загрузить манифест, опираясь на ранее полученные данные;
  4. Обработать фрагменты;
  5. Сообщить о загруженных данных.

image


Важным результатом от внедрения «функциональщины» является упрощенная тестируемость. В цепочке выполнения можно внедрять функции, поставляющие данные для тестирования поведения (https://en.wikipedia.org/wiki/Mock_object) всей цепочки.


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


Соблюдать границы насыщения монадами! Для этого есть простые правила:


  1. Вводя новые монады, сопровождайте их документацией не только с примерами, но и с описанием принципов работы.
  2. Обязательно проводите CodeReview со своей командой, во время которого решайте, так ли необходимо создание еще одной монады.
  3. Найдите смежные кейсы для каждой новой монады, которые позволят лучше понять ее.
  4. Не увлекайтесь с монадами «искусством ради искусства», то есть их созданием ради создания.
  5. Помните: три символа вполне достаточно для обозначения новой монады.
    Надеемся, что наш краткий очерк будет полезен для вас, если возникнут вопросы по теории и практике работы с монадами – постараемся ответить в комментариях.

Юрий Шикин — Uma.Tech

Теги:
Хабы:
Всего голосов 7: ↑3 и ↓4+1
Комментарии6

Публикации

Информация

Сайт
uma.tech
Дата регистрации
Численность
Неизвестно
Местоположение
Россия

Истории