Русский
Русский
English
Статистика
Реклама

Haskell

Перевод Горячая четвёрка умирающих языков программирования

25.09.2020 16:06:58 | Автор: admin
Я занимался поиском лучших языков программирования 2020 года и наткнулся на страницы, на которых шла речь о языках, теряющих популярность. Я программист, и я понимаю, что любому программисту крайне важно знать о том, какие технологии являются актуальными, а какие нет.

Каждый программист это писатель.

Серкан Лейлек


Я, после того, как насмотрелся на отчёты о языках программирования, теряющих актуальность, выбрал 4 языка, которые, как я полагаю, уже не стоят того, чтобы их изучали. Я, ради подкрепления своих выводов, прибегну к некоторым показателям популярности языков. В частности, речь идёт об индексе PYPL (PopularitY of Programming Language Index, индекс популярности языков программирования), о данных Google Trends и о некоторых сведениях, которые можно найти на платформе YouTube.


Фрагмент рейтинга PYPL (источник)

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

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

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

1. Perl


Интерес к языку программирования Perl стремительно падает. Хорошие показатели он демонстрировал в период с 2004 по 2009 годы, а после этого начался спад. Хотя этот язык пока и не мёртв, но он уже и не очень-то жив.

Информацию по нему не особенно активно ищут на YouTube и в Google. Например, есть видео по Perl, загруженное 4 года назад и набравшее всего 240 тысяч просмотров.


Видео по Perl

Кроме того, показатели языка идут вниз и в рейтинге PYPL.

Я решил сравнить Perl с каким-нибудь другим языком, с Python в данном случае, и обратился к Google Trends.


Сравнение Perl (красная линия) и Python (синяя линия), последние 12 месяцев

Как видно, красная линия, представляющая Perl, находится где-то на уровне нуля.

2. Haskell


Язык Haskell выглядит лучше, чем Perl. Он, к тому же, используется во многих крупных компаниях вроде Facebook и IBM. На YouTube есть видео по Haskell, загруженное 5 лет назад. Оно набрало 585 тысяч просмотров.


Видео по Haskell

Посмотрим теперь на показатели Google Trends, сравним Haskell и Python.


Сравнение Haskell (синяя линия) и Python (красная линия), последние 5 лет

Haskell, в сравнении с Python, выглядит более чем скромно. Синяя линия, представляющая Haskell, стелется по земле, а Python, представленный красной линией, показывает уверенный восходящий тренд.

3. Objective-C


Язык Objective-C, если ориентироваться на рейтинг PYPL, вырос в популярности на 0,2%. А что будет, если взглянуть на данные с YouTube?


Видео по Objective-C

Видео, загруженное 5 лет назад, набрало 250 тысяч просмотров.

Обратимся теперь к показателям Google Trends.


Сравнение Objective-C (синяя линия) и Python (красная линия), последние 5 лет

Конечно, многие всё ещё пользуются Objective-C. Но, хотя по этому языку есть вакансии, если вы строите планы на будущее и посматриваете на Objective-C, то вам стоит переключить внимание на Swift.

4. Visual Basic for Applications


Visual Basic for Applications, VBA, был у всех на слуху в 2004 году, а вот после 2009 интерес к нему начал падать. Я, например, изучал этот язык в школе.

Рейтинг PYPL указывает на то, что популярность VBA упала на 0,2%.

В моей ленте на YouTube видео по VBA мне видеть не доводилось. Возможно, это говорит о падении интереса к данному языку. Правда, когда я поискал подобные видео, мне попалось одно, загруженное 9 месяцев назад, набравшее 810 тысяч просмотров.


Видео по VBA

Если посмотреть на данные по VBA, которые имеются на Google Trends, то окажется, что интерес к VBA с 2004 года стабильно падает.


Сравнение VBA (красная линия) и Python (синяя линия), c 2004 года по настоящее время

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

Python


Я занимаюсь серверной разработкой, используя Python. Я, кроме того, сделал несколько проектов, используя фреймворк Django. Что тут сказать мне нравится Python.

Это, если верить тому, что выдаёт Google, язык, который лучше других языков помогает в поиске работы в 2020 году.


Языки, знание которых помогает в поиске работы

Я, например, создал проект на Django. А именно, речь идёт о сайте с вопросами и ответами для разработчиков. Этот проект всё ещё в работе. Я расширяю его и занимаюсь его оптимизацией.

Python в рейтинге PYPL демонстрирует рост на 2,9%. Если поинтересоваться данными YouTube по просмотрам видео о Python, то окажется, что они, за короткие промежутки времени, набирают миллионы просмотров.


Видео по Python

Анализ исследования Stack Overflow


Выше я опирался на рейтинг PYPL, на данные с Google Trends и на анализ видео по интересующим меня языкам программирования на YouTube. Теперь же я обращусь к результатам опроса разработчиков, проведённого Stack Overflow в 2020 году. А именно, к данным по языкам программирования, на которых программисты пишут, но не хотят продолжать этим заниматься.


Данные опроса Stack Overflow (источник)

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


Зарплаты разработчиков и их связь с языками программирования (источник)

Итоги


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

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

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

Какими языками программирования вы дополнили бы список умирающих технологий из этой статьи?



Подробнее..

Перевод Почему я считаю Haskell хорошим выбором с точки зрения безопасности ПО?

31.05.2021 18:12:13 | Автор: admin


Команда Typeable понимает ценность безопасности. Мы любим Haskell, но стоит ли его выбирать, если ваша цель создание защищенного программного обеспечения? Хотелось бы сказать да, но как и для большинства эмпирических вопросов о разработке ПО, здесь просто нет объективного доказательства, подтверждающего, что Haskell или ещё какой-нибудьй язык программирования обеспечивает большую безопасность, чем любой другой. Нельзя сказать, что выбор языка в Typeable не имеет значения для безопасности, но какое именно значение он имеет, еще нужно подумать.


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


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


   Чисто техническая            Уязвимость, относящаяся        уязвимость                исключительно к предметной области                                                                                           Инструментарий Инструментарий  Нужно     должен исправить может помочь  подумать

На оси выше показан источник различных уязвимостей программного обеспечения. На крайнем правом конце мы видим ошибки, связанные исключительно с доменной областью, то есть ошибки, совершенно не зависящие от используемого инструментария. Примером такой ошибки являются контрольные вопросы, которые в начале 2000-х использовались многими веб-сервисами для восстановления паролей. Зачастую это были вопросы типа Девичья фамилия вашей матери?. Позднее, примерно в 2009-2010 годах, возникло такое явление, как социальные сети, и неожиданно девичья фамилия матери перешла в категорию общедоступной информации. Неважно, какую технологию вы используете для реализации такой схемы с контрольными вопросами. Эта схема все равно не работает.


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


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


В таких сервисах обычно есть соблазн записать файл пользователя непосредственно в файловую систему сервера. Однако под каким именем файла? Использовать непосредственно имя файла пользователя верный путь к катастрофе, так как оно может выглядеть как ../../../etc/nginx/nginx.conf, ../../../etc/passwd/ или любые другие файлы, к которым сервер имеет доступ, но не должен их изменять.


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


Использование шкалы


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


В идеале современный инструментарий должен практически полностью устранять чисто технические уязвимости. Например, большинство современных языков, таких как Haskell, C# и Java, по большей части обеспечивают защиту содержимого памяти и в целом предотвращают переполнение буфера, попытки дважды освободить одну и ту же ячейку, а также другие технические проблемы. Однако от правильного инструментария можно получить еще больше пользы. Например, легко представить себе систему, в которой имеется техническая возможность разделить абсолютный и относительный пути к файлу, что упрощает контроль атак с обходом каталога (path traversal), таких как загрузка пользователем файла поверх какого-нибудь важного конфигурационного файла системы.


Haskell нижняя часть шкалы


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


// From imaginary CSRF token protection:if ($tokenHash == $hashFromInternet->{'tokenHash'}) {  echo "200 OK - Request accepted", PHP_EOL;}else { echo "403 DENIED - Bad CSRF token", PHP_EOL;};

Видите, в чем здесь проблема? В большинстве динамических языков, таких как PHP, тип записи JSON определяется в процессе выполнения и зачастую основывается на структуре входных данных. Кроме того, в объектно-ориентированном программировании тип используется для выбора поведения с помощью динамической диспетчеризации, что фактически позволяет злоумышленнику выбирать, какой код будет исполнен. Наконец, в языке PHP равенство, выраженное через ==, зависит от типа вводимых данных, и в приведенном выше примере атакующий может полностью обойти защиту.


Аналогичная проблема возникла с Java (и другим языками, см. https://frohoff.github.io/appseccali-marshalling-pickles/). Java предложил исключительно удобный способ сериализации любого объекта на диск и восстановления этого объекта в исходной форме. Единственной досадной проблемой стало отсутствие способа сказать, какой объект вы ждете! Это позволяет злоумышленникам пересылать вам объекты, которые после десериализации в вашей программе превращаются во вредоносный код, сеющий разрушения и крадущий данные.


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


data Request = Request {csrfToken :: Token, ... other fields}doSomething :: Session -> Request -> Handler ()doSomething session request  | csrfToken session == csrfToken request = ... do something  | otherwise = throwM BadCsrfTokenError

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


Haskell середина шкалы


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


Прежде всего, в Haskell имеется возможность моделировать данные более точно по сравнению с такими языками, как как C, Javascript или даже Java. В основном это обусловлено удобством его синтаксиса и наличием типов-сумм. Точное моделирование данных имеет значение для безопасности, поскольку код домена в основном представляет собой модель некоторого реального явления. Чем меньше ее точность, тем больше возможностей имеют злоумышленники.


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


data SSN = Unknown | Redacted | SSN Text

А теперь сравним моделирование той же идеи с использованием строковых величин "", "<REDACTED>" и "191091C211A". Что произойдет, если пользователь введет "<REDACTED>" в поле ввода SSN? Может ли это в дальнейшем привести к проблеме? В Haskell об этом можно не беспокоиться.


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


storeFileUpload :: Path Abs File -> ByteString -> IO ()storeFileUpload path = ...

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


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


Haskell и ошибки домена


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


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


Однако это все догадки. Сообщество Haskell до сих пор достаточно мало, чтобы не быть объектом атак, а специалисты по Haskell в общем случае еще не так сильно озабочены проблемами безопасности, как разработчики на Javascript или Python.


Заключение


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

Подробнее..

Перевод Языки любимые и языки страшные. Зелёные пастбища и коричневые поля

07.05.2021 14:19:42 | Автор: admin


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

В опросах есть категории Самые страшные языки программирования (The Most Dreaded Programming Languages) и Самые любимые языки. Оба рейтинга составлены на основе одного вопроса:

На каких языках вы провели обширную работу по разработке за последний год, и на каких хотите работать в следующем году? (Если вы работаете с определённым языком и намерены продолжать это делать, пожалуйста, установите оба флажка).

Страшный язык это такой, с которым вы активно работаете в текущем году, но не хотите продолжать его использовать. Любимый язык тот, который вы широко используете и хотите продолжать использовать. Результаты интересны тем, что отражают мнения людей, которые активно используют каждый язык. Не учитываются мнения типа Я слышал, что Х это круто, когда люди высоко оценивают вещи, которые они НЕ используют, потому что они слышали, что это новый тренд. Обратное тоже правда: люди, которые выражают отвращение к какому-то языку, реально широко используют его. Они боятся языка не потому, что слышали о его сложности, а потому, что им приходится работать с ним и испытывать настоящую боль.

Топ-15 страшных языков программирования:
VBA, Objective-C, Perl, Assembly, C, PHP, Ruby, C++, Java, R, Haskell, Scala, HTML, Shell и SQL.

Топ-15 любимых языков программирования:
Rust, TypeScript, Python, Kotlin, Go, Julia, Dart, C#, Swift, JavaScript, SQL, Shell, HTML, Scala и Haskell.

В списке есть закономерность. Заметили?

Худший код тот, что написан до меня


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

Причина, по которой [разработчики] считают старый код бардаком, заключается в кардинальном, фундаментальном законе программирования: читать код труднее, чем писать его.

Джоэл Спольски Грабли, на которые не стоит наступать

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


Scott Adams Understood

Легко понять код, который вы пишете. Вы его выполняете и совершенствуете по ходу дела. Но трудно понять код, просто прочитав его постфактум. Если вы вернётесь к своему же старому коду то можете обнаружить, что он непоследовательный. Возможно, вы выросли как разработчик и сегодня бы написали лучше. Но есть вероятность, что код сложен по своей сути и вы интерпретируете свою боль от понимания этой сложности как проблему качества кода. Может, именно поэтому постоянно растёт объём нерассмотренных PR? Ревью пул-реквестов работа только на чтение, и её трудно сделать хорошо, когда в голове ещё нет рабочей модели кода.

Вот почему вы их боитесь


Если реальный старый код незаслуженно считают бардаком, то может и языки программирования несправедливо оцениваются? Если вы пишете новый код на Go, но должны поддерживать обширную 20-летнюю кодовую базу C++, то способны ли справедливо их ранжировать? Думаю, именно это на самом деле измеряет опрос: страшные языки, вероятно, будут использоваться в существующих проектах на коричневом поле. Любимые языки чаще используются в новых проектах по созданию зелёных пастбищ. Давайте проверим это.1

Сравнение зелёных и коричневых языков


Индекс TIOBE измеряет количество квалифицированных инженеров, курсов и рабочих мест по всему миру для языков программирования. Вероятно, есть некоторые проблемы в методологии, но она достаточно точна для наших целей. Мы используем индекс TIOBE за июль 2016 года, самый старый из доступных в Wayback Machine, в качестве прокси для определения языков, накопивших много кода. Если язык был популярным в 2016 году, скорее всего, люди поддерживают написанный на нём код.

Топ-20 языков программирования в списке TIOBE по состоянию на июль 2016 года: Java, C, C++, Python, C#, PHP, JavaScript, VB.NET, Perl, ассемблер, Ruby, Pascal, Swift, Objective-C, MATLAB, R, SQL, COBOL и Groovy. Можем использовать это в качестве нашего списка языков, которые с большей вероятностью будут использоваться в проектах по поддержке кода. Назовём их коричневыми языками. Языки, не вошедшие в топ-20 в 2016 году, с большей вероятностью будут использоваться в новых проектах. Это зелёные языки.


Из 22 языков в объединённом списке страшных/любимых 63% коричневых

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

Java, C, C++, C#, Python, PHP, JavaScript, Swift, Perl, Ruby, Assembly, R, Objective-C, SQL


Зелёный язык: язык, который вы с большей вероятностью будете использовать в новом проекте.

Go, Rust, TypeScript, Kotlin, Julia, Dart, Scala и Haskell

У TIOBE и StackOverflow разные представления о том, что такое язык программирования. Чтобы преодолеть это, мы должны нормализовать два списка, удалив HTML/CSS, шелл-скрипты и VBA.2

Конечно, простое деление на зелёные и коричневые упускает много нюансов, в том числе по размеру полей. Я ожидаю, что больше зелёных пастбищ должно быть на Swift, чем на Objective-C, но и нынешняя методика, кажется, охватывает всё, что нам нужно. В этом списке гораздо больше коричневых языков, чем зелёных, но это вполне ожидаемо, ведь новых языков ежегодно появляется относительно немного.

Теперь можно ответить на вопрос: люди действительно боятся языков или же они просто боятся старого кода? Или скажем иначе: если бы Java и Ruby появились сегодня, без груды старых приложений Rails и старых корпоративных Java-приложений для поддержки, их всё ещё боялись бы? Или они с большей вероятностью появились бы в списке любимых?

Страшные коричневые языки



Страшные языки на 83% коричневые

Топ страшных языков почти полностью коричневый: на 83%. Это более высокий показатель, чем 68% коричневых языков в полном списке.

Любимые зелёные языки



Любимые языки на 54% зелёные

Среди любимых языков 54% зелёных. В то же время в полном списке всего лишь 36% языков являются зелёными. И каждый зелёный язык есть где-то в списке любимых.

Ещё один недостаток человеческого характера заключается в том, что все хотят строить и никто не хочет заниматься обслуживанием.

Курт Воннегут

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

Другими словами, Rust, Kotlin и другие зелёные языки пока находятся на этапе медового месяца. Любовь к ним может объясняться тем, что программистам не надо разбираться с 20-летними кодовыми базами.

Устранение предвзятости




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

Цикл хайпа языков программирования


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


Цикл хайпа языков программирования

У меня под рукой нет данных, но я отчётливо помню, что Ruby был самым популярным языком в 2007 году. И хотя сегодня у него больше конкурентов, но сегодня Ruby лучше, чем тогда. Однако теперь его боятся. Мне кажется, теперь у людей на руках появились 14-летние приложения Rails, которые нужно поддерживать. Это сильно уменьшает привлекательность Ruby по сравнению с временами, когда были одни только новые проекты. Так что берегитесь, Rust, Kotlin, Julia и Go: в конце концов, вы тоже лишитесь своих ангельских крылышек.3



1. Сначала я придумал критерии. Я не искал данных, подтверждающих первоначальную идею.

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

Вот методика измерения TIOBE, а их исторические данные доступны только платным подписчикам, поэтому Wayback Machine. [вернуться]

2. HTML/CSS не являются тьюринг-полными языками, по этой причине TIOBE не считает их полноценными языками программирования. Шелл-скрипты измеряются отдельно, а VBA вообще не исследуется, насколько я понял. [вернуться]

3. Не все коричневые языки внушают страх: Python, C#, Swift, JavaScript и SQL остаются любимыми. Хотелось бы услышать какие-нибудь теории о причине этого феномена. Кроме того, Scala и Haskell два языка, к которым я питаю слабость единственные зелёные языки в страшном списке. Это просто шум или есть какое-то обоснование??? [вернуться]
Подробнее..

Повесть о стрелке и запятой

18.10.2020 20:21:07 | Автор: admin
В этой статье мы:

  • Познакомимся с сопряженными функторами
  • Узнаем, как отвечать на вопрос что такое каррирование
  • Притворимся, что у нас есть состояние (если есть только функции)
  • И вдогонку поиграемся с примитивной оптикой (линзами)


И все это с помощью нескольких определений теории категорий и двух простейших конструкций: стрелки и запятой.




В этой статье я намеренно буду избегать полных определений, набора аксиом и доказательств мне не хотелось бы просто копировать текст из Википедии или учебников. Но если хотите по-настоящему понять эти концепции теории категорий, настоятельно рекомендую этот блог. Я хочу показать, как взаимосвязаны, казалось бы, далекие друг от друга конструкции в программировании через призму математики. Еще будут примеры кода на Haskell.

Функции и кортежи



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

Что такое функция? Это отображение одного множества на другое. Это кусок кода, принимающий аргумент s и возвращающий результат a: s -> a.



Что такое кортеж? Это самое элементарное произведение двух любых типов s и a: (s, a).



Мы привыкли смотреть на эти две конструкции в инфиксной форме. Что если мы немного изменим угол обзора и посмотрим префиксно, то есть выставим операнды после оператора?




Теперь мы видим, что они очень даже похожи! Они принимают два аргумента в одном и том же порядке. Но есть нюанс: у стрелки первый аргумент s находится в негативной позиции, а у стрелки в позитивной (что делает стрелку профунктором, а запятую бифунктором). Поэтому давайте проигнорируем первые аргументы, а точнее, просто их зафиксируем:




И теперь они не только являются ковариантными функторами, но еще и формируют такое интересное отношение как сопряжение!



Но обо всем по порядку.

Категории, функторы, сопряжения



Категория это набор объектов и стрелок между ними:


What is a Category? Definition and Examples Math3ma

Функторы это отображения между категориями:


What is a Functor? Definition and Examples, Part 1 Math3ma

А сопряжение это особое отношение между функторами. То есть, если мы можем построить две такие коммутативные диаграммы, и равенства выполняются, мы говорим что F и G сопряженные функторы (F G):


What is an Adjunction? Part 2 (Definition) Math3ma

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

Сопряжение запятой и стрелки



Примерно так выглядит определение функтора в языках с параметрическим полиморфизмом:



Начнем с того, что функция и кортеж это функторы:



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



А в случае со стрелкой, определение отображения совпадает с композицией функций!

Пожалуй, определения сопряженного функтора в коде выглядит достаточно запутанно:



Поэтому вместо операции сопряжения слева и сопряжения справа, мы определим кое-что попроще единицу и коединицу. Помните те две коммутативные диаграммы? Операция соответствует коединице, а единице:


What is an Adjunction? Part 2 (Definition) Math3ma

Начнем с коединицы:

 :: f (g a) -> a-- Вместо f и g подставляем запятую и стрелку: :: (((,) s) ((->) s a)) -> a-- Раскрываем скобки для запятой - из префиксной нотации в инфиксную: :: (s, ((->) s a)) -> a-- Раскрываем скобки для стрелки - из префиксной нотации в инфиксную: :: (s, s -> a) -> a


Что у нас тут получилось? Коединица для запятой и стрелки, которая принимает какой-то аргумент s, функцию из s в a. Хм, выглядит как применение функции:

 :: (s, s -> a) -> a (x, f) = f x


Отлично, 90% работы проделано осталось еще 90%. Теперь возьмемся за единицу:

 :: a -> g (f a)-- Вместо f и g подставляем запятую и стрелку: :: a -> ((->) s ((,) s a))-- Раскрываем скобки для стрелки - из префиксной нотации в инфиксную: :: a -> s -> ((,) s a)-- Раскрываем скобки для запятой - из префиксной нотации в инфиксную: :: a -> s -> (s, a)


Итак, единица принимает s, a и собирает их в кортеж в обратном порядке, проще некуда:

 :: a -> s -> (s, a) x s = (s, x)


А теперь вернемся к тем самым операциям, которые выглядели так страшно еще несколько абзацев назад:



Попробуем реализовать сопряжение слева и сопряжение справа:



Уффф, что-то легче не стало. Порой, в попытке что-то понять, у нас нет другого выбора кроме как посмотреть на это очень внимательно. Давайте прямо сейчас это и сделаем.

Сопряжение слева: принимает функцию, которая принимает кортеж (s, a) -> b и результатом становится функция, которая поочередно принимает по одному аргументу сначала a, потом s:



Сопряжение слева наоборот: принимает функцию, которая возвращает функцию (a -> (s -> b)) и результатом становится выражение, которое принимает кортеж.



Вам это ничего не напоминает? Да это же каррирование!

curry :: ((a, s) -> b) -> a -> s -> buncurry :: (a -> s -> b) -> (a, s) -> b

(Единственное отличие в том, что аргументы a и s идут в обратном порядке)

Если вдруг вам на собеседовании зададут вопрос что такое каррирование, можете смело отвечать: Это всего лишь сопряжение слева функтора кортежа по функтору функции, в чем проблема?

Комбинаторика функторов



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



На место f и g подставляем запятую и стрелку (с фиксированными аргументами) соответственно:



Выглядит как-то знакомо, правда? Еще как! В одной комбинации мы получаем комонаду Store, а в другой монаду State.




И они также образуют сопряжение!



Состояние и хранилище



Что мы себе представляем в первую очередь, когда создаем программы, которые зависят не только от своего аргумента, но и от какого-нибудь состояния? Наверное, то, что мы кладем/извлекаем некоторое значение в коробочке. Или, в случае конечного автомата, мы концентрируемся на переходах:



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

type State s a = s -> (s, a)-- Изменить хранимое состояние с помощью функцииmodify :: (s -> s) -> State s ()modify f s = (f s, ())-- Получить хранимое состояниеget :: State s sget s = (s, s)-- Заменить хранимое состояние другимput :: s -> State s ()put new _ = (new, ())


Хранилище это что-то совершенно другое. Мы храним некоторый источник s и инструкцию о том, как из этого s получить а:

type Store s a = (s, s -> a)pos :: Store s a -> spos (s, _) = speek :: s -> Store s a -> apeek s (_, f) = f sretrofit :: (s -> s) -> Store s a -> Store s aretrofit g (s, f) = (g s, f)


Где же это может использоваться?

Фокусируемся с линзами



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



И мы можем построить это увеличительное стекло с помощью комонады хранилища:



Как только нам удалось сфокусироваться на нужной нам части данных, мы можем просмотреть, что там лежит, заменить эту часть новой или изменить ее с помощью функции:

view :: Lens s t -> s -> tview lens = pos . lensset :: Lens s t -> t -> s -> sset lens new = peek new . lensover :: Lens s t -> (t -> t) -> s -> sover lens f = extract . retrofit f . lens


Изменяем состояние с линзами



Давайте рассмотрим эту магию в действии. Жил был человек:

data Person = Person Name Address (Maybe Job)


У нас есть линзы, которые могут фокусироваться на его работе и адресе проживания:

job :: Lens Person (Maybe Job)address :: Lens Person Address


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

hired :: State (Maybe Job) Cityrelocate :: City -> State Address ()


Итак, у нас есть выражение hired, которое оперирует состоянием текущей работы (Maybe Job). Также есть выражение, которое принимает в качестве аргумента город и в зависимости от этого меняет свое состояние Address. Оба выражения имеют эффект состояния, но мы не можем их использовать вместе, потому что тип состояний разный.

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

zoom :: Lens bg ls -> State ls a -> State bg a


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

zoom job hired :: State Person Cityzoom address (relocate _) :: State Person ()


И связываем эти выражения в монадный конвеер! Теперь мы имеем доступ к состоянию Person во всем связанном выражении:

zoom job hired >>= zoom address . relocate


Исходники с определениями можно найти здесь.
Подробнее..

Пытаясь композировать некомпозируемое монады

04.01.2021 22:21:38 | Автор: admin

Сколько раз вы слышали эту мантру "монады не композируются"? Я потратил достаточно много времени, чтобы попробовать опровергнуть это утверждение, пытаясь решить проблему в лоб. Но как и многие вещи в математике, порой, чтобы попробовать что-то понять, иногда стоит сменить масштаб.

Рекомендуется прочитать первую и вторую части этой серии, если вы еще этого не сделали.

Когда мы хотим слить два эффекта в один, то есть сцепить их в трансформер, у нас есть два варианта: вложить левый в правый, либо правый в левый. Эти два варианты определены со схемами TU и UT:

newtype TU t u a = TU (t :. u := a)newtype UT t u a = UT (u :. t := a)

Как нам уже известно из предыдущих частей этого цикла, для вычислений с неизменяемым окружением (Reader) достаточно прямой композиции функторов, а для эффектов обработки ошибок (Maybe и Either) подходит схема с обратной композицией UT.

type instance Schema (Reader e) = TU ((->) e)type instance Schema (Either e) = UT (Either e)type instance Schema Maybe = UT Maybe

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

(<$$>) :: (Functor t, Functor u) => (a -> b) -> t :. u := a -> t :. u := b(<$$>) = (<$>) . (<$>)(<**>) :: (Applicative t, Applicative u) => t :. u := (a -> b) -> t :. u := a -> t :. u := bf <**> x = (<*>) <$> f <*> xinstance (Functor t, Functor u) => Functor (TU t u) where    fmap f (TU x) = TU $ f <$$> xinstance (Applicative t, Applicative u) => Applicative (TU t u) where    pure = TU . pure . pure    TU f <*> TU x = TU $ f <**> xinstance (Functor t, Functor u) => Functor (UT t u) where    fmap f (UT x) = UT $ f <$$> xinstance (Applicative t, Applicative u) => Applicative (UT t u) where    pure = UT . pure . pure    UT f <*> UT x = UT $ f <**> x

Проблемы возникают, если мы попробуем описать монады. Непонятно, как найти обобщенный способ, учитывая, что оба эффекта нам неизвестны:

instance (Monad t, Monad u) => Monad (TU t u) where  x >>= f = ???instance (Monad t, Monad u) => Monad (UT t u) where  x >>= f = ???

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

instance Monad u => Monad (TU ((->) e) u) where    TU x >>= f = TU $ \e -> x e >>= ($ e) . run . finstance Monad u => Monad (UT (Either e) u) where    UT x >>= f = UT $ x >>= \case        Left e -> pure $ Left e        Right r -> run $ f rinstance Monad u => Monad (UT Maybe u) where    UT x >>= f = UT $ x >>= \case        Nothing -> pure Nothing        Just r -> run $ f r

В случае обработки ошибок (Maybe и Either), мы можем увидеть похожее поведение: если инвариант содержит параметр a, тогда цепочка вычислений продолжается. Это невероятно похоже на Traversable! Вот так он выглядит:

class (Functor t, Foldable t) => Traversable t where    traverse :: Applicative f => (a -> f b) -> t a -> f (t b)instance Traversable Maybe where    traverse _ Nothing = pure Nothing    traverse f (Just x) = Just <$> f xinstance Traversable (Either a) where    traverse _ (Left x) = pure (Left x)    traverse f (Right y) = Right <$> f y

Давайте попробуем его использовать:

instance (Traversable t, Monad t, Monad u) => Monad (UT t u) where    UT x >>= f = UT $ x >>= \i -> join <$> traverse (run . f) i

Работает! То есть, мы можем применять связывание для трансформера с известным нам Traversable эффектом.

А что нам делать с TU, для которого подходит эффект неизменяемого окружения - Reader? Я правда не знаю. Но мы можем кое-что попробовать - возьмём класс антипод Traversable - Distributive. И какое счастье, для него может быть определен Reader (точнее, его внутренняя часть - (->) e)!

class Functor g => Distributive g where    collect :: Functor f => (a -> g b) -> f a -> g (f b)instance Distributive ((->) e) where    collect f q e = flip f e <$> q

Но почему именно эти два класса функторов и почему они антиподы по отношению друг к другу? Понять это помогут модификации их методов, где вместо функций a -> t b мы подставляем функцию, которая ничего не меняет - id:

sequence :: (Traversable t, Applicative u) => t (u a) -> u (t a)sequence = traverse iddistribute :: (Distributive t, Functor u) => u (t a) -> t (u a)distribute = collect id

Вот оно! Мы можем увидеть, что эти методы позволяют нам менять порядок эффектов. Раз уж сработал Traversable для обратной композиции, может Distributive подойдет для прямой?

instance (Monad t, Distributive t, Monad u) => Monad (TU t u) where    TU x >>= f = TU $ x >>= \i -> join <$> collect (run . f) i

Тоже работает! Значит у нас уже есть определенный алгоритм, как определять монады для таких трансформеров:

  • Обратная схема UT - полагайся на Traversable.

  • Прямая схема TU - полагайся на Distributive.

Но у нас есть также схема посложнее, которая используется в монаде State и комонаде Store:

newtype TUT t t' u a = TUT (t :. u :. t' := a)newtype State s a = State ((->) s :. (,) s := a)newtype Store s a = Store ((,) s :. (->) s := a)type instance Schema (State s) = TUT ((->) s) ((,) s)type instance Schema (Store s) = TUT ((,) s) ((->) s)

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

instance (Functor t, Functor t', Functor u) => Functor (TUT t t' u) where    fmap f (TUT x) = TUT $ f <$$$> x

Так, стрелка ( (->) s) является Distributive, а запятая ((,) s) - Traversable... Но между этими двумя эффектами также существует связь посильнее, которая называется сопряжением (подробнее можно почитать здесь):

class Functor t => Adjunction t u where    leftAdjunct  :: (t a -> b) -> a -> u b    rightAdjunct :: (a -> u b) -> t a -> b    unit :: a -> u :. t := a    unit = leftAdjunct id    counit :: t :. u := a -> a    counit = rightAdjunct idinstance Adjunction ((,) s) ((->) s) where    leftAdjunct :: ((s, a) -> b) -> a -> (s -> b)     leftAdjunct f a s = f (s, a)    rightAdjunct :: (a -> s -> b) -> (s, a) -> b    rightAdjunct f (s, a) = f a s    unit :: a -> s -> (s, a)    unit x = \s -> (s, x)    counit :: (s, (s -> a)) -> a    counit (s, f) = f s

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

instance Monad (State s) where    State x >>= f = State $ rightAdjunct (run . f) <$> x    -- Или так: State x >>= f = State $ counit <$> ((run . f) <$$> x)    return = State . unit

А как быть в случае с трансформером? Тут у нас меж двух функторов ((->) s) и ((,) s) есть произвольный эффект, который надо учитывать. Для погружения в трансформер нам понадобится сопряжение слева, а для связывания - сопряжение справа:

instance (Adjunction t' t, Monad u) => Monad (TUT t t' u) where    x >>= f = TUT $ (>>= rightAdjunct (run . f)) <$> run x    return = TUT . (leftAdjunct pure)

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

instance (Adjunction t' t, Comonad u) => Comonad (TUT t' t := u) where    extend f x = TUT $ (=>> leftAdjunct (f . TUT)) <$> run x    extract = rightAdjunct extract . run

А что, если нам нужно не только погружать значение в трансформер, но еще и поднимать произвольный эффект до уровня трансформера? И тут нам понадобятся сопряжения! Причем, для монадных и комонадных трансформеров результаты невероятно симметричны...

instance (Adjunction t' t, Distributive t) => MonadTrans (TUT t t') where    lift = TUT . collect (leftAdjunct id)instance (Adjunction t' t, Applicative t, forall u . Traversable u) => ComonadTrans (TUT t' t) where    lower = rightAdjunct (traverse id) . run

Из этого всего, мы можем сделать выводы:

  • Если эффект имеет экземпляр Traversable - подходит обратная схема UT.

  • Если эффект имеет экземпляр Distributive - подходит прямая схема TU.

  • Если компоненты эффекта образуют сопряжение (Adjunction) - подходит схема TUT.

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

Исходники с определениями можно найтиздесь. Примеры применения описанной системы эффектов можно посмотреть тут.

Подробнее..

Сильные стороны функционального программирования

15.02.2021 16:13:26 | Автор: admin


Привет! Меня зовут Катерина, и я испытываю самые тёплые чувства к функциональному программированию, использую функциональный язык на постоянной основе и даже немного преподаю.

Основной язык разработки у нас в Typeable Haskell, и, пока все спорили о том, готов ли Haskell для продакшена, мы просто его использовали и считали конкурентным преимуществом. Нам хотелось бы поделиться своим мнением, основанным на этом опыте.

Как ФП улучшает программирование


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

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

  1. Код станет более лаконичным и выразительным. Выразительность можно определить как количество идей на единицу кода, и в целом функциональные языки, будучи более высокоуровневыми, оказываются и более выразительными. Например, преобразование каждого элемента в массиве или списке реализуется функциональным однострочником (используя map/foreach/whatever и анонимную функцию), в то время как в императивном стиле пришлось бы организовывать цикл, объявлять переменную для счётчика или итератора и использовать явное присваивание. Для более сложных примеров различие в выразительности только усиливается.
  2. Декомпозиция кода будет происходить более естественно. Принцип Разделяй и властвуй уже прочно закрепился в разработке и является базовым принципом борьбы со сложностью ПО. Речь идёт не о способе построения алгоритмов, а о более общем понятии. Например, даже простую программу, которая сначала читает целиком текст, разбивает его на слова и что-то делает с каждым словом, можно разбить на логические части: само чтение, разбиение прочитанного текста на слова (например, по пробелам) и создание структуры для хранения слов, обход этой структуры с преобразованием слов и печать результата. Каждое из этих действий можно реализовать отдельно и достаточно абстрактно, чтобы затем переиспользовать для решения других подобных задач. Декомпозиция кода на более мелкие и более общие части делает его гораздо более понятным (в том числе и для самого автора кода в будущем), позволяет избежать ошибок копипаста и упрощает дальнейший рефакторинг. Думаю, многим разработчикам приходилось копаться в своей или чужой простыне неструктурированного кода, написанного наспех, чтобы скорее заработало. Человеческому мозгу тяжело удерживать внимание на большом количестве сущностей одновременно и решать одну глобальную задачу сразу (working memory), поэтому для нас вполне естественно разбивать задачи на более мелкие, решать их по отдельности и комбинировать результат. В функциональном программировании эти мелкие задачи выражаются как небольшие вспомогательные функции, каждая из которых делает своё дело и её работу можно описать одним коротким предложением. А построение итогового результата это композиция таких функций. Конечно, разбить код на отдельные переиспользуемые части можно и в ООП, и в чисто императивном низкоуровневом языке типа C, и для этого уже есть известные принципы типа SOLID и GoF-паттерны, но, когда сам язык заставляет программиста думать в терминах функций, декомпозиция кода происходит гораздо более естественно.

  3. Побочные эффекты будут отделены от чистых функций. Чистая функция это функция в математическом смысле, результат работы которой зависит только от входных данных. В ходе вычисления такой функции не происходит ничего лишнего: не меняются значения переменных, ничего не читается и не печатается, не пишется в БД и не выполняются запросы к внешним сервисам. Действительно, вы же не будете ожидать таких действий от, скажем, тригонометрических функций? С помощью чистых функций можно реализовать большую часть логики работы с данными. Не все языки позволяют контролировать отсутствие побочных эффектов и проверять чистоту функций, но сам по себе функциональный подход мотивирует использовать чистые функции, которые работают без неожиданностей.
  4. Код станет проще отлаживать и тестировать. Этот пункт вытекает из двух предыдущих: у нас имеется набор небольших функций, часть из которых чистые, т.е. мы знаем, что их результат зависит только от входных данных. Код становится удобнее отлаживать достаточно проверить, что возвращают используемые функции по отдельности, чтобы понять, как они будут работать вместе. Так же легко пишутся юнит-тесты для чистой логики вашего приложения.

Как ФП улучшает программиста





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

  1. Изучение альтернативной парадигмы само по себе полезно для мозга, поскольку в процессе освоения программирования в функциональном стиле вы научитесь смотреть на привычные вещи по-другому. Кому-то такой способ мышления покажется гораздо более естественным, чем императивный. Можно долго спорить о том, что нужно и не нужно в индустриальном программировании, но там в любом случае нужны хорошие мозги, а их нужно тренировать. Осваивайте то, что не используете в работе: Лиспы, Пролог, Haskell, Brainfuck, Piet. Это поможет расширить кругозор и может стать для вас увлекательной головоломкой. Со временем вы сможете начать применять более элегантные решения в функциональном стиле, даже если пишете на императивном языке.
  2. За функциональными языками стоит серьёзная теория, которая тоже может стать частью ваших увлечений или даже исследований, если вы в той или иной степени хотите связать свою жизнь с computer science. Учиться никогда не поздно, особенно когда перед вами будут наглядные примеры использования довольно занятной теории для решения повседневных задач. Я бы никогда не подумала, что уже после окончания университета буду смотреть лекции по теории категорий, которую мой мозг когда-то отказывался воспринимать, и решать задачки из курса ручкой на бумаге, просто потому что мне это интересно.
  3. Помимо расширения кругозора увлечение ФП поможет вам расширить и круг общения. Возможно, за тусовкой функциональщиков закрепилась репутация академических снобов, которые спрашивают у вас определение монады перед тем, как продолжить общение. Я тоже так раньше думала, пока меня не вытащили на первые функциональные митапы и конференции. Я была совсем неопытным джуном и не знала определение монады, но не встретила по отношению к себе никакого негатива. Напротив, я познакомилась с интересными людьми, увлечёнными своим делом и готовыми делиться опытом, рассказывать и объяснять. Разумеется, в любом комьюнити есть совершенно разные люди, кто-то вам понравится больше, кто-то покажется токсичным и отталкивающим, и это совершенно нормально. Гораздо важнее то, что у вас появится возможность обмениваться идеями с теми, кто смотрит на мир разработки немного иначе и обладает другим опытом.
  4. Самый неожиданный для меня пункт: мне было легче всего найти работу именно на Haskell! На данный момент мой опыт работы чуть больше пяти лет, за это время на двух из трёх местах работы я писала на Haskell, и это был наиболее комфортный и безболезненный опыт трудоустройства. Более того, начинала я тоже с позиции Haskell-разработчика, о чём ни разу не пожалела. На первой работе я получила базовые навыки клиент-серверной разработки и работы с БД. Мы занимались такими же приземлёнными и ненаучными задачами, как и компании, использующие более распространённые языки. На популярных сайтах с вакансиями вы, скорее всего, почти не найдёте ничего по запросу Haskell-разработчик. В лучшем случае найдутся вакансии, где указано, что знание альтернативных парадигм будет преимуществом. Однако, это не значит, что таких вакансий нет. В Твиттере и тематических каналах в Телеграме вакансии появляются регулярно. Да, их мало, нужно знать, где искать, но и хороших специалистов такого узкого профиля тоже немного. Разумеется, вас не возьмут сразу и везде, но свою востребованность вы почувствуете значительно сильнее, чем при поиске работы на более распространённых языках. Возможно, компании могут быть готовы вкладываться в развитие программистов в нужном направлении: не можешь найти хаскелиста вырасти его сам!

Заключение


Появление элементов ФП в популярных языках индустриальной разработки, таких как Python, C++, Kotlin, Swift и т.д., подтверждает, что этот подход действительно полезен и обладает сильными сторонами. Применение функционального стиля позволяет получить более надёжный код, который проще разбивать на части, обобщать и тестировать, независимо от языка программирования. Разумеется, функциональный язык позволяет использовать все перечисленные преимущества по максимуму, предоставляя естественные конструкции с высокой степенью выразительности.

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

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

Создаем веб-приложение на Haskell с использованием Reflex. Часть 1

24.02.2021 20:16:16 | Автор: admin

Введение


Всем привет! Меня зовут Никита, и мы в Typeable для разработки фронтенда для части проектов используем FRP-подход, а конкретно его реализацию на Haskell веб-фреймоворк reflex. На русскоязычных ресурсах отсутствуют какие-либо руководства по данному фреймворку (да и в англоязычном интернете их не так много), и мы решили это немного исправить.


В этой серии статей будет рассмотрено создание веб-приложения на Haskell с использованием платформы reflex-platform. reflex-platform предоставляет пакеты reflex и reflex-dom. Пакет reflex является реализацией Functional reactive programming (FRP) на языке Haskell. В библиотеке reflex-dom содержится большое число функций, классов и типов для работы с DOM. Эти пакеты разделены, т.к. FRP-подход можно использовать не только в веб-разработке. Разрабатывать мы будем приложение Todo List, которое позволяет выполнять различные манипуляции со списком задач.



Для понимания данной серии статей требуется ненулевой уровень знания языка программирования Haskell и будет полезно предварительное ознакомление с функциональным реактивным программированием.

Здесь не будет подробного описания подхода FRP. Единственное, что действительно стоит упомянуть это два основных полиморфных типа, на которых базируется этот подход:


  • Behavior a реактивная переменная, изменяющаяся во времени. Представляет собой некоторый контейнер, который на протяжении всего своего жизненного цикла содержит значение.
  • Event a событие в системе. Событие несет в себе информацию, которую можно получить только во время срабатывания события.

Пакет reflex предоставляет еще один новый тип:


  • Dynamic a является объединением Behavior a и Event a, т.е. это контейнер, который всегда содержит в себе некоторое значение, и, подобно событию, он умеет уведомлять о своем изменении, в отличие от Behavior a.

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


Подготовка


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


Чтобы ускорить процесс сборки, имеет смысл настроить кэш nix. В случае, если вы не используете NixOS, то вам нужно добавить следующие строки в файл /etc/nix/nix.conf:


binary-caches = https://cache.nixos.org https://nixcache.reflex-frp.orgbinary-cache-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=binary-caches-parallel-connections = 40

Если используете NixOS, то в файл /etc/nixos/configuration.nix:


nix.binaryCaches = [ "https://nixcache.reflex-frp.org" ];nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];

В этом туториале мы будем придерживаться стандартной структуры с тремя пакетами:


  • todo-client клиентская часть;
  • todo-server серверная часть;
  • todo-common содержит общие модули, которые используются сервером и клиентом (например типы API).

Далее необходимо подготовить окружение для разработки. Для этого надо повторить действия из документации:


  • Создать директорию приложения: todo-app;
  • Создать проекты todo-common (library), todo-server (executable), todo-client (executable) в todo-app;
  • Настроить сборку через nix (файл default.nix в директории todo-app);
    • Также надо не забыть включить опцию useWarp = true;;
  • Настроить сборку через cabal (файлы cabal.project и cabal-ghcjs.project).

На момент публикации статьи default.nix будет выглядеть примерно следующим образом:


{ reflex-platform ? ((import <nixpkgs> {}).fetchFromGitHub {    owner = "reflex-frp";    repo = "reflex-platform";    rev = "efc6d923c633207d18bd4d8cae3e20110a377864";    sha256 = "121rmnkx8nwiy96ipfyyv6vrgysv0zpr2br46y70zf4d0y1h1lz5";    })}:(import reflex-platform {}).project ({ pkgs, ... }:{  useWarp = true;  packages = {    todo-common = ./todo-common;    todo-server = ./todo-server;    todo-client = ./todo-client;  };  shells = {    ghc = ["todo-common" "todo-server" "todo-client"];    ghcjs = ["todo-common" "todo-client"];  };})

Примечание: в документации предлагается вручную склонировать репозиторий reflex-platform. В данном примере мы воспользовались средствами nix для получения платформы из репозитория.

Во время разработки клиента удобно пользоваться инструментом ghcid. Он автоматически обновляет и перезапускает приложение при изменении исходников.


Чтобы убедиться, что все работает, добавим в todo-client/src/Main.hs следующий код:


{-# LANGUAGE OverloadedStrings #-}module Main whereimport Reflex.Dommain :: IO ()main = mainWidget $ el "h1" $ text "Hello, reflex!"

Вся разработка ведется из nix-shell, поэтому в самом начале необходимо войти в этот shell:


$ nix-shell . -A shells.ghc

Для запуска через ghcid требуется ввести следующую команду:


$ ghcid --command 'cabal new-repl todo-client' --test 'Main.main'

Если все работает, то по адресу localhost:3003 вы увидите приветствие Hello, reflex!



Почему 3003?


Номер порта ищется в переменной окружения JSADDLE_WARP_PORT. Если эта переменная не установлена, то по умолчанию берется значение 3003.


Как это работает


Вы можете заметить, мы использовали при сборке не GHCJS, а обычный GHC. Это возможно благодаря пакетам jsaddle и jsaddle-warp. Пакет jsaddle предоставляет интерфейс для JS для работы из-под GHC и GHCJS. С помощью пакета jsaddle-warp мы можем запустить сервер, который посредством веб-сокетов будет обновлять DOM и играть роль JS-движка. Как раз для этого и был установлен флаг useWarp = true;, иначе по умолчанию использовался бы пакет jsaddle-webkit2gtk, и при запуске мы бы увидели десктопное приложение. Стоит отметить, что еще существуют прослойки jsaddle-wkwebview (для iOS приложений) и jsaddle-clib (для Android приложений).


Простейшее приложение TODO


Приступим к разработке!


Добавим следующий код в todo-client/src/Main.hs.


{-# LANGUAGE MonoLocalBinds #-}{-# LANGUAGE OverloadedStrings #-}module Main whereimport Reflex.Dommain :: IO ()main = mainWidgetWithHead headWidget rootWidgetheadWidget :: MonadWidget t m => m ()headWidget = blankrootWidget :: MonadWidget t m => m ()rootWidget = blank

Можно сказать, что функция mainWidgetWithHead представляет собой элемент <html> страницы. Она принимает два параметра head и body. Существуют еще функции mainWidget и mainWidgetWithCss. Первая функция принимает только виджет с элементом body. Вторая первым аргументом принимает стили, добавляемые в элемент style, и вторым аргументом элемент body.


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

Функция blank равносильна pure () и она ничего не делает, никак не изменяет DOM и никак не влияет на сеть событий.


Теперь опишем элемент <head> нашей страницы.


headWidget :: MonadWidget t m => m ()headWidget = do  elAttr "meta" ("charset" =: "utf-8") blank  elAttr "meta"    (  "name" =: "viewport"    <> "content" =: "width=device-width, initial-scale=1, shrink-to-fit=no" )    blank  elAttr "link"    (  "rel" =: "stylesheet"    <> "href" =: "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"    <> "integrity" =: "sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"    <> "crossorigin" =: "anonymous")    blank  el "title" $ text "TODO App"

Данная функция сгенерирует следующее содержимое элемента head:


<meta charset="utf-8"><meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport"><link crossorigin="anonymous" href="http://personeltest.ru/aways/stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"  integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" rel="stylesheet"><title>TODO App</title>

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


Функция elAttr имеет следующий тип:


elAttr :: forall t m a. DomBuilder t m => Text -> Map Text Text -> m a -> m a

Она принимает название тэга, атрибуты и содержимое элемента. Возвращает эта функция, и вообще весь набор функций, строящих DOM, то, что возвращает их внутренний виджет. В данном случае наши элементы пустые, поэтому используется blank. Это одно из наиболее частых применений этой функции когда требуется сделать тело элемента пустым. Так же используется функция el. Ее входными параметрами являются только название тэга и содержимое, другими словами это упрощенная версия функции elAttr без атрибутов. Другая функция, используемая здесь text. Ее задача вывод текста на странице. Эта функция экранирует все возможные служебные символы, слова и тэги, и поэтому именно тот текст, который передан в нее, будет выведен. Для того чтобы встроить кусок html, существует функция elDynHtml.


Надо сказать, что в приведенном выше примере использование MonadWidget является избыточным, т.к. эта часть строит неизменяемый участок DOM. А, как было сказано выше, MonadWidget позволяет строить или перестраивать DOM, а также позволяет определять сеть событий. Функции, которые используются здесь, требуют только наличие класса DomBuilder, и тут, действительно, мы могли написать только это ограничение. Но в общем случае, ограничений на монаду гораздо больше, что затрудняет и замедляет разработку, если мы будем прописывать только те классы, которые нам нужны сейчас. Поэтому существует класс MonadWidget, которые представляет собой эдакий швейцарский нож. Для любопытных приведём список всех классов, которые являются надклассами MonadWidget:


type MonadWidgetConstraints t m =  ( DomBuilder t m  , DomBuilderSpace m ~ GhcjsDomSpace  , MonadFix m  , MonadHold t m  , MonadSample t (Performable m)  , MonadReflexCreateTrigger t m  , PostBuild t m  , PerformEvent t m  , MonadIO m  , MonadIO (Performable m)#ifndef ghcjs_HOST_OS  , DOM.MonadJSM m  , DOM.MonadJSM (Performable m)#endif  , TriggerEvent t m  , HasJSContext m  , HasJSContext (Performable m)  , HasDocument m  , MonadRef m  , Ref m ~ Ref IO  , MonadRef (Performable m)  , Ref (Performable m) ~ Ref IO  )class MonadWidgetConstraints t m => MonadWidget t m

Теперь перейдем к элементу body страницы, но для начала определим тип данных, который будем использовать для задания:


newtype Todo = Todo  { todoText :: Text }newTodo :: Text -> TodonewTodo todoText = Todo {..}

Тело будет иметь следующую структуру:


rootWidget :: MonadWidget t m => m ()rootWidget =  divClass "container" $ do    elClass "h2" "text-center mt-3" $ text "Todos"    newTodoEv <- newTodoForm    todosDyn <- foldDyn (:) [] newTodoEv    delimiter    todoListWidget todosDyn

Функция elClass на вход принимает название тэга, класс (классы) и содержимое. divClass это сокращенная версия elClass "div".


Все выше описанные функции отвечают за визуальное представление и не несут в себе никакой логики, в отличие от функции foldDyn. Она является частью пакета reflex и имеет следующую сигнатуру:


foldDyn :: (Reflex t, MonadHold t m, MonadFix m) => (a -> b -> b) -> b -> Event t a -> m (Dynamic t b)

Она похожа на foldr :: (a -> b -> b) -> b -> [a] -> b и, по сути, выполняет такую же роль, только в роли списка здесь событие. Результирующее значение обернуто в контейнер Dynamic, т.к. оно будет обновляться после каждого события. Процесс обновления задаётся функцией-параметром, которая принимает на вход значение из возникшего события и текущее значение из Dynamic. На их основе формируется новое значение, которое будет находиться в Dynamic. Это обновление будет происходить каждый раз при возникновении события.


В нашем примере функция foldDyn будет обновлять динамический список заданий (изначально пустой), как только будет добавлено новое задание из формы ввода. Новые задания добавляются в начало списка, т.к. используется функция (:).


Функция newTodoForm строит ту часть DOM, в которой будет форма ввода описания задания, и возвращает событие, которое несет в себе новое Todo. Именно при возникновении этого события будет обновляться список заданий.


newTodoForm :: MonadWidget t m => m (Event t Todo)newTodoForm = rowWrapper $  el "form" $    divClass "input-group" $ do      iEl <- inputElement $ def        & initialAttributes .~          (  "type" =: "text"          <> "class" =: "form-control"          <> "placeholder" =: "Todo" )      let        newTodoDyn = newTodo <$> value iEl        btnAttr = "class" =: "btn btn-outline-secondary"          <> "type" =: "button"      (btnEl, _) <- divClass "input-group-append" $        elAttr' "button" btnAttr $ text "Add new entry"      pure $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl

Первое нововведение, которое мы встречаем тут, это функция inputElement. Ее название говорит само за себя, она добавляет элемент input. В качестве параметра она принимает тип InputElementConfig. Он имеет много полей, наследует несколько различный классов, но в данном примере нам наиболее интересно добавить нужные атрибуты этому тегу, и это можно сделать при помощи линзы initialAttributes. Функция value является методом класса HasValue и возвращает значение, которое находится в данном input. В случае типа InputElement оно имеет тип Dynamic t Text. Это значение будет обновляться при каждом изменении, происходящем в поле input.


Следующее изменение, которое тут можно заметить, это использование функции elAttr'. Отличие функций со штрихом от функций без штриха для построения DOM заключается в том, что эти функции вдобавок возвращают сам элемент страницы, с которым мы можем производить различные манипуляции. В нашем случае он необходим, чтобы мы могли получить событие нажатия на этот элемент. Для этого служит функция domEvent. Эта функция принимает название события, в нашем случае Click и сам элемент, с которым связано это событие. Функция имеет следующую сигнатуру:


domEvent :: EventName eventName -> target -> Event t (DomEventType target eventName)

Ее возвращаемый тип зависит от типа события и типа элемента. В нашем случае это ().


Следующая функция, которую мы встречаем tagPromptlyDyn. Она имеет следующий тип:


tagPromptlyDyn :: Reflex t => Dynamic t a -> Event t b -> Event t a

Ее задача заключается в том, чтобы при срабатывании события, поместить в него значение, которое находится в данный момент внутри Dynamic. Т.е. событие, являющееся результатом функции tagPromptlyDyn valDyn btnEv возникает одновременно с btnEv, но несёт в себе значение, которое было в valDyn. Для нашего примера это событие будет происходить при нажатии кнопки и нести в себе значение из текстового поля ввода.


Тут следует сказать про то, что функции, которые содержат в своём названии слово promptly, потенциально опасные они могут вызывать циклы в сети событий. Внешне это будет выглядеть так, как будто приложение зависло. Вызов tagPromplyDyn valDyn btnEv, по возможности, надо заменять на tag (current valDyn) btnEv. Функция current получает Behavior из Dynamic. Эти вызовы не всегда взаимозаменяемые. Если обновление Dynamic и событие Event в tagPromplyDyn возникают в один момент, т.е. в одном фрейме, то выходное событие будет содержать те данные, которые получил Dynamic в этом фрейме. В случае, если мы будем использовать tag (current valDyn) btnEv, то выходное событие будет содержать те данные, которыми исходный current valDyn, т.е. Behavior, обладал в прошлом фрейме.


Здесь мы подошли к еще одному различию между Behavior и Dynamic: если Behavior и Dynamic получают обновление в одном фрейме, то Dynamic будет обновлен уже в этом фрейме, а Behavior приобретет новое значение в следующем. Другими словами, если событие произошло в момент времени t1 и в момент времени t2, то Dynamic будет обладать значением, которое принесло событие t1 в промежутке времени [t1, t2), а Behavior (t1, t2].


Задача функции todoListWidget заключается в выводе всего списка Todo.


todoListWidget :: MonadWidget t m => Dynamic t [Todo] -> m ()todoListWidget todosDyn = rowWrapper $  void $ simpleList todosDyn todoWidget

Здесь встречается функция simpleList. Она имеет следующую сигнатуру:


simpleList  :: (Adjustable t m, MonadHold t m, PostBuild t m, MonadFix m)  => Dynamic t [v]  -> (Dynamic t v -> m a)  -> m (Dynamic t [a])

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


todoWidget :: MonadWidget t m => Dynamic t Todo -> m ()todoWidget todoDyn =  divClass "d-flex border-bottom" $    divClass "p-2 flex-grow-1 my-auto" $      dynText $ todoText <$> todoDyn

Функция dynText отличается от функции text тем, что на вход принимает текст, обернутый в Dynamic. В случае, если элемент списка будет изменен, то это значение также обновится в DOM.


Также было использовались 2 функции, о которых не было ничего сказано: rowWrapper и delimiter. Первая функция является оберткой над виджетом. В ней нет ничего нового и выглядит следующим образом:


rowWrapper :: MonadWidget t m => m a -> m arowWrapper ma =  divClass "row justify-content-md-center" $    divClass "col-6" ma

Функция delimiter просто добавляет элемент-разделитель.


delimiter :: MonadWidget t m => m ()delimiter = rowWrapper $  divClass "border-top mt-3" blank


Полученный результат можно посмотреть в нашем репозитории.


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

Подробнее..

Как мы выбираем языки программирования в Typeable

26.04.2021 18:04:48 | Автор: admin

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

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

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

  1. Baseline по скорости работы ПО.
  2. Особенности распространения и эксплуатации программы, например требование интерпретатора или возможность статической линковки.
  3. Экосистема библиотек и компонентов, которые можно переиспользовать. Отмечу что важно не только количество библиотек, но и качество релевантных для вас.
  4. Возможности параллельного/конкурентного/асинхронного исполнения программ, что может быть важным для многих систем.
  5. Сложность обучения людей выбранной технологии, что значительно влияет и на сообщество языка, и на перепрофилирование разработчиков.
  6. Выразительность языка.

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

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

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

  1. Статическая типизация должна поддерживаться языком программирования. Это позволяет сократить длительность для каждой итерации цикла изменения и валидации кода для разработчика. Также это позволяет существенно снизить количество багов, как со стороны функциональных требований, так и безопасности ПО.
  2. Алгебраические типы данных очень сложно переоценить влияние этой фичи, после того, как начинаешь ей пользоваться. Очень доступная и абсолютно необходимая при моделировании инвариантов вещь. Те же типы-суммы настолько незаменимы, что выбрать язык, в котором их нужно моделировать через другие конструкции, это ставить себе преграды уже на первом шаге.
  3. Гибкая возможности для поддержки и исполнения многопоточных программ. Языки с GIL (Global Interpreter Lock) сразу же не удовлетворяют этому требованию. Хочется иметь возможность хорошо утилизировать возможности железа и иметь достаточно высокоуровневые абстракции для работы.
  4. Достаточная экосистема библиотек, оцениваем субъективно их качество в том числе. Не считаем необходимым все подключать в виде библиотек, но наиболее базовые вещи, вроде биндингов к популярным базам данных, должны быть в наличии.
  5. Светлые головы в коммьюнити разработчиков на этом языке программирования. Профиль разработчика, которого бы мы хотели видеть своим сотрудником это заинтересованный в CS и разработке человек. В противопоставлении этому можно поставить статус легких в освоении технологий, которые привлекают людей в IT ради легкой наживы, что сильно размывает рынок спецов.
  6. В нашем распоряжении должны быть языки программирования, которые позволяют реализовывать ПО с жесткими требованиями ко времени обработки и потребления памяти.

В результате всего вышесказанного в нашем ящичке с инструментами есть достаточно блоков, которые позволяют нам занять уверенную позицию во многих проектах. Возвращаясь к шахматной аналогии, это наши принципы, которые позволяют вести позиционную игру. Позиционная игра игра направленная на создание долгосрочной позиции, открывающей игроку возможности и минимизирующей слабости. Она противопоставляется игре атакующей, острой, где есть большая доля риска, а атакующий игрок пытается эту игру завершить до того, как его противник сможет занять хорошую оборонительную позицию. Острая разработка это олимпиадное программирование, MVP для маркетинговых экспериментов, многие задачи в data science, да и зачастую создание ПО, сопровождающее Computer Science публикации. Их объединяет то, что долгой поддержки они зачастую не требуют, им только нужно отработать в определенный момент времени. Позиционная же игра это игра вдолгую, где ключевыми показателями являются поддерживаемость и обновляемость. Именно этим мы и занимаемся, и нам нужен хороший фундамент, для того чтобы быть уверенными в долгосрочной работе ПО, которое мы пишем и обновляем. Такие проекты тоже могут начинаться с MVP, но они делаются с совсем другими предпосылками.

Почему же список соображений для выбора технологий именно такой? Причин тут несколько. Во-первых, желательно исключить вопросы модности и трендовости технологий, для того чтобы увеличить предсказуемость на долгом промежутке времени. Компилятор с долгой историей и активным сообществом это пусть и консервативный, но надежный выбор, в противопоставление новым сверкающим вещам, появляющимся из года в года. Наверняка часть из них перейдет из последней категории в первую, но мы об этом узнаем позже, вероятно, через годы. Вместо трендов мы пытаемся использовать фундаментальный Computer Science и большое количество исследований по этой теме, которые нашли применение в используемых нами языках программирования. Например: теория типов это смежная математике и CS дисциплина, которая занимается фундаментальными вопросами формализации требований. Это ровно то, что нам нужно для написания ПО. К тому же это совокупный опыт других людей, занимающихся точными науками, и как-то глупо, на мой взгляд, этим опытом пренебрегать. Лучше взять за основу такую дисциплину, чем не взять ничего, либо же взять субъективное мнение, основанное на жизненном опыте одного отдельно взятого человека.

Во-вторых, мы ищем реализацию наибольшего количества взятых нами принципов в языках программирования и компиляторах. По этой причине вдобавок к нашему любимому Haskell, в нашем ящичке инструментов стал появляться Rust. С real-time требованиями и жесткими ограничениями на использование памяти нам нужно что-то достаточно низкоуровневое. Строгость типизации в C все же оставляет желать лучшего, поэтому если есть возможность использовать Rust для такой задачи, мы предпочтем это сделать.

Третья причина: мы делаем ПО в первую очередь для наших клиентов, и мы бы хотели, чтобы они были защищены от наших предрассудков. Поэтому при выборе инструмента риск не может превышать некий уровень, который должен быть оговорен с клиентом. Но даже с такими условиями у нас появились довольно маргинальные технологии вроде GHCJS, т.к. при совокупном разборе сильных и слабых сторон картинка все равно была привлекательной для нас и наших клиентов. Про то, как мы пришли к этому решению, мы уже писали: Elm vs Reflex.

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

Создаем веб-приложение на Haskell с использованием Reflex. Часть 3

17.05.2021 18:06:47 | Автор: admin

Часть 1.


Часть 2.


Всем привет! В этой части мы рассмотрим использование класса EventWriter и библиотеки ghcjs-dom.



Использование EventWriter


Сейчас, для того, чтобы прокинуть события с более глубоких уровней, мы передаем их в качестве возвращемых значений. Это не всегда удобно, особенно, когда надо возвращать что-то, помимо события (например, форма ввода может возвращать одновременно и событие нажатия кнопки, и данные из формы). Гораздо удобнее было бы использовать механизм, который может "прокинуть" события наверх автоматически, не задумываясь о том, что надо их постоянно возвращать. И такой механизм есть EventWriter. Этот класс позволяет записывать события, наподобие стандартной монады Writer. Перепишем наше приложение с использованием EventWriter.


Для начала рассмотрим сам класс EventWriter:


class (Monad m, Semigroup w) => EventWriter t w m | m -> t w where  tellEvent :: Event t w -> m ()

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


Существует трансформер, являющийся экземпляром этого класса EventWriterT, для его запуска используется функция runEventWriterT.


Далее переходим к изменению функций. Наибольшие изменения ожидают функцию rootWidget.


rootWidget :: MonadWidget t m => m ()rootWidget =  divClass "container" $ mdo    elClass "h2" "text-center mt-3" $ text "Todos"    (_, ev) <- runEventWriterT $ do      todosDyn <- foldDyn appEndo mempty ev      newTodoForm      delimiter      todoListWidget todosDyn    blank

Мы добавили запуск трансформера и избавились от всех возвращаемых событий.


Изменения в newTodoForm не такие большие, но все же, стоит их отметить:


newTodoForm :: (EventWriter t (Endo Todos) m, MonadWidget t m) => m ()newTodoForm = rowWrapper $ el "form" $ divClass "input-group" $ mdo  iEl <- inputElement $ def    & initialAttributes .~      (  "type" =: "text"      <> "class" =: "form-control"      <> "placeholder" =: "Todo" )    & inputElementConfig_setValue .~ ("" <$ btnEv)  let    addNewTodo = \todo -> Endo $ \todos ->      insert (nextKey todos) (newTodo todo) todos    newTodoDyn = addNewTodo <$> value iEl    btnAttr = "class" =: "btn btn-outline-secondary"      <> "type" =: "button"  (btnEl, _) <- divClass "input-group-append" $    elAttr' "button" btnAttr $ text "Add new entry"  let btnEv = domEvent Click btnEl  tellEvent $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl

Мы видим, что обновился тип функции, она теперь ничего не возвращает, и добавлено необходимое ограничение EventWriter. В теле функции, соответственно, мы избавились от возвращаемого значения и теперь используем функцию tellEvent.


Функция todoListWidget сильно упростилась.


todoListWidget  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Dynamic t Todos -> m ()todoListWidget todosDyn = rowWrapper $  void $ listWithKey (M.fromAscList . IM.toAscList <$> todosDyn) todoWidget

Нас теперь вообще не интересует возвращаемое событие, и, соответственно, отпала необходимость в извлечении Event из Dynamic.


В функции todoWidget также произошли заметные изменения. Больше нет необходимости работать с возвращаемым типом преобразовывать Event t (Event t TodoEvent). Отличие функции dyn_ от функции dyn, в том, что она игнорирует возвращаемое значение.


todoWidget  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Int -> Dynamic t Todo -> m ()todoWidget ix todoDyn' = do  todoDyn <- holdUniqDyn todoDyn'  dyn_ $ ffor todoDyn $ \td@Todo{..} -> case todoState of    TodoDone         -> todoDone ix todoText    TodoActive False -> todoActive ix todoText    TodoActive True  -> todoEditable ix todoText

Единственное изменение в функциях todoDone, todoActive и todoEditable это новый тип и запись события вместо его возврата.


todoActive  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Int -> Text -> m ()todoActive ix todoText = divClass "d-flex border-bottom" $ do  divClass "p-2 flex-grow-1 my-auto" $    text todoText  divClass "p-2 btn-group" $ do    (doneEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Done"    (editEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Edit"    (delEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Drop"    tellEvent $ Endo <$> leftmost      [ update (Just . toggleTodo) ix <$ domEvent Click doneEl      , update (Just . startEdit) ix  <$ domEvent Click editEl      , delete ix <$ domEvent Click delEl      ]todoDone  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Int -> Text -> m ()todoDone ix todoText = divClass "d-flex border-bottom" $ do  divClass "p-2 flex-grow-1 my-auto" $    el "del" $ text todoText  divClass "p-2 btn-group" $ do    (doneEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Undo"    (delEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Drop"    tellEvent $ Endo <$> leftmost      [ update (Just . toggleTodo) ix <$ domEvent Click doneEl      , delete ix <$ domEvent Click delEl      ]todoEditable  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Int -> Text -> m ()todoEditable ix todoText = divClass "d-flex border-bottom" $ do  updTodoDyn <- divClass "p-2 flex-grow-1 my-auto" $    editTodoForm todoText  divClass "p-2 btn-group" $ do    (doneEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Finish edit"    let updTodos = \todo -> Endo $ update (Just . finishEdit todo) ix    tellEvent $      tagPromptlyDyn (updTodos <$> updTodoDyn) (domEvent Click doneEl)

Применение класса EventWriter упростило код и сделало его более читаемым.


ghcjs-dom


reflex позволяет нам только модифицировать DOM, но зачастую от JS-приложений требуется больше. Например, если требуется копировать текст по нажатию на кнопку, то reflex не предоставляет нужных нам для этого средств. На помощь приходит библиотека ghcjs-dom. По сути, это реализация JS API на Haskell. В ней можно найти все те же самые типы и функции, которые есть в JS.


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


function toClipboard(txt){  var inpEl = document.createElement("textarea");  document.body.appendChild(inpEl);  inpEl.value = txt  inpEl.focus();  inpEl.select();  document.execCommand('copy');  document.body.removeChild(inpEl);}

В привычном использовании мы вешаем этот обработчик, например, на кнопку.
Как это будет выглядеть на Haskell? В первую очередь, создадим новый модуль GHCJS для работы с ghcjs и определим соответствующую функцию.


{-# LANGUAGE OverloadedStrings #-}{-# LANGUAGE MonoLocalBinds #-}module GHCJS whereimport Control.Monadimport Data.Functor (($>))import Data.Text (Text)import GHCJS.DOMimport GHCJS.DOM.Document  (createElement, execCommand, getBodyUnchecked)import GHCJS.DOM.Element as Element hiding (scroll)import GHCJS.DOM.HTMLElement as HE (focus)import GHCJS.DOM.HTMLInputElement as HIE (select, setValue)import GHCJS.DOM.Node (appendChild, removeChild)import GHCJS.DOM.Types hiding (Event, Text)import Reflex.Dom as RtoClipboard :: MonadJSM m => Text -> m ()toClipboard txt = do  doc <- currentDocumentUnchecked  body <- getBodyUnchecked doc  inpEl <- uncheckedCastTo HTMLInputElement <$> createElement doc    ("textarea" :: Text)  void $ appendChild body inpEl  HE.focus inpEl  HIE.setValue inpEl txt  HIE.select inpEl  void $ execCommand doc ("copy" :: Text) False (Nothing :: Maybe Text)  void $ removeChild body inpEl

Почти каждой строке из haskell функции toClipboard есть соответствие из JS функции. Стоит отметить, что здесь нет привычного класса MonadWidget, а используется MonadJSM это та монада, в которой производятся вся работы с помощью ghcjs-dom. Класс MonadWidget наследует класс MonadJSM. Рассмотрим, как осуществляется привязка обработчика к событию:


copyByEvent :: MonadWidget t m => Text -> Event t () -> m ()copyByEvent txt ev =  void $ performEvent $ ev $> toClipboard txt

Здесь мы видим новую для нас функцию performEvent, и с помощью нее осуществляется привязка обработчика к событию. Она является методом класса PerformEvent:


class (Reflex t, Monad (Performable m), Monad m) => PerformEvent t m | m -> t where  type Performable m :: * -> *  performEvent :: Event t (Performable m a) -> m (Event t a)  performEvent_ :: Event t (Performable m ()) -> m ()

Теперь изменим виджет невыполненного задания, предварительно не забыв добавить импорт import GHCJS:


todoActive  :: (EventWriter t TodoEvent m, MonadWidget t m) => Int -> Todo -> m ()todoActive ix Todo{..} =  divClass "d-flex border-bottom" $ do    divClass "p-2 flex-grow-1 my-auto" $      text todoText    divClass "p-2 btn-group" $ do      (copyEl, _) <- elAttr' "button"        (  "class" =: "btn btn-outline-secondary"        <> "type" =: "button" ) $ text "Copy"      (doneEl, _) <- elAttr' "button"        (  "class" =: "btn btn-outline-secondary"        <> "type" =: "button" ) $ text "Done"      (editEl, _) <- elAttr' "button"        (  "class" =: "btn btn-outline-secondary"        <> "type" =: "button" ) $ text "Edit"      (delEl, _) <- elAttr' "button"        (  "class" =: "btn btn-outline-secondary"        <> "type" =: "button" ) $ text "Drop"      copyByEvent todoText $ domEvent Click copyEl      tellEvent $ leftmost        [ ToggleTodo ix <$ domEvent Click doneEl        , StartEditTodo ix <$ domEvent Click editEl        , DeleteTodo ix <$ domEvent Click delEl        ]

Была добавлена новая кнопка Copy и вызов определенной функции copyByEvent. Эти же самые действия можно проделать с виджетами для других состояний задания.


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


В следующей части рассмотрим использование JSFFI (JS Foreign Function Interface).

Подробнее..

Создаем веб-приложение на Haskell с использованием Reflex. Часть 4

18.06.2021 18:20:53 | Автор: admin

Часть 1.


Часть 2.


Часть 3.


Всем привет! В новой части мы рассмотрим использование JSFFI.


intro


JSFFI


Добавим в наше приложение возможность установки даты дедлайна. Допустим, требуется сделать не просто текстовый input, а чтобы это был выпадающий datepicker. Можно, конечно, написать свой datepicker на рефлексе, но ведь существует большое множество различных JS библиотек, которыми можно воспользоваться. Когда существует уже готовый код на JS, который, например, слишком большой, чтобы переписывать с использованием GHCJS, есть возможность подключить его с помощью JSFFI (JavaScript Foreign Function Interface). В нашем случае мы будем использовать flatpickr.


Создадим новый модуль JSFFI, сразу добавим его импорт в Main. Вставим в созданный файл следующий код:


{-# LANGUAGE MonoLocalBinds #-}module JSFFI whereimport Control.Monad.IO.Classimport Reflex.Domforeign import javascript unsafe  "(function() { \  \ flatpickr($1, { \  \   enableTime: false, \  \   dateFormat: \"Y-m-d\" \  \  }); \  \})()"  addDatePicker_js :: RawInputElement GhcjsDomSpace -> IO ()addDatePicker :: MonadWidget t m => InputElement er GhcjsDomSpace t -> m ()addDatePicker = liftIO . addDatePicker_js . _inputElement_raw

Так же не забудем добавить в элемент head необходимые скрипт и стили:


  elAttr "link"    (  "rel" =: "stylesheet"    <> "href" =: "https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" )    blank  elAttr "script"    (  "src" =: "https://cdn.jsdelivr.net/npm/flatpickr")    blank

Пробуем скомпилировать, так же как и раньше, и получаем следующую ошибку:


src/JSFFI.hs:(9,1)-(16,60): error:     The `javascript' calling convention is unsupported on this platform     When checking declaration:        foreign import javascript unsafe "(function() {    flatpickr($1, {      enableTime: false,      dateFormat: \"Y-m-d\"    });   })()" addDatePicker_js          :: RawInputElement GhcjsDomSpace -> IO ()  |9 | foreign import javascript unsafe  |

Действительно, сейчас мы собираем наше приложение с помощью GHC, который понятия не имеет, что такое JSFFI. Напомним, что сейчас запускается сервер, который с помощью вебсокетов отправляет обновленный DOM, когда требуется, и код на JavaScript для него чужд. Здесь напрашивается вывод, что использовать наш datepicker при сборке с помощью GHC не получится. Тем не менее, в продакшене GHC для клиента не будет использоваться, мы будем компилировать в JS при помощи GHCJS, и полученный JS встраивать уже в нашу страницу. ghcid не поддерживает GHCJS поэтому смысла грузиться в nix shell нет, мы будем использовать nix сразу для сборки:


nix-build . -A ghcjs.todo-client -o todo-client-bin

В корневой директории приложения появится директория todo-client-bin со следующей структурой:


todo-client-bin bin     todo-client-bin     todo-client-bin.jsexe         all.js         all.js.externs         index.html         lib.js         manifest.webapp         out.frefs.js         out.frefs.json         out.js         out.stats         rts.js         runmain.js

Открыв index.html в браузере, увидим наше приложение. Мы собрали проект с помощью GHCJS, но ведь для разработки все равно удобнее использовать GHC вместе с ghcid, поэтому модифицируем модуль JSFFI следующем образом:


{-# LANGUAGE CPP #-}{-# LANGUAGE MonoLocalBinds #-}module JSFFI whereimport Reflex.Dom#ifdef ghcjs_HOST_OSimport Control.Monad.IO.Classforeign import javascript unsafe  "(function() {\    flatpickr($1, {\      enableTime: false,\      dateFormat: \"Y-m-d\"\    }); \  })()"  addDatePicker_js :: RawInputElement GhcjsDomSpace -> IO ()addDatePicker :: MonadWidget t m => InputElement er GhcjsDomSpace t -> m ()addDatePicker = liftIO . addDatePicker_js . _inputElement_raw#elseaddDatePicker :: MonadWidget t m => InputElement er GhcjsDomSpace t -> m ()addDatePicker _ = pure ()#endif

Мы добавили условную компиляцию: в зависимости от платформы, либо будем использовать вызов JS функций, либо заглушку.


Теперь требуется изменить форму добавления нового задания, добавив туда поле выбора даты:


newTodoForm :: (EventWriter t (Endo Todos) m, MonadWidget t m) => m ()newTodoForm = rowWrapper $ el "form" $ divClass "input-group" $ mdo  iEl <- inputElement $ def    & initialAttributes .~      (  "type" =: "text"      <> "class" =: "form-control"      <> "placeholder" =: "Todo" )    & inputElementConfig_setValue .~ ("" <$ btnEv)  dEl <- inputElement $ def    & initialAttributes .~      (  "type" =: "text"      <> "class" =: "form-control"      <> "placeholder" =: "Deadline"      <> "style" =: "max-width: 150px" )  addDatePicker dEl  let    addNewTodo = \todo -> Endo $ \todos ->      insert (nextKey todos) (newTodo todo) todos    newTodoDyn = addNewTodo <$> value iEl    btnAttr = "class" =: "btn btn-outline-secondary"      <> "type" =: "button"  (btnEl, _) <- divClass "input-group-append" $    elAttr' "button" btnAttr $ text "Add new entry"  let btnEv = domEvent Click btnEl  tellEvent $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl

Скомпилируем наше приложение, попробуем его запустить, и мы все еще ничего не увидим. Если посмотрим в консоль разработчика в браузере, увидим следующую ошибку:


uncaught exception in Haskell main thread: ReferenceError: flatpickr is not definedrts.js:5902 ReferenceError: flatpickr is not defined    at out.js:43493    at h$$abX (out.js:43495)    at h$runThreadSlice (rts.js:6847)    at h$runThreadSliceCatch (rts.js:6814)    at h$mainLoop (rts.js:6809)    at rts.js:2190    at runIfPresent (rts.js:2204)    at onGlobalMessage (rts.js:2240)

Замечаем, что необходимая нам функция не определена. Так получается, потому что элемент script со ссылкой создается динамически, равно как и вообще все элементы страницы. Поэтому, когда мы используем вызов функции flatpickr, скрипт, содержащий библиотеку с этой функцией может быть еще не загружен. Надо явно расставить порядок загрузки.
Решим эту проблему при помощи пакета reflex-dom-contrib. Этот пакет содержит много полезных при разработке функций. Его подключение нетривиально. Дело в том, что на Hackage лежит устаревшая версия этого пакета, поэтому придется брать его напрямую c GitHub. Обновим default.nix следующим образом.


{ reflex-platform ? ((import <nixpkgs> {}).fetchFromGitHub {    owner = "reflex-frp";    repo = "reflex-platform";    rev = "efc6d923c633207d18bd4d8cae3e20110a377864";    sha256 = "121rmnkx8nwiy96ipfyyv6vrgysv0zpr2br46y70zf4d0y1h1lz5";    })}:(import reflex-platform {}).project ({ pkgs, ... }:let  reflexDomContribSrc = builtins.fetchGit {    url = "https://github.com/reflex-frp/reflex-dom-contrib.git";    rev = "11db20865fd275362be9ea099ef88ded425789e7";  };  override = self: pkg: with pkgs.haskell.lib;  doJailbreak (pkg.overrideAttrs  (old: {    buildInputs = old.buildInputs ++ [ self.doctest self.cabal-doctest ];  }));in {  useWarp = true;  overrides = self: super: with pkgs.haskell.lib; rec {    reflex-dom-contrib = dontHaddock (override self      (self.callCabal2nix "reflex-dom-contrib" reflexDomContribSrc { }));  };  packages = {    todo-common = ./todo-common;    todo-server = ./todo-server;    todo-client = ./todo-client;  };  shells = {    ghc = ["todo-common" "todo-server" "todo-client"];    ghcjs = ["todo-common" "todo-client"];  };})

Добавим импорт модуля import Reflex.Dom.Contrib.Widgets.ScriptDependent и внесем изменения в форму:


newTodoForm :: MonadWidget t m => m (Event t (Endo Todos))newTodoForm = rowWrapper $ el "form" $ divClass "input-group" $ mdo  iEl <- inputElement $ def    & initialAttributes .~      (  "type" =: "text"      <> "class" =: "form-control"      <> "placeholder" =: "Todo" )    & inputElementConfig_setValue .~ ("" <$ btnEv)  dEl <- inputElement $ def    & initialAttributes .~      (  "type" =: "text"      <> "class" =: "form-control"      <> "placeholder" =: "Deadline"      <> "style" =: "max-width: 150px" )  pb <- getPostBuild  widgetHoldUntilDefined "flatpickr"    (pb $> "https://cdn.jsdelivr.net/npm/flatpickr")    blank    (addDatePicker dEl)  let    addNewTodo = \todo -> Endo $ \todos ->      insert (nextKey todos) (newTodo todo) todos    newTodoDyn = addNewTodo <$> value iEl    btnAttr = "class" =: "btn btn-outline-secondary"      <> "type" =: "button"  (btnEl, _) <- divClass "input-group-append" $    elAttr' "button" btnAttr $ text "Add new entry"  let btnEv = domEvent Click btnEl  pure $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl

Мы воспользовались новой функцией widgetHoldUntilDefined, которая построит элемент, переданный ей в последнем параметре, только в тот момент, когда указанный скрипт будет загружен.
Теперь, если загрузим нашу страницу, полученную при помощи GHCJS, мы увидим используемый нами datepicker.


Но мы никак не задействовали это поле. Изменим тип Todo, не забыв добавить импорт Data.Time:


data Todo = Todo  { todoText     :: Text  , todoDeadline :: Day  , todoState    :: TodoState }  deriving (Generic, Eq, Show)newTodo :: Text -> Day -> TodonewTodo todoText todoDeadline = Todo {todoState = TodoActive False, ..}

Теперь изменим функцию с формой для нового задания:


...  today <- utctDay <$> liftIO getCurrentTime  let    dateStrDyn = value dEl    dateDyn = fromMaybe today . parseTimeM True      defaultTimeLocale "%Y-%m-%d" . unpack <$> dateStrDyn    addNewTodo = \todo date -> Endo $ \todos ->      insert (nextKey todos) (newTodo todo date) todos    newTodoDyn = addNewTodo <$> value iEl <*> dateDyn    btnAttr = "class" =: "btn btn-outline-secondary"      <> "type" =: "button"...

И добавим отображение даты в списке:


todoActive  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Int -> Text -> Day -> m ()todoActive ix todoText deadline = divClass "d-flex border-bottom" $ do  elClass "p" "p-2 flex-grow-1 my-auto" $ do    text todoText    elClass "span" "badge badge-secondary px-2" $      text $ pack $ formatTime defaultTimeLocale "%F" deadline  divClass "p-2 btn-group" $ do  ...

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


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

Подробнее..

Из песочницы Как скомпилировать декоратор C, Python и собственная реализация. Часть 2

29.06.2020 22:07:32 | Автор: admin

Декораторы одна из самых необычных особенностей Python. Это инструмент, который полноценно может существовать только в динамически типизированном, интерпретируемом языке. В первой части статьи мой товарищ Witcher136 показал, как в С++ реализовать наиболее приближенную к эталонной (питоновской) версию декораторов.


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



Оглавление


  1. Как работают декораторы в Python
  2. Haskell и LLVM собственный компилятор
  3. Так как же скомпилировать декоратор?


Как работают декораторы в Python


Перед погружением в алгоритм компиляции декораторов, поговорим про реализацию декораторов в питоне, и почему она не может быть в таком же виде воспроизведена в компилируемом языке. Сразу отмечу в данной статье под питоном понимается CPython. Все подкапотные детали относятся только к нему.


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


Как работают декораторы в Python, в общем-то интуитивно понятно любому человеку, знакомому с этим языком:


Функции decorator, принимающей другую функцию как свой аргумент func, в момент применения декоратора в качестве данного аргумента передается декорируемая функция old. Результатом является новая функция new и с этого момента она привязывается к имени old

def decorator(func):    def new(*args, **kwargs):        print('Hey!')        return func(*args, **kwargs)    return new@decoratordef old():    pass# old() выведет "Hey!" - к имени old теперь приязан вызов new

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


Про интерпретатор CPython

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


Благодаря этому, интерпретатору не надо знать о типах объектов, соответстующих символам в коде, вплоть до момент выполнения операций над ними когда очередь дойдет до выполнения какой-либо "конкретной" инструкции тогда он и проверит тип. Сильно упрощая можно объяснить это так: BINARY_SUBSTRACT (вычитание) упадет с TypeError, если дальше на стэке лежат число 1 и строка 'a'. В тоже время, выполнение STORE_FAST для одного и того же имени (запись в одну и ту же переменную), когда один раз на стэке лежит число, а в другой раз строка, не приведет к TypeError, т.к. в инструкцию по выполнению команды STORE_FAST не входит проверка типа только связывание имени с новым объектом.


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


Проблема 1. Декораторы это просто функции


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


name = input('Введите имя декоратора')def first(func):    ...  # тело декоратораdef second (func):    ...  # тело декоратораif name == 'first':    decorator = firstelif name == 'second':    decorator = secondelse:    decorator = lambda f: f   # оставляем функцию в покое@decorator def old():    pass

С точки зрения нашего умозрительного компилятора, значение функции old может поменяться на что угодно в процессе выполнения программы. В некоторых языках (например, C++) замыкания реализованы так, что даже при одинаковой сигнатуре они будут разного типа (из-за разницы в захваченном ими окружении), что не позволит провернуть такой трюк. В Python же каждое замыкание носит всё свое окружение с собой в питоне всё, включая функции, это объекты, так что замыкания "могут себе это позволить", тем более потребление памяти и быстродействие не являются приоритетом для этого языка.


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


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


Проблема 2. Python мало интересуют типы


def decorator(func):    def two_args(x, y):        ...    return two_args@decoratordef one_arg(x):    ...

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


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


Это также подводит нас к обратной проблеме какой тип должен быть у аргумента декоратора в наших примерах это аргумент с названием func? Чаще всего этот аргумент, представляющий декорируемую функцию, вызвается внутри замыкания значит нам хотелось бы знать хотя бы тип возвращаемого значения, не говоря уже об аргументах. Если мы его строго зафиксируем с помощью объявления func как функции типа A, мы ограничили область применения декоратора функциями типа A. Если же мы и это объявляем как void* func, и предлагаем программисту самому везде приводить нужные типы, то проще писать на питоне.


Один из вариантов выводить тип func автоматически, например с помощью шаблонов так и сделал Witcher136 в предыдущей статье. Однако это не решает проблему с разными сигнатурыми возвращаемых замыканий, а так же делает определения декораторов совершенно нечитаемыми для простых смертных (см. примеры на C++ в предыдущей статье).




Подведем итоги. Реализация декораторов в компилируемом языке создает следующие сложности:


  • Тип декорируемой функции какие аргументы принимает декоратор?
  • Тип итоговой функции какая сигнатура должна быть у результата работы декоратора?
  • Применение на этапе компиляции создает дополнительные ограничения, применение в рантайме уменьшает гарантии, которые компилятор может дать относительно результата (либо требует продвинутой системы типов)

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

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


Про этот компилятор я и расскажу далее.



Haskell и LLVM собственный компилятор


Для создания компилятора я выбрал Haskell, как язык для написания фронтенда, и LLVM в качестве компиляторного бэкенда. Для Haskell есть замечательная библиотека llvm-hs, предоставляющая все необходимые биндинги к LLVM. В поставку самого языка также входит библиотека Parsec, предназначенная для создания парсеров, путем комбинации различных парсер-функций этой библиотеки (я думаю, что на этом моменте читатель догадался, что Parsec сокращение от parser combinators).


Я опущу описание синтаксиса получившегося языка, который я окрестил Grit, и всех его тривиальных деталей (язык достаточн похож на Си, чтоб не требовать лишних пояснений, по крайней мере по части синтаксиса) остановимся подробно только на интересных его особенностях. Исходный код компилятора можно найти здесь.


Grit expression-oriented (фразированный) язык


Любое выражение в Grit, будь то сложение, блок кода или if-else, возвращает результат, который можно использовать внутри любого другого выражения например, присвоить переменной или передать функции как аргумент.


int main() = {    int i = 0;    i = i + if(someFunction() > 0) {        1;    }    else {        0;    };};

В данном примере, i будет равен 1, если функция someFunction вернет положительное значение, и нулю, если вернется 0 или меньше.


Нет ключевого слова return


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


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


Обратите внимание, что поскольку блок кода является выражением, то тело функции как бы "присваивается" ей же после объявления сигнатуры функции идет знак "равно", затем блок кода и в конце точка с запятой такой же синтаксис, как у обычной операции присваивания.


int simple(int x) = {    /*       Данная фукция вернет результат сложения      своего аргумента x и переменной y    */    int y = someOtherFunction();    x + y;};/*  Для функций, состоящих из одного выражения, фигурные скобки можно опустить.  Эта функция вернет свой аргумент, увеличенный на единицу*/int incr(int x) = x + 1;int main() returns statusCode {    /*      В объявлении функции с помощью ключевого слова returns      можно указать название переменной, значение которой      будет возвращено после окончания работы функции.      Это переменная будет "объявлена" автоматически      с таким же типом, как у возвращаемого значения функции    */    int statusCode = 0;    int result = someFunction();    if (someFunction < 0) {        statusCode = 1;    };};

Auto компилятор Grit обладает базовой возможностью выведения типов


В языке Grit есть ключевое слово auto, означающее, что тип переменной (или функции) должен быть выведен автоматически.


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


auto half (int x) = x / 2;   // для функции incr будет выведен тип float



Фразированность (expression-oriented), отсутствие return из произвольного места (тело функции тоже выражение) и базовое выведение типов это самые интересные для нас особенности Grit. Я выделил их потому, что они напрямую используются в реализации декораторов в этом языке.

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


А для нас теперь пришло время наконец ответить на главный вопрос этой серии статей как скомпилировать декоратор?



Так как же скомпилировать декоратор?


Как было указано ранее, при разработке декораторов для компилируемого языка программирования, нужно определиться с двумя вещами будут они применяться в runtime или compile-time, и как определять типы аргументов и результата итоговой функции.


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


Во-первых, декораторы в Grit применяются на этапе компиляции это позволяет, после перестройки синтаксического дерева (оно же AST, abstract syntax tree), вывести тип возвращаемого значения и скопировать объявления аргументов. Во-вторых, декораторы не являются функциями, а лишь похожими на них конструкциями.


Посмотрим на конкретный пример и разберемся, как данный способ объявления декораторов решает указнные в первой части статьи проблемы:


@auto flatten = {    auto result = @target;    if (result < 0) {        0;    }    else {         result;    };};

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


@auto flatten объявление декоратора с именем flatten и типом @auto то есть тип будет выведен в зависимости от функции, к которой он применен (@ указатель но то, что это объявление декоратора, а не функции).


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


Вы наверняка уже обратили внимание на необычную метку в теле декоратора @target. Это указание компилятору на то, что в это место надо вставить целиком тело декорируемой функции. Аргументы функции будут скопированы как аргументы для данного инстанса декоратора (что на уровне синтаксического дерева превращает его в новую функцию), а блок кода, являющийся телом исходной функции, вернет свой результат в место вызова, поскольку язык фразированный (этот механизм был описан ранее).
Таким образом, подобное изменение AST эквивалентно вызову исходной функции на месте @target, с правильными аргументами. После этого, исходную функцию можно просто заменить получившейся новой функцией, и все декоратор применен. Если же декораторов у функции несколько, то этот процесс можно повторять несколько раз.


Варианты использования декораторов

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


Например, можно ожидать захвата ресурса:


@auto lockFunction = {    mutex.lock();    @target};

Или вызывать функцию, только если установлен какой-либо флаг:


@auto optional = if (checkCondition()) {    @target;}else {    someDefaultValue;};

И так далее


Рассмотрим этот механизм на примере сгенерированного компилятором Grit синтаксического дерева для простой программы с декораторами:


@auto flatten = {    auto result = @target;    if (result < 0) {        0;    }    else {         result;    };};@flattenint incr(int x) = x+1;

Здесь уже рассмотренный нами декоратор flatten применяется к функции, увеличивающей свой аргумент на единицу.


Так выглядит "сырое" внутреннее представление, до какой-либо обработки вообще:


Decorator "flatten" auto {  BinaryOp = (Def auto "result") (DecoratorTarget)  If (BinaryOp < (Var "result") (Int 0)) {    Int 0  }  else {    Var "result"  }}Function "incr" int ; args [Def int "x"] ; modifiers [Decorator "flatten"] ; returns Nothing {  BinaryOp + (Var "x") (Int 1)}

Не вдаваясь в конкретные обозначения, сразу видно две вещи определение декоратора это самостоятельная конструкция с типом Decorator, и функция incr на данном этапе существует в своем исходном виде, и хранит информацию о том что к ней применен модификатор Decorator "flatten". В теле декоратора же мы видим метку DecoratorTarget сюда будет вставленно тело функции incr.


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


Посмотрим на аннотированное, полностью обработанное и готовое к кодогенерации предсавление той же программы:


Function (int -> int) incr ["x"] {  BinaryOp i= (Def int "result") (    Block int {      BinaryOp i+ (Var int "x") (int 1)    }  )  If int (BinaryOp b< (Var int "result") (int 0)) {    int 0  }  else {    Var int "result"  }}

Здесь мы можем заметить несколько важных вещей:


  • Определение декоратора было удалено после его применения на этапе обработки AST, он больше не нужен.
  • Тело функции incr изменилось теперь оно такое же, каким было тело декоратора flatten, но на месте DecoratorTarget теперь Block {...} выражение вида "блок кода", совпадающее с исходным телом функции. Внутри этого выражения есть обращения к аргументам функции, и оно возвращает то же значение, которое вернула бы исходная функция это значение присваивается новой переменной int "result", с которой декоратор и работает дальше. BinaryOp i= это операция присваивания int-а, но в исходном коде тип result был указан как auto значит тип возвращаемого значения и переменных в теле функции, работающих с ним, был выведен правильно.

Таким образом, в процессе разработки этого компилятора, удалось вывести общий алгоритм применения декораторов для компилируемого, статически типизированного языка. Он не обладает всеми возможностями декораторов в Python, но приближается к ним настолько, насколько это возможно в таком языке, как Grit.


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


@auto lockF(mutex M) {    M.lock();    @target;};@lockF(мьютексКоторыйФункцияДолжнаЗахватить)int someFunction(...)

Это вполне сработало бы при текущем подходе самый простой вариант это при применении декоратора убрать аргумент mutex M, и в теле конкретного инстанса декоратора заменить обращения к этому аргументу обращениями к "мьютексКоторыйФункцияДолжнаЗахватить", который должен существовать в области видимости декорируемой функции исходя из объявления (кстати, такой способ создавать декораторы с аргументами выглядит гораздо привлекательнее того, как они реализованы в Python замыкание внутри замыкания внутри функции).


Кроме того, я экспереминтировал с меткой @args, дающей внутри декоратора доступ к аргументам целевой функции, и так же разварачивающейся в "обычный код" на этапе обработки синтаксического дерева. Например, @args.length количество аргументов, @args.1 ссылка на первый аргумент и так далее. Что-то из этого работает, что-то пока не совсем но принципиально сделать это возможно.


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


Код получившегося компилятора можно найти здесь, собирается он с помощью stack


P.S. Это был очень интересный и необычный для меня опыт надеюсь, что и вы смогли вынести для себя что-нибудь полезное из этого рассказа. Если нужна отдельная статья про написание компилятора на Haskell на основе LLVM пишите в комментарии.
На любые вопросы постараюсь ответить в комментариях, или в телеграмме @nu11_pointer_exception

Подробнее..

Перевозим волка, козу и капусту через реку с эффектами на Haskell

02.08.2020 16:06:23 | Автор: admin
Однажды крестьянину понадобилось перевезти через реку волка, козу и капусту. У крестьянина есть лодка, в которой может поместиться, кроме самого крестьянина, только один объект или волк, или коза, или капуста. Если крестьянин оставит без присмотра волка с козой, то волк съест козу; если крестьянин оставит без присмотра козу с капустой, коза съест капусту.




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



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

data Direction = Back | Forwardroute :: [Direction]route = iterate alter Forwardalter :: Direction -> Directionalter Back = Forwardalter Forward = Back


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

data Character = Wolf | Goat | Cabbage deriving Eqclass Survivable a wheresurvive :: a -> a -> Orderinginstance Survivable Character wheresurvive Wolf Goat = GTsurvive Goat Wolf = LTsurvive Goat Cabbage = GTsurvive Cabbage Goat = LTsurvive _ _ = EQ


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

  • Чтобы найти решение, при котором все персонажи будут перевезены на противоположный берег, надо перебрать много вариантов перестановок. Для этого мы будем использовать эффект множественности, которого можно добиться с помощью обычного списка.
  • Еще нам нужно запоминать местоположение персонажа, чтобы проверять условия совместимости с другими персонажами (волк ест козу, коза ест капусту) и кого можно посадить на лодку. Мы можем хранить состав двух берегов type River a = ([a],[a]) c помощью эффекта состояния State (River a).
  • Лодка может взять кого-нибудь на борт, а может и не брать тут нам пригодится эффект частичности с Maybe.


В коде я буду использовать свою экспериментальную библиотеку joint (на Хабре есть две статьи, объясняющие ее суть первая и вторая), но при желании решение можно перенести на transformers или mtl.

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

  • В аппликативной/монадной цепочке вычислений для Maybe, если мы где-то получили Nothing, то и результат всего вычислений будет Nothing. Мы оставим его отдельно, так как не хотим, чтобы при отправлении пустой лодки (без персонажа, крестьянина мы не учитываем) мы потеряли весь прогресс в нахождении решения.
  • Каждый последующий выбор хода (эффект множественности) должен опираться на состав текущего берега (эффект состояния), так как мы не можем взять персонажа в лодку, если она находится на другом берегу. Следовательно, нам нужно эти эффекты сцепить в трансформер: State (River a) :> [].


Один ход в головоломке можно описать как последовательность действий:

  1. Получить состав персонажей на текущем берегу
  2. Выбрать следующего персонажа для транспортировки
  3. Переместить персонажа на противоположный берег


step direction = bank >>= next >>= transport


Давайте пройдемся по каждому шагу подробнее.

В зависимости от направления перемещения лодки, применяем линзу для источника отправления к состоянию всей реки и получаем состав текущего берега:

bank :: (Functor t, Stateful (River a) t) => t [a]bank = view (source direction) <$> current


Выбор следующего персонажа происходит так: получая набор персонажей с берега (предыдущее выражение bank), мы формируем пространство выбора, добавляя к этому самому пространству пустую лодку:

\xs -> Nothing : (Just <$> xs) 


Для каждого кандидата (пустая лодка (Nothing) тоже кандидат) проверяем чтобы на оставшемся берегу не оставалось персонажей, которые были бы не прочь полакомиться друг другом:

valid :: Maybe a -> Boolvalid Nothing = and $ coexist <$> xs <*> xsvalid (Just x) = and $ coexist <$> delete x xs <*> delete x xscoexist :: Survivable a => a -> a -> Boolcoexist x y = survive x y == EQ


И когда мы отфильтровали пространство выбора персонажей, поднимаем эффект множественности и возвращаем каждый элемент из этого пространства выбора:

next :: (Survivable a, Iterable t) => [a] -> t (Maybe a)next xs = lift . filter valid $ Nothing : (Just <$> xs)


Остался последний шаг фактическая транспортировка c помощью линз: удаляем персонажа с берега отправки и добавляем к берегу назначения:

leave, land :: River a -> River aleave = source direction %~ delete xland = target direction %~ (x :)


Если в лодке был персонаж изменяем состояние реки, иначе ход был холостым:

transport :: (Eq a, Applicative t, Stateful (River a) t) => Maybe a -> t (Maybe a)transport (Just x) = modify @(River a) (leave . land) $> Just x wheretransport Nothing = pure Nothing


Было бы неплохо посмотреть на работу программы в действии. Для нахождения решения нам нужно как минимум совершить семь шагов по маршруту:

start :: River Characterstart = ([Goat, Wolf, Cabbage], [])solutions = run (traverse step $ take 7 route) start


И у нас есть два решения:



Полные исходник можно посмотреть здесь.
Подробнее..

Let vs where в OcamlHaskell

15.03.2021 22:22:10 | Автор: admin

Языки Ocaml и Haskell ведут родословную из языка ISWIM, описанного в знаменитой статье Питера Лендина "The next 700 programming languages". В ней автор, отталкиваясь от языка LISP, создаёт новый язык программирования и, в частности, вводит ключевые слова let, and и where, которые широко используются в языках семейства ML. Рано или поздно у всякого пытливого ума, занимающегося функциональным программированием возникает вопрос: почему в Ocaml не прижилось ключевое слово where, широко используемое в Haskell?

С моей точки зрения, это, в основном, обусловлено различиями в семантике этих языков, а именно императивно-энергичным характером Ocaml и чистотой-ленивостью вычислений в Haskell (которые непосредственно и жёстко связаны с impure/pure характерами этих языков).

Оба эти выражения, let и where, произошли от (let ...) из языка LISP, которое имеет два варианта этой особой формы: (let ...) и (let* ...). Вариант (let* ...) отличается тем, что все связывания в блоке происходят последовательно и могут зависеть друг от друга:

(let* ((x 3)
(y (+ x 2))
(z (+ x y 5)))
(* x z))

В некоторых диалектах Scheme объявления переменных могут быть автоматически переупорядочены интерпретатором, поэтому их становится необязательно писать в "правильном" порядке. Оба варианта связывания, let ... in и where соответствуют вот этому, "продвинутому" варианту (let* ...). При этом в Ocaml для разделения "параллельных связываний" используется ключевое слово and, а в Haskell они просто помещаются в один блок.

Если смотреть исключительно в суть вещей, видно, что выражения let ... in и where различаются в двух аспектах: место, где ставится связывание, и количество выражений в блоке.

Связывание имён до и после использования.

Первое принципиальное отличие - связывание let ... in ставится перед выражением, в котором используется связанное имя, а where употребляется после:

let x = 1 in
x + 1

z = x + 1
where x = 1

Таким образом, let ... in лучше описывает семантику последовательного выполнения программы Ocaml, с её энергичными и, в целом, императивными/псевдоимперативными вычислениями. Если связывание содержит побочные эффекты, например

let x = Printf.printf "Hello ";
"World!"
in
Printf.printf "%s" x

мы интуитивно будем ожидать последовательного выполнения программы сверху вниз. И это прекрасно сочетается с последовательным вычислением top-level выражений в Ocaml, которые обрабатываются именно сверху вниз, с первой директивы open до последней строчки в традиционном let () = ...

В то же время, связывание where прекрасно передаёт non-strict семантику языка Haskell, когда в качестве модели вычисления используется term/graph reduction. Фактически, мы используем блок связываний where как сноски, примечания:

main = putStrLn (x ++ y)
where x = "Hello "
y = "World!"
z = undefined

И программа читается естественным образом: мы хотим вывести x и y, которые даны ниже. А если сноска z не используется, так и читать её не надо - она ведь не упоминается в основном тексте.

А вот связывание x, y, z в блоке let ... in, который тоже поддерживается языком Haskell, будет выглядеть ненатурально - вроде z и читается глазом, но вычисляться ведь точно не будет. С другой стороны, внутри псевдоимперативного блока do, связывание let очень к месту.

Переиспользование имён или shadowing

Cвязывание let ... in, как в Ocaml, так и в Haskell, может употребляться несколько раз в одном и том же блоке. А where - лишь однократно на одном уровне вложенности:

let x = 1 in
let y = 1 in
x + y

z = x + y
where x = 1
y = 1

Это опять таки, играет на руку соответствующей модели выполнения, поощряя или, наоборот, запрещая переопределение имён, или, как ещё оно романтично называется "shadowing". В коде прикладных программ на Ocaml shadowing используется повсеместно, позволяя эмулировать присвоение и писать в псевдоимперативном стиле:

let x = 1 in
let x = x * 10 in
x * x

В результате, хотя программа выше и написана в функциональном стиле, мы можем читать её как императивную:

x := 1;
x := x * 10;
return x*x;

А в лагере Haskell, однократность where явно, "лингвистически", запрещает shadowing внутри одного блока, заставляя использовать многочисленные апострофы. Этот запрет shadowing великолепно сочетается с тем, что все top-level имена в модуле должны быть уникальными, ведь из-за non-strict порядка вычислений мы их не можем переопределять. А также с тем, что по семантике языка Haskell вычисление

x = x + 1

обязано зацикливаться.

Такое принципиально противоположное отношение к shadowing в Ocaml и Haskell косвенно, помимо традиционного для Ocaml псевдоимперативного стиля, вызвано отличием сложной системы модулей Ocaml и простыми модулями Haskell (backpack не взлетел, и к счастью - поиск значения очередного типа t из модуля M в коде на Ocaml так же утомителен как и отладка фабрики фабрик в ООП).

Поскольку у модулей Ocaml может быть несколько сигнатур, по-умолчанию, язык использует разные файлы для сигнатуры (.mli) и для кода самого модуля (.ml). Причём, опять таки, по-умолчанию, компилятор автоматически генерирует файл сигнатуры, экспортируя все top-level выражения модуля, написанные программистом. Из-за этого, в прикладном коде на Ocaml разработчики склонны минимизировать количество top-level выражений, скрывая все детали внутри них. То есть, писать функции по несколько страниц с большим количеством let ... in связываний (см., к примеру report_constructor_mismatch в файле https://github.com/ocaml/ocaml/blob/trunk/typing/includecore.ml#L212 )

В Haskell упрощённая система модулей совмещает сигнатуру и тело модуля, позволяя легко создавать список экспорта. А поэтому, в типичном случае для прикладного кода, когда из модуля нужно лишь одна-две функции, а остальное содержимое инкапсулировано, этот подход позволяет создавать большое количество top-level выражений малого размера. А значит, в каждом из этих выражений можно обойтись связыванием where без shadowing.

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

Заключение

Для полноты, необходимо упомянуть, что where лучше, чем let ... in поддерживает стиль программирования "сверху-вниз", поскольку с ним мы сначала пишем болванку результата, а уже потом заполняем пропущенные места. Но это, в общем, согласуется с тем, что Haskell лучше подходит для прототипирования, а у Ocaml проще предсказывать производительность.

Конечно, в хорошо написанном простом библиотечном коде на языке Ocaml, уровня Stdlib совсем бы не помешала директива where для того, чтобы подчёркивать особенности кода, написанного в чистом, функциональном стиле. Например, в функциях List.mapi и List.rev_map. Но, положа руку на сердце, большая часть текстов на Ocaml значительно хуже по качеству, и требует несоизмеримых усилий для того, чтобы понять - можно ли использовать интерпретацию в стиле graph rewriting или же стоит предерживаться традиционного псевдоимперативного понимания. Поэтому, программируя на Ocaml мы вполне можем обойтись без where, точно также, как для чистого функционального кода на Haskell мы почти не используем let ... in.

Таким образом, как хорошие инженерные произведения, языки Ocaml и Haskell создают синергию синтаксиса и семантики. Директивы связывания let и where играют свою роль, подчёркивая подчёркивая линейную псевдоимперативную и "ленивую" (graph reduction) модели выполнения. Они также прекрасно сочетаются с предпочитаемым стилем написания прикладных программ и соответствующей системой модулей.

Подробнее..

Змейка на Haskell с циклом Гамильтона

08.04.2021 20:08:42 | Автор: admin

После прохождения курса по Haskell решил закрепить знания первым проектом. Писать будем змейку для терминала. Чтобы придать игре уникальности, добавим бота, который сам будет проходить игру.

Проект написан на haskell-platform, Ubuntu 20.04.

GitHub проекта

Игровой цикл

Начнем с реализации игрового цикла. Змея может двигаться независимо от нажатия клавиш, следовательно нам понадобится два параллельных потока. Используем модуль Control.Concurrent. Ответвляемся от основного процесса при помощи forkIO и синхронизируем потоки через MVar. С каждой итерацией игрового цикла, tryInput будет содержать Maybe Char значение, в зависимости от ввода пользователя. Потоки при этом не блокируются и работают параллельно. Для настройки буферизации ввода воспользуемся System.IO - отключим ожидание EOL символа при вводе и уберем отображение пользовательского вывода. Интересно, что hSetBuffering stdin NoBuffering не работает для Windows консоли - getChar будет ждать EOL и запустить игру в форточках в текущем виде не получится. Также подключим System.Console.ANSI для очистки экрана и перемещения курсора терминала.

import Control.Concurrentimport System.Console.ANSIimport System.IOgameLoop :: ThreadId -> MVar Char -> IO ()gameLoop inputThId input = do  tryInput <- tryTakeMVar input  gameLoop inputThId input  inputLoop :: MVar Char -> IO ()inputLoop input = (putMVar input =<< getChar) >> inputLoop input main = do  hSetBuffering stdin NoBuffering   hSetEcho stdin False  clearScreen  input <- newEmptyMVar  inputThId <- forkIO $ inputLoop input  gameLoop inputThId input

Мир для змеи

Определим типы данных. У игры будет 4 состояния: Process - змеей управляет игрок, Bot - змеей рулит игра, GameOver и Quit. Мир игры определен типом data World, он будет каким-то образом меняться в игровом цикле gameLoop. Сейчас он содержит змею, направление ее движения, координату фрукта и текущее игровое состояние. Далее по мере разработки будет добавлять в него новые поля. Начальная точка (0,0) будет верхним левым краем консоли. Змея двигается параллельно осям, следовательно у нас 4 возможных направления движения.

data StepDirection  =  DirUp                    | DirDown                     | DirLeft                    | DirRight deriving (Eq)   type Point = (Int, Int)type Snake = [Point]data WorldState = Process                    | GameOver                   | Quit                    | Bot deriving (Eq)   data World = World  { snake :: Snake                    , direction :: StepDirection                    , fruit :: Point                    , worldState :: WorldState                    }        gameLoop :: ThreadId -> MVar Char -> World -> IO (){--  --}

Таймер

Для анимации движения змеи нам потребуются функции работы со временем. Воспользуемся модулем Data.Time.Clock. Добавим в наш мир 3 поля: lastUpdateTime - время последнего обновления мира, updateDelay - сколько ждем до следующего обновления и isUpdateIteration - флаг необходимости обновить мир в текущей итерации. Укажем начальные значения мира и напишем для него первый обработчик timerController. Он принимает текущее время и устанавливает флаг isUpdateIteration, если пришло время обновляться.

import Data.Time.Clockdata World  = World  {                              {--  --}                                  , lastUpdateTime :: UTCTime                                  , updateDelay :: NominalDiffTime                                  , isUpdateIteration :: Bool                                  }  initWorld :: UTCTime -> WorldinitWorld timePoint = World { snake = [(10, y) | y <- [3..10]]                             , direction = DirRight                            , fruit = (3, 2)                            , lastUpdateTime = timePoint                            , updateDelay = 0.3                            , isUpdateIteration = True                            , worldState = Process                            }  timerController :: UTCTime -> World -> WorldtimerController timePoint world  | isUpdateTime timePoint world = world { lastUpdateTime = timePoint                                         , isUpdateIteration = True                                          }  | otherwise                    = world where    isUpdateTime timePoint world =     diffUTCTime timePoint (lastUpdateTime world) >= updateDelay worldgameLoop inputThId input oldWorld = do {--  --}  timePoint <- getCurrentTime  let newWorld = timerController timePoint oldWorld  gameLoop inputThId input newWorld { isUpdateIteration = False }  main = do  {--  --}  timePoint <- getCurrentTime  gameLoop inputThId input (initWorld timePoint)

Контроллер ввода

Далее добавим обработчик ввода inputController. Клавиши WSAD меняют направление нашей змеи. Стоит обратить внимание, что змея не может двигаться назад, за исключением случая, когда она состоит из 1 сегмента. Поэтому если новое направление ведет ко второму от головы сегменту змеи, мы игнорируем такой ввод. Также если текущее направление совпадает с предыдущим, то есть пользователь зажал клавишу управления, ускорим движение змеи уменьшив updateDelay. Функция pointStep принимает направление и точку, возвращая новую точку, перемещенную на один шаг в заданном направлении.

pointStep :: StepDirection -> Point -> PointpointStep direction (x, y) = case direction of  DirUp -> (x, y - 1)   DirDown -> (x, y + 1)  DirLeft -> (x - 1, y)  DirRight -> (x + 1, y)inputController :: Maybe Char -> World -> WorldinputController command world = let  boost dir1 dir2 = if dir1 == dir2 then 0.05 else 0.3  filterSecondSegmentDir (x:[]) dirOld dirNew = dirNew  filterSecondSegmentDir (x:xs) dirOld dirNew | pointStep dirNew x == head xs = dirOld                                              | otherwise = dirNew in     case command of    Just 'w' -> world { direction = filterSecondSegmentDir (snake world) (direction world) DirUp                       , updateDelay = boost (direction world) DirUp                      , worldState = Process                      }    Just 's' -> world { direction = filterSecondSegmentDir (snake world) (direction world) DirDown                      , updateDelay = boost (direction world) DirDown                      , worldState = Process                      }    Just 'a' -> world { direction = filterSecondSegmentDir (snake world) (direction world) DirLeft                       , updateDelay = boost (direction world) DirLeft                      , worldState = Process                      }    Just 'd' -> world { direction = filterSecondSegmentDir (snake world) (direction world) DirRight                       , updateDelay = boost (direction world) DirRight                      , worldState = Process                      }    Just 'q' -> world { worldState = Quit }    Just 'h' -> world { worldState = Bot }    _        -> world { updateDelay =  0.3 }

Двигаем змею

Следующий контроллер moveController сдвинет нашу змею, если пришло время isUpdateIteration для обновления мира.

snakeStep :: StepDirection -> Snake ->  SnakesnakeStep direction snake = (pointStep direction $ head snake):(init snake)moveController :: World -> WorldmoveController world   | not $ isUpdateIteration world = world  | otherwise = world { snake = snakeStep (direction world) (snake world) }

Столкновения с препятствиями

Границы поля

Последний контроллер будет обрабатывать столкновения змеи с препятствиями. В текущий момент ими могут быть только сама змея и фрукт, поэтому добавим стены. Они статичны, поэтому не будет добавлять их в мир и объявим отдельно. Первый элемент пары (1,1) верхняя левая точка стены и (20,20) нижняя правая.

initWalls :: WallsinitWalls = ((1,1),(20,20))

ГСЧ

Фрукт появляется на поле в случайном месте, следовательно нам нужен ГСЧ. В Haskell он реализован в модуле System.Random, функция randomR. Так как мы работаем с чистыми функциями, которые возвращают при одинаковых аргументах одинаковый результат, вторым аргументом randomR служит генератор, который обновляется с каждым вызовом. Добавим его как поле нашего мира и зададим ему начальное значение. Когда змея ест фрукт, она растет в хвосте. Сохраним координату крайней точки хвоста при обновлении мира.

import System.Randomdata World = World  {                     {--  --}                    , oldLast :: Point                    , rand :: StdGen                    }    initWorld timePoint = World   {                              {--  --}                              , oldLast = (0, 0)                              , rand = mkStdGen 0                              } {--  --}timerController timePoint world  | isUpdateTime timePoint world  = world {                                           {--  --}                                          , oldLast = last $ snake world                                          }{--  --}                                       

Контроллер столкновений

Добавим функции проверки столкновений змеи с телом и головы со стеной.

collisionSnake :: Snake -> BoolcollisionSnake (x:xs) = any (== x)  xscollisionWall :: Point -> Walls -> BoolcollisionWall (sx,sy) ((wx1,wy1),(wx2,wy2)) = sx <= wx1 || sx >= wx2 || sy <= wy1 || sy >= wy2

Все готово для контроллера столкновений collisionController. Переводим состояние мира в GameOver при столкновении с препятствиями и увеличим длину змеи в хвосте при съедании фрукта, также сгенерив новый в пределах стен. Если координата нового фрукта является координатой тела змеи, пробуем новую координату.

collisionController :: World -> WorldcollisionController world  | not $ isUpdateIteration world                = world  | collisionSnake $ snake world                 = world { worldState = GameOver }   | collisionWall (head $ snake world) initWalls = world { worldState = GameOver }   | collisionFruit (snake world) (fruit world)   = world { snake =                                                            (snake world) ++ [oldLast world]                                                         , fruit = newFruit                                                         , rand = newRand                                                         }  | otherwise                                    = world where    collisionFruit snake fruit = fruit == head snake    (newFruit, newRand) = freeRandomPoint world (rand world)    randomPoint ((minX, minY), (maxX, maxY)) g = let      (x, g1) = randomR (minX + 1, maxX - 1) g      (y, g2) = randomR (minY + 1, maxY - 1) g1 in         ((x, y), g2)    freeRandomPoint world g | not $ elem point ((fruit world):(snake world)) =                                 (point, g1)                            | otherwise = freeRandomPoint world g1 where                                (point, g1) = randomPoint initWalls g

Графика

Перейдем к отображению мира. Базовая функция нашей графики drawPoint принимает символ и отображает его в заданной координате экрана. Функция renderWorld отображает наш мир. Без установленного флага isUpdateIteration, контроллеры moveController, collisionController и renderWorld не производят никаких изменений. Рендер отображает наш фрукт, новое положение змеи и затирает ее хвост. Стены отображаются один раз при старте и не обновляются.

renderWorld :: World -> IO ()renderWorld world  | not $ isUpdateIteration world = return ()  | otherwise = do    drawPoint '@' (fruit world)      drawPoint ' ' (oldLast world)    mapM_ (drawPoint 'O') (snake world)    setCursorPosition 0 0 drawPoint :: Char -> Point -> IO ()drawPoint char (x, y) = setCursorPosition y x >> putChar char   drawWalls :: Char -> Walls -> IO ()drawWalls char ((x1, y1),(x2, y2)) = do  mapM_ (drawPoint char) [(x1, y)| y <- [y1..y2]]  mapM_ (drawPoint char) [(x, y1)| x <- [x1..x2]]  mapM_ (drawPoint char) [(x2, y)| y <- [y1..y2]]   mapM_ (drawPoint char) [(x, y2)| x <- [x1..x2]]main = do  {--  --}  drawWalls '#' initWalls  {--  --}

Подключаем все контроллеры и добавляем рендер в игровом цикле.

gameLoop inputThId input oldWorld = do  {--  --}  let newWorld = collisionController . moveController $ timerController timePoint (inputController tryInput oldWorld)   renderWorld newWorld  {--  --}

На текущем этапе у нас есть рабочая змейка с контролем от пользователя. Добавим возможность игре проходить себя самостоятельно. Задачу идеального прохождения змейки очень подробно в своих видео разобрал австралийский кодер CodeBullet. Также об этом можно почитать у RussianDragon тут. Позаимствуем идею с циклом Гамильтона и приступим.

Цикл Гамильтона

Немного теории: цикл Гамильтона в контексте нашей игры - замкнутый путь, проходящий через каждую точку поля ровно один раз. Мы сможем его найти сделав проход по часовой стрелке. Добавим пару синонимов типов: Path - путь по игровому полю, ClosedPath - замкнутый путь или цикл Гамильтона для нашего поля.

type Path = [Point]type ClosedPath = [Point]

Напишем несколько вспомогательных функций, wallsFirstPoint вернет нулевую точку игрового поля внутри стен. С нее мы начнем составление цикла Гамильтона и соответственно в нее мы должны вернуться. isPathContain аналогично проверки столкновения змеи с телом, проверяет содержит ли путь точку. clockwise вернет список возможных направлений по часовой стрелке. distBetweenPoints - расстояние между двумя точками, учитывая что змея не может двигаться по диагонали.

clockwise = [DirUp, DirRight, DirDown, DirLeft]wallsFirstPoint :: PointwallsFirstPoint = ((fst $ fst initWalls) + 1, (snd $ fst initWalls) + 1)isPathContain :: Path -> Point -> BoolisPathContain path point = any (== point) pathdistBetweenPoints :: Point -> Point -> IntdistBetweenPoints (x1, y1) (x2, y2) = abs (x1 - x2) + abs (y1 - y2)

И сама функция поиска цикла Гамильтона getHamPath. Она принимает начальную точку цикла, второй аргумент является аккумулятором рекурсии, при вызове указываем пустой список. Функция проверяет, равна ли площадь нашего поля длине найденного пути Гамильтона и равно ли расстояние между первой и последней точкой пути единице, то есть пусть замкнулся и является циклом. Если нет, ищем следующую точку при помощи nextHamPathPoint. Даем ей текущий найденный путь и 4 возможных направления движения. Если точка не имеет коллизий со стеной и найденным путем, выбираем ее и включаем в путь. Вариант, что nextHamPathPoint не нашел ни одной точки крашит программу, так как цикл Гамильтона гарантированно должен быть найден. В нашем случае это может произойти только при условии, что у поля обе стороны четные и у пути нет возможности вернуться к начальной точке.

getHamPath :: Point -> ClosedPath -> ClosedPathgetHamPath currentPoint hamPath  | hamPathCapacity initWalls == length (currentPoint:hamPath)                                    && distBetweenPoints currentPoint (last hamPath) == 1                                      = currentPoint:hamPath                                 | otherwise = getHamPath newPoint (currentPoint:hamPath) where                                    newPoint = nextHamPathPoint (currentPoint:hamPath) clockwise                                    hamPathCapacity ((x1, y1),(x2, y2)) = (x2 - x1 - 1) * (y2 - y1 - 1) nextHamPathPoint :: Path -> [StepDirection] -> PointnextHamPathPoint _       []         = error "incorrect initWalls"nextHamPathPoint hamPath (dir:dirs) | isPathContain hamPath virtualPoint                                       || collisionWall virtualPoint initWalls =                                       nextHamPathPoint hamPath dirs                                     | otherwise = virtualPoint where  virtualPoint = pointStep dir (head hamPath)

Добавим найденный цикл Гамильтона в наш мир.

data World = World  {                     {--  --}                     , hamPath :: ClosedPath                    }    initWorld timePoint = World   {                              {--  --}                              , hamPath = getHamPath wallsFirstPoint []                              } 

Внутри замкнутого пути наша змея может двигаться в 2х направлениях. Несмотря на то, что путь зациклен, он представляет из себя список с головой и хвостом. Будем считать движение от головы к хвосту DirFromHead и DirFromTail в обратном направлении.

data PathDirection = DirFromHead | DirFromTail deriving (Eq)

Добавим к контроллеру движения змеи управление ботом при помощи функции nextDirOnPath, которую опишем позже. Она возвращает пару (botStepDir, botPathDir) первый элемент дает нам предложенное ботом направление змеи на поле. Второй указывает на движение внутри цикла Гамильтона. Если вернулось DirFromHead, то есть обратное текущему, переворачиваем цикл.

moveController world   {--  --}  | worldState world == Process = world {snake = snakeStep (direction world) (snake world)}  | otherwise                   = world { snake = snakeStep botStepDir (snake world)                                        , hamPath = if botPathDir == DirFromTail then hamPath world else reverse $ hamPath world                                          } where                                            (botStepDir, botPathDir) = nextDirOnPath (snake world) (hamPath world) nextDirOnPath :: Snake -> ClosedPath -> (StepDirection, PathDirection)nextDirOnPath = undefined

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

dirBetweenPoints :: Point -> Point -> StepDirectiondirBetweenPoints (x1, y1) (x2, y2)  | x1 == x2 = if y1 > y2 then DirUp else DirDown                                    | y1 == y2 = if x1 > x2 then DirLeft else DirRight                                     | otherwise = if abs (x1 - x2) < abs (y1 - y2) then                                         dirBetweenPoints (x1, 0) (x2, 0) else                                        dirBetweenPoints (0, y1) (0, y2)  pointNeighborsOnPath :: Point -> ClosedPath -> (Point, Point)pointNeighborsOnPath point path | not $ isPathContain path point || length path < 4 = error "incorrect initWalls"                                | point == head path = (last path, head $ tail path)                                | point == last path = (last $ init path, head path)                                | otherwise = _pointNeighborsOnPath point path where                                  _pointNeighborsOnPath point (a:b:c:xs) = if point == b then (a,c) else _pointNeighborsOnPath point (b:c:xs)

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

nextDirOnPath :: Snake -> ClosedPath -> (StepDirection, PathDirection)nextDirOnPath (snakeHead:snakeTail)  path   | snakeTail == [] = (dirBetweenPoints snakeHead point1, DirFromTail)                                             | point1 == head snakeTail = (dirBetweenPoints snakeHead point2, DirFromHead)                                            | otherwise = (dirBetweenPoints snakeHead point1, DirFromTail) where                                                 (point1, point2) = pointNeighborsOnPath snakeHead path

Сейчас у нас есть бот, который проходит змейку по найденному пути, не сокращая его.

Ускоряем бота

Попробуем ускорить бота, добавив еще пару функций: collisionSnakeOnPath проверит, свободен ли замкнутый путь начиная с точки в заданном направлении от тела змеи и distBetweenPointsOnPath которая вернет пару расстояний от точки до точки на замкнутом пути. Первый элемент будет расстоянием для DirFromTail направления, второй для DirFromHead.

collisionSnakeOnPath :: Snake -> Point -> ClosedPath -> PathDirection -> BoolcollisionSnakeOnPath snake point path pathDir | null $ common snake pathPart = False                                              | otherwise = True where  pathPart = takePathPart point (if pathDir == DirFromHead then path else reverse path) (length snake)  common xs ys = [ x | x <- xs , y <- ys, x == y]  takePathPart point path len = _takePathPart point (path ++ (take len path)) len where    _takePathPart _     []     _    = []    _takePathPart point (x:xs) len  | x == point = x:(take (len - 1) xs)                                    | otherwise = _takePathPart point xs lendistBetweenPointsOnPath :: Point -> Point -> ClosedPath -> (Int, Int)distBetweenPointsOnPath point1 point2 path  | point1 == point2 = (0, 0)                                            | id1 < id2 = (length path - id2 + id1,id2 - id1)                                            | otherwise = (id1 - id2, length path - id1 + id2) where  (id1,id2) = pointIndexOnPath (point1,point2) path 0 (0,0)  pointIndexOnPath _               []     _   ids                     = ids  pointIndexOnPath (point1,point2) (x:xs) acc (id1,id2) | x == point1 = pointIndexOnPath (point1,point2) xs (acc+1) (acc,id2)                                                        | x == point2 = pointIndexOnPath (point1,point2) xs (acc+1) (id1,acc)                                                         | otherwise   = pointIndexOnPath (point1,point2) xs (acc+1) (id1,id2)  

Сведем все в новую функцию управления змеей. Находим точку для сокращения цикла Гамильтона enterPointBypass в направлении фрукта, ищем самый короткий путь до фрукта в прямом и обратном направлении и проверяем ляжет ли туда змея. Если ничего не нашли, двигаемся дальше по циклу через nextDirOnPath.

nextDirBot :: Snake -> Point -> ClosedPath -> (StepDirection, PathDirection)nextDirBot snake fruit path | distBypass1 < distBypass2 && distBypass1 < distToFruit1                               && not (collisionSnakeOnPath snake enterPointBypass path DirFromTail)                                 = (dirBetweenPoints (head snake) enterPointBypass, DirFromTail)                            | distBypass2 < distToFruit1                               && not (collisionSnakeOnPath snake enterPointBypass path DirFromHead)                                 = (dirBetweenPoints (head snake) enterPointBypass, DirFromHead)                             | otherwise = nextDirOnPath snake path where  dirBypass = dirBetweenPoints (head snake) fruit  enterPointBypass = pointStep dirBypass (head snake)  (distBypass1, distBypass2) = distBetweenPointsOnPath enterPointBypass fruit path  (distToFruit1, _) = distBetweenPointsOnPath (head snake) fruit path

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

Подключим наш nextDirBot к контроллеру движения змеей, добавим меню и смотрим на результат.

Подробнее..

Заберите свои скобки

09.06.2021 10:16:43 | Автор: admin

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

В большинстве современных языков программирования скобки используются в качестве оператора применения функции к ее аргументам:

q (x, y z)

Но вHaskellоператор применения функции - это обычный пробел:

q :: a -> b -> c -> dx :: ay :: bz :: yq x y z

Подождите-ка, мы применяем функциюfк аргументам, а пробелов намного больше! Значит ли это, что у нас тут несколько применений функции к аргументам? Да:

(((q x) y) z)

Мы получаем несколько применений функции в каррированной форме:

q x :: b -> c -> dq x y :: c -> dq x y z :: d

Значит ли это, что мы можем отказаться от скобок? Нет, потому что в качестве аргументов нам могут попадаться применения функций, нам может быть просто лень придумывать новое имя для выражения, которое будет лишь промежуточным шагом:

q:: a -> b -> c -> dp:: b -> cq x y (p y)

И вот тут без скобочек нам не обойтись, потому что иначе проверка типов будет считать, что мы подаем на вход функцииfдва разных аргумента:

q :: a -> b -> (b -> c) -> b -> ???q x y p y

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

q:: a -> b -> c -> dp:: b -> cq x y $ p y

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

Одной функции никогда не бывает достаточно, и мы хотим применить другую, но уже вкусив код без скобочек, хочется просто написать:

($) :: a -> (a -> b) -> bo :: d -> eo (q x y $ p y) === o $ q x y $ p y

Неплохо выглядит! Но что, если вместо того, чтобы явно подавать аргументы для их последующего скармливания функциям, мы будем соединять функции между собой без какого-либо упоминания аргументов? Вы, наверное, уже видели эту диаграмму, когда читали статьи о функциональном программировании:

"What is a Category? Definition and Examples" (c) Math3ma

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

f :: a -> bg :: b -> cg . f :: a -> c(.) :: (b -> c) -> (a -> b) -> (a -> c)g (f x) === g . f

Зачастую вопрос о том, использовать ли композицию или применение - стилистический, так как эти два представления приводят к одному и тому же результату:

g . f === g $ f x

Если у нас в выражении есть где-то свободная переменная, мы можем применить ее с помощью одного оператора применения, а дальше использовать композицию (я в большинстве случаев именно так и делаю). Да и код читается намного лучше:

j $ h $ f $ g $ x === j . h . f . g $ x

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

f :: a -> b -> cg :: a -> b -> c -> dh :: c -> d -> eh (f x y) (g x y z)

Можно убрать лишь скобки справа:

h (f x y) (g x y z) === h (f x y) $ g x y z

Как убрать скобки слева? Чтобы понять, как подступиться к этой проблеме, давайте разберем композицию (.) и применение ($). Оба эти оператора - правоассоциативные. Ассоциативность - это про скобки. Ассоциативность справа - значит скобки группируются справа.

f . g . h === f $ g $ h x === f (g (h x))

Так как в Haskell функции уже в каррированной форме, мы не обязаны подавать все аргументы сразу - мы можем подавать их по одному.

f :: a -> b -> c -> df x :: b -> c -> df x y :: c -> df x y z :: d

Выходит, если мы хотим придумать такой оператор, который мог бы принимать несколько операндов, нам надо сделать его левоасcоциативным, чтобы он мог принимать операнды по-одному справа налево:

(???) :: (a -> b -> c -> ...) -> a -> b -> c -> ...((??? x) y) z) ...

Мы можем взять какую-нибудь часто используемую функцию, чтобы на ней протестировать наш новый оператор. Например, обработчик опциональной ошибки:

maybe :: b -> (a -> b) -> Maybe a -> b

Давайте выберем для нашего нового оператора какой-нибудь ASCII-символ, который еще вроде никак не используется в базовой библиотеке.

(#) :: (a -> b -> c -> ...) -> a -> b -> c -> ...f # x y z ... = ???

Кроме ассоциативности, для оператора нужно выбрать старшинство - это номер от 0 до 9, который определяет приоритет операторов. Чем выше номер, тем ниже приоритет. Например:

infixr 9 .infixr 0 $

Именно поэтому мы группируем скобки вокруг$, а не.:

h . g . f $ x === (h . (g . f)) $ (x)

В общем, для нашего нового оператора мы вольны выбрать любое число между 0 и 9. Давайте выберем что-нибудь среднее - 5.

infixl 5 #

Но как нам его написать? На самом деле, это тоже оператор применения, только сфокусированный на функции, а не на аргументах:

f # x = f x

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

maybe :: b -> (a -> b) -> Maybe a -> bmaybe x :: (a -> b) -> Maybe a -> bmaybe x f :: Maybe a -> bmaybe x f ma :: b

Каррирование... отлично, значит, мы можем группировать скобки слева!

maybe # x # f # ma === ((maybe # x) # f) # mamaybe # "undefined" # show . even # Just 1 === "False"maybe # "undefined" # show . even # Just 2 === "True"maybe # "undefined" # show . even # Nothing === "undefined"

Если применяемые аргументы достаточно большие и нет особой надобности в том, чтобы придумывать им имена, мы можем выстроить их в такой блок с отступом:

string_or_int :: Either String Inteither :: (a -> c) -> (b -> c) -> Either a b -> ceither  # print . ("String: " <>)  # print . ("Int: " <>) . show # string_or_int

Я пока что нигде еще не видел такого оператора, но мне бы он сильно понадобился там, где не справляются$и.. Напишите в комментариях, если подобное уже где-то существует, но по какой-то причине отсутсвует вbase.

Подробнее..

Как мы строили параллельные вселенные для нашего (и вашего) CICD пайплайна в Octopod

08.02.2021 18:21:29 | Автор: admin

Как мы строили параллельные вселенные для нашего (и вашего) CI/CD пайплайна в Octopod



Привет, Хабр! Меня зовут Денис и я вам расскажу как нас надоумило сделать техническое решение для оптимизации процесса разработки и QA у себя в Typeable. Началось с общего ощущения, что вроде делаем все правильно, но все равно можно было бы двигаться быстрее и эффективнее принимать новые задачки, тестировать, меньше синхронизироваться. Это все нас привело к дискуссиям и экспериментам, результатом которых стало решение, которое я опишу ниже.

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

  • Production основное рабочее окружение, куда попадают пользователи системы.
  • Pre-Production окружение для тестирования релиз-кандидатов (версий, которые будут использованы в production, если пройдут все этапы тестирования; их также называют RC), максимально схожее с production, где используются production доступы для интеграции с внешними сервисами. Цель тестирования на Pre-production получить достаточную уверенность в том, что на Production проблем не будет.
  • Staging окружение для черновой проверки, как правило тестирование последних изменений, по возможности использует тестовые интеграции со сторонними системами, может отличаться от Production, используется для проверки правильности реализации новых функциональностей.

Что и как нам хотелось улучшить


C Pre-production все достаточно понятно: туда последовательно попадают релиз-кандидаты, история релизов такая же, как на Production. Со Staging же есть нюансы:

  1. ОРГАНИЗАЦИОННЕ. Тестирование критических частей может потребовать задержки публикации новых изменений; изменения могут взаимодействовать непредсказуемым образом; отслеживание ошибок становится трудным из-за большого количества активности на сервере; иногда возникает путаница, что в какой версии реализовано; бывает непонятно, какое из накопившихся изменений вызвало проблему.
  2. СЛОЖНОСТЬ УПРАВЛЕНИЯ ОКРУЖЕНИЯМИ. Нужны разные окружения для тестирования разных изменений: для одной внешней системы может потребоваться production доступ, а работать с другой нужно в тестовой среде вместо боевой. А если staging один, то эти настройки распространяются на все реализованные фичи до следующего деплоймента. Приходится всё время об этом помнить и предостерегать сотрудников. Ситуация в целом похожа на работу многопоточного приложения с единым разделяемым ресурсом: он блокируется одними потребителями, остальные ждут. Например, один QA инженер ждет возможности проверки платежного шлюза с production интеграцией, пока другой проверяет все на интеграции тестовой.
  3. ДЕФЕКТ. Критичный дефект может заблокировать тестирование всех новых изменений разом

  4. ИЗМЕНЕНИЯ СХЕМ БД. Тяжело управлять изменениями схемы баз данных на одном стенде в периоды её активной разработки. Откати туда-сюда. Упс, тут не ревертится. А тут отревертили, но данные тестировщиков потеряли. Хочется для тестирования разных функциональностей иметь разные, изолированные друг от друга базы.
  5. УВЕЛИЧЕННОЕ ВРЕМЯ ПРИЁМКИ. Из-за того, что комбинация прошлых пунктов иногда приводит к ситуациям, когда часть фичей становится недоступна тестировщикам вовремя, или настройки окружения не позволяют приступить к тестированию сразу, происходят задержки на стороне разработки и тестирования. Допустим, в фиче обнаруживается дефект, и она возвращается на доработку разработчику, который уже вовсю занят другой задачей. Ему было бы удобнее получить её на доработку скорее, пока контекст задачи не потерян. Это приводит к увеличению отрезка времени от разработки до релиза в production для этих фич, что увеличивает так называемые time-to-production и time-to-market метрики.

Каждый из этих пунктов так или иначе решается, но все это привело к вопросу, а получится ли упростить себе жизнь, если мы уйдем от концепции одного staging-стенда к их динамическому количеству. Аналогично тому, как у нас есть проверки на CI для каждой ветки в git, мы можем получить и стенды для проверки QA для каждой ветки. Единственное, что нас останавливает от такого хода недостаток инфраструктуры и инструментов. Грубо говоря, для принятия фичи создается отдельный staging с выделенным доменным именем, QA тестирует его, принимает или возвращает на доработку. Примерно вот так:


Проблематика различных окружений при таком подходе решается естественным способом:


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

Наш путь к решению


Первое, что мы попробовали это прототип, собранный нашим DevOps: комбинация из docker-compose (оркестрация), rundeck (менеджмент) и portainer (интроспекция), которая позволила протестировать общее направление мысли и подход. С удобством были проблемы:

  1. Для любого изменения требовался доступ к коду и rundeck, которые были у разработчиков, но их не было, например, у QA инженеров.
  2. Поднято это было на одной большой машине, которой вскоре стало недостаточно, и для следующего шага уже был нужен Kubernetes или что-то аналогичное.
  3. Portainer давал информацию не о состоянии конкретного staging, а о наборе контейнеров.
  4. Приходилось постоянно мерджить файлик с описанием стейджингов, старые стенды надо было удалять.

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

  1. Использовать Kubernetes, чтобы масштабироваться на любое количество staging-окружений и иметь стандартный для современного DevOps набор инструментов.
  2. Решение, которое было бы просто интегрировать в инфраструктуру, уже использующую Kubernetes.
  3. Простой и удобный графический интерфейс для сотрудников на таких ролях как Руководители проектов, Product менеджеры и QA-инженеры. Они могут не работать с кодом напрямую, но инструменты для интроспекции и возможности задеплоить новый стейджинг у них должны быть. Результат не дергаем разработчиков на каждый чих.
  4. Решение, которое удобно интегрируется со стандартными CI/CD пайплайнами, чтобы его можно было использовать в разных проектах. Мы начали с проекта, который использует Github Actions как CI.
  5. Оркестрацию, детали которой сможет гибко настраивать DevOps инженер.
  6. Возможность скрывать логи и внутренние детали кластера в графическом интерфейсе, если проектная команда не хочет, чтобы они были доступны всем и/или есть какие-то опасения на этот счет.
  7. Полная информация и список действий должны быть доступны суперпользователям в лице DevOps инженеров и тимлидов.

И мы приступили к разработке Octopod. Названием послужило смешение нескольких мыслей про K8S, который мы использовали для оркестрации всего на проекте: множество проектов в этой экосистеме отражает морскую эстетику и тематику, а нам представлялся эдакий осьминог, щупальцами оркестрирующий множество подводных контейнеров. К тому же Pod один из основополагающих объектов в Kubernetes.

По техническому стеку Octopod представляет из себя Haskell, Rust, FRP, компиляцию в JS, Nix. Но вообще рассказ не об этом, поэтому я подробнее на этом останавливаться не буду.

Новая модель стала называться Multi-staging внутри нашей компании. Эксплуатация одновременно нескольких staging окружений сродни путешествиям по параллельным вселенным и измерениям в научной (и не очень) фантастике. В ней вселенные похожи друг на друга за исключением одной маленькой детали: где-то разные стороны победили в войне, где-то случилась культурная революция, где-то технологический прорыв. Предпосылка может быть и небольшой, но к каким изменениям она может привести! В наших же процессах эта предпосылка содержимое каждой отдельно взятой feature-ветки.


Внедрение у нас происходило в несколько этапов и начиналось с одного проекта. Оно включает в себя и подгонку оркестрации проекта со стороны DevOps и реорганизацию процесса тестирования и коммуникации со стороны руководителя проекта.

В результате ряда итераций некоторые фичи самого Octopod были удалены или изменены до неузнаваемости. Например, у нас в первой версии была страница с логом деплоя для каждого контура, но вот незадача не в каждой команде приемлемо то, что credentials могут через эти логи протекать ко всем сотрудникам, задействованным в разработке. В итоге мы решили избавиться от этой функциональности, а потом вернули её в другом виде теперь это настраиваемо (а поэтому опционально) и реализовано через интеграцию с kubernetes dashboard.

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

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

В результате


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

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


Для нас Octopod стал ответом на процессуальный вопрос, и я бы назвал текущее состояние однозначным успехом гибкости и удобства явно прибавилось. Есть еще не до конца решенные вопросы: авторизацию самого Octopod в кластере перетаскиваем на Atlassian oauth для нескольких проектов, и этот процесс затягивается. Впрочем, это не более чем вопрос времени, технически проблему уже удалось решить в первом приближении.

Open-source


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

Весь исходный код с примерами настройки и документацией доступен в репозитории на Github.
Подробнее..

Перевод Встраивание Haskell компиляторы и компиляция компиляторов

13.07.2020 00:19:13 | Автор: admin

Эта статья является переводом поста Chris Hodapp Embedding Haskell: Compilers, and compiling compilers В своём посте автор рассматривает различные подходы к использованию Haskell для написания кода для встраиваемых систем. Предоставим слово автору.


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


Этот блог посвящен использованию Haskell со встраиваемыми системами. Что это хотя бы значит? Мы видим пару широких категорий (которые отражают слайды на последней странице, а также наша страница ссылок):


  • Полная компиляция: компиляция кода на Haskell для встраиваемого назначения.
  • Ограниченная компиляция: компиляция некоторого ограниченного подмножества кода на Haskell для встраиваемого назначения.
  • Хостинг EDSL и компилятора: хостинг в Haskell, EDSL и компилятор для встраиваемого назначения.

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


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


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


Полная компиляция


Это то, что обычно приходит на ум, когда люди слышат об использовании Haskell со встраиваемыми системами компиляция кода на Haskell для запуска непосредственно во встраиваемой системе, в сочетании с обычной средой выполнения (плюс все, что требуется для начальной загрузки и поддержки). Раздел Compiling to Embedded Targets на странице ссылок будет особенно интересен в этом отношении.


Тем не менее, это на самом деле кажется довольно редким. Природа языка Haskell ставит некоторые сложные задачи. В частности, необходимо обеспечить соответствие среды выполнения Haskell целевой системе и заставить сборщик мусора и ленивые вычисления вести себя предсказуемым и вменяемым образом.


Ajhc, компилятор, производный от JHC, от Kiwamu Okabe из METASEPI, является единственным таким примером, который я обнаружил он может компилироваться и выполняться на ARM Cortex-M3 / M4. Kiwamu много писал о своем опыте работы с Haskell по этим следам. Его последующее переключение на язык ATS может быть подсказкой.


HaLVM от Galois, возможно, вписывается в эту категорию.


Ограниченная компиляция


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


GHC обеспечивает это, позволяя разработчикам вызывать функциональность GHC из Haskell в качестве библиотеки.


В разделе Compiling for FPGA/ASIC есть несколько примеров такого.


Хостинг EDSL и компилятора


Разделы Code Generation EDSLs и Circuit Design EDSLs на странице ссылок содержат множество примеров этого. Атом, тема нескольких моих предыдущих постов, находится в этой же категории.


Именно эту категорию мне чаще всего приходится объяснять. Обычно здесь используется EDSL (Embedded Domain-Specific Language, встроенный предметно-ориентированный язык) внутри Haskell, чтобы направить процесс генерации кода в представление более низкого уровня. Иначе это называется компиляция.


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


Здесь есть ограничения одного вида:


  • В основном все понятия времени выполнения во встраиваемой целевой среде должны поддерживаться отдельно. (Ivory работает над этим до сих пор, например с классом типов Num, в некоторых удивительных отношениях. Подробнее об этом будет рассказано в следующем посте!)
  • Это добавляет путаницу и усложнение другого этапа (возможно, нескольких этапов) к процессу связывания кода / спецификаций со встроенной средой. Вот почему я использую Shake.

Этот подход также даёт преимущество другого рода:


  • Любая среда Haskell, совместимая с рассматриваемыми библиотеками, должна давать те же результаты (насколько это касается встраиваемой среды). Её среда времени исполнения не имеет значения, не имеет значения и окружение, которое знает об архитектуре встроенной системы.
  • Такое разделение этапов также добавляет прекрасную возможность для статического анализа и оптимизации. Например, Copilot использует это для добавления интерпретатора / симулятора, SBV использует его для доказательства или опровержения заданных свойств кода, а Atom использует его для проверки некоторых временных ограничений.

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


Общность


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


Обдумайте следующее:


  • Нормальная программа на Haskell взаимодействует через то, что упорядочено в известной монаде ввода-вывода (в частности, значение, называемое main).
  • Спецификация Atom взаимодействует через то, что упорядочено в монаде Atom (в частности, любые значения, передаваемые компилятору Atom).
  • Программа на Ivory взаимодействует через то, что упорядочено в монадах Ivory eff
    и Module (в частности, какие бы значения ни передавались компилятору Ivory).
  • Описание CaSH взаимодействует через аппликативный Signal (в частности, значение, называемое topEntity).

Тенденция ясна? (Нет, это не монады. Сигнал только аппликативен, и я подозреваю, что Lava ведет себя аналогично.)


Этот список охватывает наши три категории. В каждой из них создается программа (в широком смысле), просто создавая значение в Haskell. Помимо этого, единственные реальные различия это:


  • тип этого значения,
  • какая система его обрабатывает (компилятор и среда исполнения Haskell, какой-то другой компилятор и, возможно, среда исполнения или их комбинация),
  • и возможный вывод (собственный двоичный файл, битовый код LLVM, код на C, код VHDL, код на ассемблере, вход в средство проверки модели и т.д.).

Игнорирование расплывчатой природы термина декларативный довольно напрямую связано с декларативным характером программ на Haskell.


С этой точки зрения, Haskell все еще компилируется для запуска в какой-то встраиваемой среде. При этом компиляция может продолжаться вне системного компилятора Haskell, и запуск может не использовать его среду исполнения.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru