Заявление об ограничении ответственности: следующий пост является ОЧЕНЬ самоуверенным отрывком. Его проза, мнения, используемый язык и изображения могут оскорбить некоторых людей (хотя это и не является целью данной статьи), если это так: извините.

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

Когда я учился в старшей школе, я спрашивал всех знакомых, изучающих программирование: «На каком языке вы пишете код?».

Эта странная причуда сопровождала меня в моей профессиональной жизни, до такой степени, что люди, с которыми я работал, все еще помнят, что я классифицирую людей в ИТ-индустрии по тем языкам, которые они используют ежедневно. Тогда я много раз слышал слова «если вы умеете программировать, вы можете выучить любой язык в кратчайшие сроки». Спустя 7 лет после того, как я присоединился к команде, я не мог не согласиться с ними.

Из-за кучи странных совпадений, окружающих человека, имя которого останется неназванным, мне пришлось дважды решить одну и ту же задачу по кодированию. Сначала на Java, затем на Scala. Указанная задача заключалась в решении следующей задачи:

Найдите все уникальные конфигурации набора обычных шахматных фигур на шахматной доске с размерами MxN, где ни одна из фигур не в состоянии занять любую другую. Предположим, что цвет фигуры не имеет значения, и что среди фигур нет пешек. Напишите программу, которая принимает в качестве входных данных: Размеры доски: M, N Количество фигур каждого типа (короля, ферзя, слона, ладьи и коня), которые нужно попытаться разместить на доске.

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

При возврате решения укажите в своем ответе общее количество уникальных конфигураций для доски 7x7 с 2 королями, 2 ферзями, 2 слонами и 1 рыцарем. Также укажите время, которое потребовалось для получения окончательной оценки. Излишне говорить, что чем меньше время, тем лучше.

Эта проблема возникла, когда я согласился на собеседование с кадровой компанией, которая представит меня [Большой компании].

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

Мои знания о структурах данных и алгоритмах были просто достаточными. Так что я подошел к этой задаче как к типичной наивной проблеме с возвратом, которую можно решить с помощью жадного алгоритма, который во многом напоминал задачу N-Queen с большим количеством шахматных фигур. Ничего фантастического.

Он состоит в размещении фигуры на доске и проверке ее безопасности, другими словами, если она не атакована и не атакует другие фигуры на доске. Если это так, то это предварительное решение; в противном случае я выбрасываю доску.

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

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

Ява

Код находится здесь: https://github.com/ignaciomosca/ChessChallenge

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

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

Также есть метод атаки:

public abstract boolean attacks(ChessPiece piece);

Это я использую, чтобы указать, как движется фигура.

Это пример реализации одной из частей:

Хорошо, теперь, когда у нас есть элементы, давайте смоделируем доску.

А затем методы:

showBoard - не требует пояснений

place - кладет фигуру на доску

findPiece - находит кусок для заданной строки и столбца

isSafe - фигура может быть помещена на доску, не захватив ее, или наоборот

doesNotAttackOtherPieces - тоже не требует пояснений

Теперь перейдем к решению:

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

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

Для управления зависимостями я использовал Maven, ничего особенного. Интересной частью было использование флагов JVM для повышения производительности кода. В частности, использование ConcurrentMarkSweep и CMSParallelRemarkEnabled. Когда-то (примерно в 2016 году) G1 не был сборщиком мусора по умолчанию, как сейчас.

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

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

Когда дело доходит до производительности JVM, Java не перестает меня удивлять. Должен признаться, что у меня к JVM отношения любви-ненависти. Java была моим первым языком, и с самого начала я относился к JVM как к инопланетной технологии. Вы знаете, что он делает, но не знаете, как он это делает, и он скрывает от вас много сложностей. Это обоюдоострый меч, потому что, с одной стороны, я верю, что он будет быстрым, с другой стороны, когда он будет медленным (или не таким быстрым), я в конечном итоге почесываю голову и задаюсь вопросом, может ли текущая фаза Луны быть отвечает за работу моего кода. Тогда я начинаю осознавать ожидающий меня ужас; Мне придется использовать профилировщик и копаться в документации, чтобы найти, какой флаг JVM использовать для повышения производительности моего кода.

Вещи, которые я получил от этого:

  • Выберите сложную проблему и решите ее на удобном для вас языке.
  • Постарайтесь сделать свой код максимально чистым.
  • Используйте линтер.
  • ТЕСТ, ТЕСТ, ТЕСТ
  • Научитесь использовать профилировщик. Статический анализ кода уведет вас так далеко в поисках узких мест в производительности. В какой-то момент я не знал, почему мой код был таким медленным; оказывается, что реализация hashCode в Board была чрезвычайно неэффективной, что делало метод размещения необычно медленным.

Haskell

Код находится здесь: https://github.com/ignaciomosca/ChessChallengeHaskell

С точки зрения личной истории, со мной связалась другая компания, которая уже использовала это упражнение в качестве критерия приема на работу; Мне пришлось решать это на Scala, то есть на идиоматическом Scala с неизменяемостью и функциями высокого порядка. Меня попросили сделать этот репозиторий частным, чтобы уменьшить количество случаев «вдохновения кандидатов», и поэтому я решил решить эту проблему на Haskell, функциональном языке, менее снисходительном, чем Scala, когда дело доходит до использования чистых функций.

Основная масса раствора остается прежней. Чтобы определить, когда на доске используются все элементы, я добавил к доске переменную numberOfPieces, которая представляет количество фигур, которые были изначально указаны (причина этого будет объяснена позже).

Итак, мы получили ту же иерархию классов всего в трех строчках:

Как только я это сделал, я нашел решение:

В Haskell итерация по структурам данных осуществляется с помощью рекурсии, так что поцелуй за все, и пока до свидания, мы отправляемся в Immutable City. На этот раз мы собираемся изменить подход к проблеме, мы будем искать решение с хвостовой рекурсией. Мы получаем пустую доску, список фигур, которые нужно разместить, и пустой набор, куда будут добавлены решения. На этот раз создается впечатление, что у нас есть TestConfigurations и наборы решений из предыдущего решения, объединенные в один набор. Поэтому нам нужно найти способ различать, какие из этих плат являются решениями, а какие - конфигурациями, которые мы уже тестировали. Вот почему я добавил в Board переменную numberOfPieces. Потому что мы собираемся фильтровать готовые доски, и под словом «готово» я подразумеваю доски, на которых изначально указаны все элементы.

Поскольку мы используем рекурсию, нам нужно определить начальные условия: если больше нет частей, которые нужно использовать, мы возвращаем набор с решениями, отфильтрованными по методу «Готово». Вот и все, это наше исходное состояние.

Теперь наступает сложная часть, мы берем заголовок списка фигур и запускаем findCandidate, указанная функция берет доску и кусок и генерирует все возможные доски, на которых можно разместить эту фигуру, не захватывая какие-либо из usedPieces на плате уже есть. Мы применяем (с помощью карты) функцию findCandidate к каждой из доск, присутствующих в наборе решений. Это возвращает набор возможных досок, на которых можно разместить фигуры, не захватив другую. Мы будем рекурсивно запускать решения с оставшимися фишками и досками кандидатов, пока не останутся фишки. Когда это произойдет, мы отфильтруем доски, которые "готовы".

Мне очень понравился Haskell. Это несколько сложнее отладить, но довольно легко найти, где может быть ошибка.

Эту кодовую базу все еще можно улучшить.

Вещи, которые я получил от этого:

  • Не пытайтесь решить проблему, которую вы уже решили, «переводом» решения, это будет выглядеть чушь, и вы не многого получите от опыта.
  • Это та часть, с которой вы начинаете учиться. Вы выходите из своей зоны комфорта. Возьмите первое решение проблемы (на удобном для вас языке) в качестве предсезонного.
  • Отладить это практически невозможно. Делайте паузы. В какой-то момент я застрял, оставил код гнить около 3 месяцев, вернулся и решил все за полдня. Не советую делать это в профессиональной обстановке; вас наверняка уволят.
  • Компилировать с -O2
  • Используйте строгие поля для таких вещей, как Int, с BangPatterns.
  • ТЕСТ, ТЕСТ, ТЕСТ

Go

Код находится здесь: https://github.com/ignaciomosca/ChessChallengeGolang

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

По поводу этого репо есть анекдот. Мне было так стыдно за свое решение, что я удалил его целиком. Я так и не успел подсчитать количество решений. В CodeExchange и Reddit было несколько сообщений, в которых я просил дать обзор кода. Тем не менее, я все еще чувствовал, что написал кучу дрянного кода, и, поскольку я решил, что «изменчивость в кодовой базе не является моим стилем кодирования», я решил, что стану опытным разработчиком Haskell, и похоронил все это, удалив репо. А потом я пожалел о том, что сделал это, потому что чувствовал, что из этой неудачи можно было многому научиться. Это даже не неудача; это просто набор кода, который меня не устраивает.

К счастью, 3 месяца спустя я вспомнил, что сделал резервную копию всего моего GitHub в GitLab, поэтому я пошел туда в поисках своего дерьмового решения Go, и вот оно! Я, наверное, единственный человек на Земле, который счастлив найти тонну дерьмового кода, и тем не менее, это один из уроков этой публикации, никогда НИКОГДА не удаляйте написанные вами вещи. Сделайте репо закрытым, если вам слишком стыдно, но не уничтожайте его; вы никогда не знаете, когда это может вам понадобиться, или вы можете почувствовать вдохновение исправить или оптимизировать то, что вы делали в прошлом.

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

Итак, давайте напишем какой-нибудь голанг, чтобы я мог объяснить, почему это не лучший язык для решения этой проблемы, чтобы люди не думали, что я получил свои аргументы из сообщения Hacker News или чего-то в этом роде.

У Голанга есть наборы. Вот, например, совершенно правильный код:

map[string]bool

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

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

ChessPiece может быть ключом, поскольку int и rune сопоставимы.

Board не может, поскольку ChessPiece нельзя сравнивать, вам понадобятся универсальные шаблоны для реализации способа сравнения этих типов структур. В Java нет структур, есть объекты, которые могут реализовывать hashCode и compareTo (как мы видели ранее).

Я поспрашивал на Reddit и получил такой способ проверить, есть ли на двух досках одинаковые части:

Для тех из вас, кто любит Go, пожалуйста, не начинайте с аргумента, что преждевременная абстракция - это корень всего зла, я не пытаюсь заниматься программированием на уровне типов, я просто хочу поместить структуру внутри Set, поэтому я не получайте повторяющихся элементов. Выкидывать из окна 20 лет теории языков программирования во имя минимализма нерационально, компьютерное программирование - это не наведение порядка с Мари Кондо, и даже если бы это было так, ГЕНЕРИКИ ПОЛУЧАЮТ УДОВОЛЬСТВИЕ В МОЕЙ ЖИЗНИ.

Несмотря на эту критику, я хочу быть благодарным сообществу Reddit Golang, особенно leaf_bebop, который потратил время, отвечая на мои вопросы и просматривая мой дерьмовый код. То, как я структурировал свое решение, повлияло на предложения, которые были сделаны, чтобы помочь мне написать идиоматический код.

Сказать, что мои отношения с Голангом не совсем были любовью с первого взгляда, значит ничего не сказать. Тем не менее, необходимость для меня изучить это стала очевидной, учитывая, что я также проявляю некоторый интерес к DevOps, где Go стал лингва-франка. И поэтому мне пришлось создать новые проблемы, которые нужно было решить, чтобы научиться этому. Это было время, когда я узнал об упражнении. Exercism - это платформа, на которой вы решаете игрушечные задачи и получаете обзоры кода от экспертов, которые выступают в качестве наставников. Моим наставником был Джон Арундел (он до сих пор остается, так как мне нужно решить 5 упражнений, прежде чем закончить трек Go).

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

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

2) Я научился продуктивно работать без любимых инструментов. У меня не было IntelliJ, у меня не было системы типов, у меня не было монад, у меня не было функций высокого порядка, но все же мне приходилось решать разные вещи. Это придало мне уверенности в себе и помогло мне развить другую точку зрения, когда дело доходит до решения проблем.

Спасибо, Джон Арундел. Кстати, вы можете подписаться на него в Твиттере или купить его книгу: Cloud Native DevOps с Kubernetes. Создав такой гротескный показ продакт-плейсмента, я почувствовал себя женой Джима Керри в шоу Трумэна, рекламирующем Mococoa.

В какой-то момент я отказался от Go, потому что «изменчивость не была моим стилем кодирования». Я достаточно взрослый, чтобы понимать, когда я чушу себе чушь и избегаю чего-то только потому, что это сложно. Научитесь ловить себя на дерьмовых оправданиях того, что не делаете то, что вам нужно делать, и вы добьетесь прогресса в любом стремлении / цели, которую вы ставите перед собой.

У Golang есть много ресурсов, отличные инженеры и части современной критически важной инфраструктуры (например, Docker и Kubernetes), которые были написаны на нем. Иногда, когда я имею дело с медленной компиляцией Scala, я вижу повсюду кейсы для Golang.

Учитывая все обстоятельства, я думаю, что мог бы стать «следующим американским разработчиком Go».

Ржавчина

Код находится здесь: https://github.com/ignaciomosca/ChessChallengeRust

Итак, мы подошли к последнему изученному мной языку: Rust.

Rust - это язык системного программирования; в нем нет сборщика мусора; это заставляет задуматься о памяти. Вместо того, чтобы позволить вам использовать указатели, как вы считаете нужным, и называть это днем ​​(как это делает C), он использует так называемую проверку заимствований.

Средство проверки заимствований - это компонент компилятора Rust, который гарантирует, что у вас не будет ссылки на переменную, которой больше нет. Под «там» я подразумеваю переменную, которая возвращается из функции, или передается другой функции, не заимствуя ее, или перемещается в закрытие; таким образом ссылка становится недействительной, и программа проверки заимствований не позволит нашему коду скомпилироваться.

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

  • Я все еще белый пояс в Rust, поэтому мои объяснения того, как работает проверка заимствований, принесут больше вреда, чем пользы, очень похоже на бесчисленное множество руководств по монадам, которые мы видим в Интернете.
  • Документация отличная.
  • Вы научитесь справляться с этим, написав реальный код. Я бы предпочел видеть, как вы в течение 15 минут бьетесь головой о стену (образно говоря), работая над базовым упражнением, чем я буду болтать о том, насколько хороша программа проверки заимствований.

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

На этот раз использование рекурсии может привести к снижению производительности, поскольку в Rust нет оптимизации хвостового вызова. Итак, мы выбрали следующее итеративное решение:

Это решение создает стек и передает протестированные конфигурации в виде кортежей (board, usedPieces). Когда ничего не остается, он добавляется в набор решений. Благодаря этому нам больше не нужно фильтровать решения по «готовым».

Заключение

Я не знаю, можно ли из всего этого «сделать вывод», кроме нативного, не обязательно подразумевающего более быструю работу - заблуждение, которое у меня возникло с тех пор, как я начал программировать.

Вот диаграмма со временем выполнения:

Интересные факты: я до сих пор не умею играть в шахматы.

Кто-то, кто проявляет большой интерес к информатике и / или парадигмам программирования, может спросить: «А где же решение для логического программирования?» на что я отвечаю: «Нигде, потому что мне совсем не нравится парадигма». Или, может быть, мне следует сказать: «Мне не нравится Пролог», что является единственным опытом, который у меня был с указанной парадигмой. Думаю, можно было бы сказать, что это было скорее исследование разных языков (задуманных как инструменты) и поиск того, который заставил бы меня чувствовать себя наиболее комфортно, а не решать проблему всеми возможными способами, доступными в настоящее время.

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

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

Если вы участвуете в найме новых инженеров и собираетесь использовать техническую задачу для оценки навыков кандидата (совершенно действительный и заслуживающий одобрения способ оценки, если вы спросите меня), пожалуйста, проявите порядочность, чтобы выполнить работу, которую он или она сделала. Мы все взрослые, и сейчас 2019 год, и тем не менее, такие вещи продолжают происходить. Они посылают взрослым домашнее задание, а потом даже не смотрят на него. Есть причина, по которой некоторые из моих коллег категорически отказываются проводить собеседования / технические собеседования.

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

Но это всего лишь анекдоты.

Хотя это не помогло мне найти работу, я узнал, как активно выучить язык, на практике. Не читая книги или сообщения в блогах (или, что еще хуже, сообщения в блогах о мнениях других людей на каком-либо языке), ни просматривая видео на YouTube, ни проходя «курсы», которые просят вас заполнить пробелы в неполном фрагменте кода. сценарий обучения «Обезьяна-видит-обезьяна-делает». Волшебное место, которое кто-то умнее меня назвал «Учебный ад».

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

Некоторое время назад Эмили Пиллмор написала в Твиттере цитату Ронни Брауна:

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

Я хотел бы закончить этот пост на этой заметке. Идти по пути самопомощи, пить «кул-хид» и читать Пауло Коэло - последнее из моих намерений. При этом становится каким-то образом необходимо сформулировать цель решения одной и той же проблемы на 4 разных языках (5, если вы считаете решение Scala), особенно потому, что решение ее таким образом фактически бесполезно, поскольку, вероятно, существует комбинаторический подход, который решает ее на О (1).

Я считаю, что есть что-то выиграть от решения сложных проблем. Это не бессмысленная сизифова задача. Это заставляет нас изменить наш взгляд на проблему и понять, как инструменты, которые мы используем, влияют на форму решения проблемы. «Если вы знаете, как программировать, вы можете выучить любой язык в кратчайшие сроки» неверно, потому что в нем предполагается, что в глубине души все языки одинаковы, в конце концов, поскольку все они являются полными по Тьюрингу, все они имеют одинаковую силу, верно? Но если это правда, зачем вообще кому-то создавать новый язык вместо оптимизации компилятора?

TL;DR

Просто чувак, который направил свое ОКР, потратив последние пару лет на решение одной и той же проблемы на разных языках программирования и мучая других людей, заставляя их читать его дерьмовый код; только для того, чтобы сделать вывод, что обучение на основе проектов лучше «читать о всяких вещах», поскольку активное обучение сложнее, чем чтение сообщений в Hacker News или просмотр видеороликов с конференций на YouTube.

В прошлом году я потратил много времени на изучение различных языков программирования и тем самым слишком сильно распространил свои знания о языке программирования Scala, который я использую ежедневно. Я также много узнал о том, как другие языки решают проблемы и как они работают в глубине души. Думаю, это превращает меня в одного из тех мифических существ, известных как «Разработчики полиглотов».

2020 - это год, когда я вернусь к Scala 100% и исследую такие темы, как Scala Native, http4s, ZIO и cats-effect. Быть в курсе! И Java отличный 2020 год.