воскресенье, 16 августа 2015 г.

Должен ли программист знать математику?

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

Почему вдруг именно сейчас сформировалось моё мнение? Дело в том, что мне недавно исполнилось 27 лет, а в среде программистов 27 лет (плюс-минус два года) – это такой возраст, когда многие достигают некоторого "потолка" в своей карьере. То есть среднестатистический программист к этому возрасту уже прошёл ступени от начинающего разработчика до опытного разработчика (тимлид/сеньор/архитектор). И тут у них возникает вопрос – что делать дальше? Существует два пути: либо продолжить карьерный рост, став менеджером/начальником/директором, или остаться программистом и пытаться расти дальше профессионально.

И вот тут-то начинает решать знание математики. Программисты, которые не знают математику, расти дальше профессионально не могут. Ну просто они достигли той грани, до которой ещё можно было расти исключительно засчёт экстенсивного расширения своих навыков, увеличивая количество баззвордов в своём резюме, но подняться выше которой нельзя без определённых фундаментальных математических знаний. Например, после 5 лет написания веб-сайтов ты вряд ли сможешь написать свою распределённую отказоустойчивую базу данных, если у тебя нету знаний хотя бы основ матанализа, сколько бы баззвордов ты не знал. Таким образом, тебе рано или поздно надоедает клепать однообразные CRUD'ы, а решать более сложные задачи ты не способен, поэтому ты выбираешь путь менеджера.

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

Что касается меня, то я определённо не собираюсь становиться менеджером. Некоторая математическая база у меня есть, которая сильно пригодилась в моей текущей работе. А дальше посмотрим. Математическая база – это, конечно хорошо, но хочется копать вглубь и развиваться в какой-то конкретной области. Возможно, это будет связано с функциональным программированием.

понедельник, 20 июля 2015 г.

Ковариантность/контравариантность в Java без wildcard'ов

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

Например:

  • Тип Optional<A> ковариантен. То есть, например, всегда можно привести Optional<Integer> к Optional<Number> и это будет безопасно.
  • Тип Consumer<A> контравариантен. Это значит, что всегда можно безопасно привести Consumer<Number> к Consumer<Integer>.
  • Наконец, тип List<A> инвариантен, так как он не является ни ковариантным, ни контравариантным . То есть нельзя безопасно привести не от List<Integer> к List<Number> и не от List<Number> к List<Integer>
Так получилось, что в Java ввели полиморфные типы не сразу, а лишь в пятой версии. Поэтому, когда в то время типы снабжали параметрами, оказалось, что 99% типов оказались инвариантными. Но что делать в тех случаях, когда программист имеет переменную типа List<Integer>, а функция принимает аргумент типа List<Number>? Как привести List<Integer> к List<Number>, если List не является ковариантным? Для этого в Java ввели костыль под названием usage-site variance: каждый раз объявляя переменную, вы решаете, является ли тип этой переменной ковариантным, контравариантным или инвариантным. Например:
  • Если вам нужно только читать из списка, то вы объявляете тип переменной как ковариантный: List<? extends Number>.
  • Если вам нужно только писать в список, то вы объявляете тип переменной как контравариантный: List<? super Number>.
  • Наконец, если вам нужно и читать из списка, и писать в него, то вы пишете просто List<Number>, то есть объявляете тип переменной как инвариантный.
Печально то, что люди используют usage-site variance даже в тех случаях, когда тип является ковариантным или контравариантным, усложняя сигнатуры функций и таская по всему проекту типы с этими дурацкими символами вопроса (wildcard'ами).

В этом посте я предлагаю альтернативу wildcard'ам. Конечно, лучше бы если бы в Java, наконец, ввели declaration site variance на уровне языка, т.е. возможность указать ковариантность/контравариантность типа во время его объявления (Optional<out A> / Consumer<in A>), и я не понимаю, почему это до сих пор не сделали.

Итак, правило следующее. Если вы объявляете ковариантный тип, то вы добавляете метод widen() со следующей сигнатурой:

public class Optional<A> {
  ...

  public static <B, A extends B> Optional<B> widen(Optional<A> opt) {
    return (Optional) opt;
  }
}

Теперь всегда, когда вам нужно привести Optional<Integer> Optional<Number> вы пишите:

Optional<Integer> opt = ...;
Optional<Number> opt2 = Optional.widen(opt);

И всё, никаких wildcard'ов.

Аналогично для контравариантных типов вы добавляете метод narrow():

public interface Consumer<A> {
  ...

  public static <A, B extends A> Consumer<B> narrow(Consumer<A> con) {
    return (Consumer) con;
  }
}

Теперь если нужно привести Consumer<Number> к Consumer<Integer>, вы пишите:

Consumer<Number> con = ...;
Consumer<Integer> con2 = Consumer.narrow(con);
По-моему, такой подход делает код намного чище. Я понимаю, что мы заменили одно уродство на другое, но я думаю, что моё уродство является менее страшным.

четверг, 18 июня 2015 г.

Про дублирование кода

Я не понимаю людей, которые всерьёз оправдывают дублирование кода. Ну не понимаю и всё. Причём, говорят они вовсе не о мелких часто повторяемых конструкциях, а о вполне больших и сложных кусках логики на несколько сотен строк кода!
В свою защиту они приводят следующие аргументы:
  • "Вот когда мне понадобится третий раз скопировать код, тогда и вынесу общую часть в отдельный модуль".
  • "Эти куски кода лежат в разных проектах, поэтому чтобы выделить общую часть нужно создавать общую зависимость".
По-моему, всё это полнейший бред. Единственное логичное объяснение, почему люди не хотят бороться с дублированием кода, – это элементарная лень и непонимание последствий. А последствия дублирования кода часто бывают катастрофическими.
Обычно проявляется это следующим образом: в программе обнаруживается критический баг, причиной которого является допущенная ошибка в этом самом куске кода. Это баг назначают не на автора кода, а на другого члена команды. Этот член команды понятия не имеет, что где-то в другом месте проекта есть точно такой же кусок кода, и соответственно исправляет ошибку только в одном месте, но не исправляет в другом. Итог: баг закрыт, но только для определённых сценариев использования программы, когда вызывается именно этот участок кода. Но другие сценарии, когда вызывается другой, скопированный участок кода, по-прежнему будут работать ошибочно.
Другой вариант развития событий – член команды (который не в теме копипасты кода) оказался более проницательным и всё-таки смог обнаружить, что у этого кода есть дубликат в другой части проекта. Соответственно, он выбирает одно из двух: либо исправляет ошибку в обоих местах (по-сути, оставляет дублирование), либо героически берётся выносить дублирующийся код в общий модуль. Но выносить дублирующийся код – это часто задача не из простых, потому что ты должен отлично понимать логику, которую писал автор, чтобы ничего не поломать. А если логика сложная, то ты потратишь уйму драгоценного времени для её понимания. Хорошо если автор этого кода ещё работает в проекте, и у него можно спросить, что же он там имел в виду. А если он уже давно уволился? Вот если бы автор кода сразу вынес код в общий модуль, то он бы сделал это гораздо быстрее, так как тогда был в теме и держал все детали в голове.
Дублирование кода – это способ выиграть полчаса времени при написании программы засчёт траты в будущем от одного часа до целого рабочего дня. Это может быть приемлемо для какого-нибудь прототипа на Питоне, который вы точно знаете, что он будет гарантированно выброшен, но совершенно неприемлемо для больших и сложных программ, которые будут жить, развиваться и поддерживаться следующие несколько лет.