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

Фп

Перевод Почему я считаю 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 предлагает очень хороший набор функций для разработки безопасного программного обеспечения.

Подробнее..

Мультивселенная и задачи о переправе

16.06.2021 04:11:19 | Автор: admin

Как-то прочел на Хабре статью Перевозим волка, козу и капусту через реку с эффектами на Haskell, которая так понравилась, что решил написать фреймворк для всего класса задач о переправах, используя мультипарадигменное проектирование. Наконец удалось найти время, и вот, спустя почти год, фреймворк готов. Теперь персонажи, их взаимодействия и описание искомого результата задаются через domain-specific language, который позволяет решать любые головоломки подобного рода с пошаговым выводом. Ниже приводится поэтапный разбор реализации DSL. Статья подойдет тем кто изучает язык Kotlin или просто интересуется примерами его использования. Некоторые малозначимые детали (вроде импортов и вывода) для кратости опущены.

Персонажа легко можно описать открытым для наследования классом:

open class Person(private val name: String)

Также просто определим понятие берега, как набора персонажей задачи:

typealias Riverside = Set<Person>

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

abstract class QuantumBoat(  val left: Riverside, val right: Riverside) {    abstract fun invert(): List<QuantumBoat>    fun where(    condition: Riverside.() -> Boolean,     selector: QuantumBoat.() -> Boolean  ) = Multiverse(this, condition).search(selector)}

Лодка также снабжена высокоуровневым методом where, для поиска необходимого состояния через N шагов по реке. Условие (condition) определяет валидность берегов в процессе, а селектор (selector) задает искомое конечное состояние. Обратите внимание, что при использовании этого метода лодка на самом деле не двигается с места, а перебирает альтернативные вселенные, пока не обнаружит подоходящую :)
Но об этом мы поговорим позже, а пока что перейдем к простой имплементации лодки для перемещения слева направо:

class LeftBoat(left: Riverside, right: Riverside) : QuantumBoat(left, right) {  override fun invert() =    left.map {      RightBoat(left - it - Farmer, right + it + Farmer)    } + RightBoat(left - Farmer, right + Farmer)}

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

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

typealias History = LinkedList<QuantumBoat>  fun Sequence<History>.fork() = sequence {  for (history in this@fork) {    for (forked in history.last.invert()) {      yield((history.clone() as History).apply {        add(forked)      })    }  }}

Заодно описали функцию форка мультиверсума (историй перемещений) в следующий набор состояний (шаг). Чтобы все это добро не забивало лишний раз память, используем ленивые последовательности и yield.

Теперь нам осталось всего лишь описать мультиверсум (а код для поиска состояний у нас уже есть):

/** * Мультиверсум для лодки * @param boat исходное состояние лодки * @param condition валидатор промежуточных состояний */class Multiverse(boat: QuantumBoat, val condition: Riverside.() -> Boolean) {  /**   * Все смоделированные истории передвижений лодки   */  private var multiverse = sequenceOf(historyOf(boat))  /**   * Найти историю подходящей нам лодки   * @param selector нужное состояние берегов и лодки   * @return все найденные варианты достижения состояния   */  tailrec fun search(selector: QuantumBoat.() -> Boolean): List<History> {    multiverse = multiverse.fork().distinct().filter {      it.last.left.condition()        && it.last.right.condition()    }    val results = multiverse.filter { it.last.selector() }.toList()    return when {      results.isNotEmpty() -> results      else -> search(selector)    }  }}

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

Наконец, пример использования DSL на всем известной задачке про волка, козу и капусту:

object Wolf : Person("")object Goat : Person("")object Cabbage : Person("")fun Riverside.rule() =  contains(Farmer) ||    (!contains(Wolf) || !contains(Goat)) &&    (!contains(Goat) || !contains(Cabbage))fun main() {  val property = setOf(Wolf, Goat, Cabbage)  // стартовали с левого берега  LeftBoat(property)     // отбросили все невалидные состояния    .where(Riverside::rule)    // выбрали из оставшихся те варианты,    // где все имущество оказалось на правом берегу    { right.containsAll(property) }     // выводим на экран пошаговое решение    .forEach(History::prettyPrint)}

Вот что получилось, вставляю скриншотом, потому что смайлики хабр не переваривает:

Всем удачного дня и побольше времени на написание собственных DSL :)

Исходный код здесь: demidko/Wolf-Goat-Cabbage
Приветствуется критика и предложения как сделать лучше.

Подробнее..

Создаем веб-приложение на 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. В этой части мы рассмотрели настройку окружения и приступили непосредственно к самой разработке приложения. В следующей части будет добавлена работа с элемента списка.

Подробнее..

Создаем веб-приложение на 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.

Подробнее..

Функциональное программирование в Python. Генераторы, как питонячий декларативный стиль

01.09.2020 22:17:56 | Автор: admin
  • Общее введение
  • ФП
    • Введение в ФП
    • Главные свойства функционального программирования
    • Основные термины
    • Встроенное ФП поведение в Python
    • Библиотека Xoltar Toolkit
    • Библиотека returns
    • Литература
  • Генераторы
    • Введение в итераторы
    • Введение в генераторы
    • Генераторы vs итераторы
    • Генераторы как пайплайн
    • Концепт yield from
    • Маршрутизация данных на генераторах (мультиплексирование, броадкастинг)
    • Пример трейсинга генератора
    • Стандартные инструменты генераторы
    • Выводы
      • Плюсы
      • Минусы
    • Литература
  • Итоги

Общее введение


Говоря о Python, обычно используется процедурный и ООП стиль программирования, однако это не значит, что другие стили невозможны. В презентации ниже мы рассмотрим ещё пару вариантов Функциональное программирование и программирование с помощью генераторов. Последние, в том числе, привели к появлению сопрограмм, которые позднее помогли создать асинхронность в Python. Сопрограммы и асинхронность выходят за рамки текущего доклада, поэтому, если интересно, можете ознакомиться об этом самостоятельно. Лично я рекомендую книгу "Fluent Python", в которой разговор начинается от итераторов, плавно переходит в темы о генераторах, сопрограммах и асинхронности.


ФП


Введение в ФП


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


Выделяют две крупные парадигмы программирования: императивная и декларативная.


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


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


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


В рамках процедурного подхода к программированию основное внимание сосредоточено на декомпозиции разбиении программы / задачи на отдельные блоки / подзадачи. Разработка ведётся пошагово, методом сверху вниз. Наиболее распространенным языком, который предполагает использование процедурного подхода к программирования является язык C, в нем, основными строительными блоками являются функции.


В рамках объектно-ориентированного (ООП) подхода программа представляется в виде совокупности объектов, каждый из которых является экземпляром определенного класса, классы образуют иерархию наследования. ООП базируется на следующих принципах: инкапсуляция, наследование, полиморфизм, абстракция. Примерами языков, которые позволяют вести разработку в этой парадигме являются C#, Java.


В рамках функционального программирования выполнение программы процесс вычисления, который трактуется как вычисление значений функций в математическом понимании последних (в отличие от функций как подпрограмм в процедурном программировании). Языки, которые реализуют эту парадигму Haskell, Lisp.


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


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


  • Функции являются объектами первого класса (First Class Object).
    Это означает, что с функциями вы можете работать, также как и с данными передавать их в качестве аргументов другим функциям, присваивать переменным и т.п.
  • Использование рекурсии в качестве основной структуры контроля потока управления. В некоторых языках не существует иной конструкции цикла, кроме рекурсии.
  • Акцент на обработке списков (lists, отсюда название Lisp LISt Processing). Списки с рекурсивным обходом подсписков часто используются в качестве замены циклов.
  • Используются функции высшего порядка (High Order Functions). Функции высшего порядка функции, которые могут в качестве аргументов принимать другие функции.
    Функции высшего порядка принимают в качестве аргументов другие функции. В стандартную библиотеку Python входит достаточно много таких функций, в качестве примера приведем функцию map. Она принимает функцию и Iterable объект, применяет функцию к каждому элементу Iterable объекта и возвращает Iterator объект, который итеративно возвращает все модифицированные после функции элементы.
  • Функции являются чистыми (Pure Functions) т.е. не имеют побочных эффектов (иногда говорят: не имеют сайд-эффектов).
    В Python это не выполняется. Необходимо самостоятельно следить за тем, чтобы функция была чистой.
  • Акцент на том, что должно быть вычислено, а не на том, как вычислять.

Основные термины


Не все термины ниже необходимы для понимания доклада, но необходимы для понимания ФП (спасибо одному другу за их подборку)


  • Ссылочная прозрачность.


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


  • Функции


    • Детерминированные.


      Детерминированная функция возвращает тот же результат для одних и тех же входных данных.


    • Чистые.


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


    • Тотальные.


      Тотальная функция возвращает вывод для каждого ввода.
      Тотальные функции всегда завершаются и никогда не вызывают исключений.



  • Композиция функций применение одной функции к результату другой


  • Сайд эффект.


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


  • Полиморфизм.


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


    • Параметрический полиморфизм.


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


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



  • Замыкание (closure)


    Замыкание процедура вместе с привязанной к ней совокупностью данных. Steve Majewski
    Замыкание функция, которая ссылается на свободные переменные в своей области видимости.



Встроенное ФП поведение в Python


Базовые элементы ФП в Python функции map(), reduce(), filter() и оператор lambda. В Python 1.x введена также функция apply(), удобная для прямого применения функции к списку, возвращаемому другой. Python 2.0 предоставляет для этого улучшенный синтаксис. Начиная с Python 2.3 считается устаревшей, удалена в Python 3.0


Несколько неожиданно, но этих функций и всего нескольких базовых операторов почти достаточно для написания любой программы на Python; в частности, все управляющие утверждения (if, elif, else, assert, try, except, finally, for, break, continue, while, def) можно представить в функциональном стиле, используя исключительно функции и операторы. Несмотря на то, что задача реального удаления всех команд управления потоком, возможно, полезна только для представления на конкурс "невразумительный Python" (с кодом, выглядящим как программа на Lisp'е), стоит уяснить, как ФП выражает управляющие структуры через вызовы функций и рекурсию.


В соответствии с вышесказанным, попробуем сделать несколько хаков, чтобы наш код был более ФПшный. Избавимся от if/elif/else в Python


# Normal statement-based flow controlif <cond1>:     func1() elif <cond2>:     func2() else:     func3()     # Equivalent "short circuit" expression(<cond1> and func1()) or (<cond2> and func2()) or (func3()) 

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


pr = lambda s:s namenum = lambda x: (x==1 and pr("one")) or (x==2 and pr("two")) or (pr("other"))assert namenum(1) == 'one' assert namenum(2) == 'two' assert namenum(3) == 'other'

Замена циклов на выражения так же проста, как и замена условных блоков. for может быть переписана с помощью map().


for e in lst:      func(e)      # statement-based loopmap(func,lst)    # map-based loop

То же самое мы можем сделать и с функциями.


do_it = lambda f: f()# let f1, f2, f3 (etc) be functions that perform actionsmap(do_it, [f1,f2,f3])

Перевести while впрямую немного сложнее, но вполне получается.


# statement-based while loopwhile <cond>:     <pre-suite>     if <break_condition>:         break    else:         <suite> # FP-style recursive while loopdef while_block():     <pre-suite>     if <break_condition>:         return 1     else:         <suite>         return 0 while_FP = lambda: (<cond> and while_block()) or while_FP() while_FP()

ФП вариант while все еще требует функцию while_block(), которая сама по себе может содержать не только выражения, но и утверждения (statements). Но мы могли бы продолжить дальнейшее исключение утверждений в этой функции (как, например, замену блока if/else в вышеописанном шаблоне).


К тому же, обычная проверка на месте (наподобие while myvar == 7) вряд ли окажется полезной, поскольку тело цикла (в представленном виде) не может изменить какие-либо переменные (хотя глобальные переменные могут быть изменены в while_block()). Один из способов применить более полезное условие заставить while_block() возвращать более осмысленное значение и сравнивать его с условием завершения.

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


# imperative version of "echo()"def echo_IMP():    while 1:         x = input("IMP -- ")         if x == 'quit':             break        else:            print(x) echo_IMP() # utility function for "identity with side-effect"def monadic_print(x):    print(x)     return x     # FP version of "echo()" echo_FP = lambda: monadic_print(input("FP -- ")) == 'quit' or echo_FP() echo_FP()

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


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


После всей проделанной работы по избавлению от совершенно осмысленных конструкций и замене их на невразумительные вложенные выражения, возникает естественный вопрос "Зачем?!". Перечитывая описания характеристик ФП, мы можем видеть, что все они достигнуты в Python. Но важнейшая (и, скорее всего, в наибольшей степени реально используемая) характеристика исключение побочных эффектов или, по крайней мере, ограничение их применения специальными областями наподобие монад. Огромный процент программных ошибок и главная проблема, требующая применения отладчиков, случается из-за того, что переменные получают неверные значения в процессе выполнения программы.


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


# Nested loop procedural style for finding big products xs = (1,2,3,4) ys = (10,15,3,22) bigmuls = [] # ...more stuff...for x in xs:     for y in ys:         # ...more stuff...        if x*y > 25:             bigmuls.append((x,y))         # ...more stuff...    # ...more stuff...    print(bigmuls)

Секции, комментированные как #...more stuff... места, где побочные эффекты с наибольшей вероятностью могут привести к ошибкам.


В любой из этих точек переменные xs, ys, bigmuls, x, y могут приобрести неожиданные значения в гипотетическом коде. Далее, после завершения этого куска кода все переменные могут иметь значения, которые могут ожидаться, а могут и не ожидаться посдедующим кодом.


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


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


bigmuls = lambda xs,ys: filter(lambda (x,y):x*y > 25, combine(xs,ys))combine = lambda xs,ys: map(None, xs*len(ys), dupelms(ys,len(xs)))dupelms = lambda lst,n: reduce(lambda s,t:s+t, map(lambda l,n=n: [l]*n, lst))print(bigmuls((1,2,3,4),(10,15,3,22)))

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


print([(x,y) for x in (1,2,3,4) for y in (10,15,3,22) if x*y > 25])

Да, всё верно, list, tuple, set, dict comprehensions и generator expressions изначально ФП техники, которые, как и многое другое, перекочевало в другие не-ФП языки


Библиотека Xoltar Toolkit


Сразу оговоримся, что библиотека достаточно старая и подходит лишь для Python 2, однако для ознакомления её достаточно. Библиотека Xoltar Toolkit Брина Келлера (Bryn Keller) покажет нам больше возможностей ФП.


Основные возможности ФП Келлер представил в виде небольшого эффективного модуля на чистом Python. Помимо модуля functional, в Xoltar Toolkit входит модуль lazy, поддерживающий структуры, вычисляемые "только когда это необходимо". Множество функциональных языков программирования поддерживают отложенное вычисление, поэтому эти компоненты Xoltar Toolkit предоставят вам многое из того, что вы можете найти в функциональном языке наподобие Haskell.


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


>>> car = lambda lst: lst[0] >>> cdr = lambda lst: lst[1:] >>> sum2 = lambda lst: car(lst)+car(cdr(lst)) >>> sum2(range(10))1 >>> car = lambda lst: lst[2] >>> sum2(range(10))5

К несчастью, одно и то же выражение sum2(range(10)) вычисляется к разным результатам в двух местах программы, несмотря на то, что аргументы выражении не являются изменяемыми переменными.


К счастью, модуль functional предоставляет класс Bindings, предотвращающий такое переприсваивание.


>>> from functional import * >>> let = Bindings() >>> let.car = lambda lst: lst[0] >>> let.car = lambda lst: lst[2] Traceback (innermost last):     File "<stdin>",         line 1, in ? File "d:\tools\functional.py",         line 976, in __setattr__ raise BindingError, "Binding '%s' cannot be modified." % name         functional.BindingError: Binding 'car' cannot be modified. >>> car(range(10)) 0

Разумеется, реальная программа должна перехватить и обработать исключение BindingError, однако сам факт его возбуждения позволяет избежать целого класса проблем.


Библиотека returns


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


from returns.maybe import Maybe, maybe@maybe  # decorator to convert existing Optional[int] to Maybe[int]def bad_function() -> Optional[int]:    ...    maybe_number: Maybe[float] = bad_function().map(    lambda number: number / 2,    )# => Maybe will return Some[float] only if there's a non-None value#    Otherwise, will return Nothing

Более реальный пример, из императивного стиля в декларативный можно переписать так:


# Imperative styleuser: Optional[User]discount_program: Optional['DiscountProgram'] = Noneif user is not None:     balance = user.get_balance()     if balance is not None:         credit = balance.credit_amount()         if credit is not None and credit > 0:            discount_program = choose_discount(credit)# same with returnsuser: Optional[User]# Type hint here is optional, it only helps the reader here:discount_program: Maybe['DiscountProgram'] = Maybe.from_value(    user,    ).map(  # This won't be called if `user is None`    lambda real_user: real_user.get_balance(),    ).map(  # This won't be called if `real_user.get_balance()` returns None    lambda balance: balance.credit_amount(),    ).map(  # And so on!    lambda credit: choose_discount(credit) if credit > 0 else None,    )

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


# Imperative styledef fetch_user_profile(user_id: int) -> 'UserProfile':    """Fetches UserProfile dict from foreign API."""    response = requests.get('/api/users/{0}'.format(user_id))    # What if we try to find user that does not exist?    # Or network will go down? Or the server will return 500?    # In this case the next line will fail with an exception.    # We need to handle all possible errors in this function    # and do not return corrupt data to consumers.    response.raise_for_status()    # What if we have received invalid JSON?    # Next line will raise an exception!    return response.json()

И то же самое, только с помощью returns


import requestsfrom returns.result import Result, safefrom returns.pipeline import flowfrom returns.pointfree import binddef fetch_user_profile(user_id: int) -> Result['UserProfile', Exception]:    """Fetches `UserProfile` TypedDict from foreign API."""    return flow(        user_id,        _make_request,        bind(_parse_json),    )@safedef _make_request(user_id: int) -> requests.Response:    response = requests.get('/api/users/{0}'.format(user_id))    response.raise_for_status()    return response@safedef _parse_json(response: requests.Response) -> 'UserProfile':    return response.json()

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


Вместо обычных значений возвращаются значения, заключенные в специальный контейнер, благодаря декоратору @safe. Он вернет Success [YourType] или Failure [Exception]. И никогда не бросит нам исключение!


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


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

Литература



Генераторы


Введение в итераторы


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


>>> for x in [1,4,5,10]:... print(x, end=' ')...1 4 5 10

Также, как мы знаем, итерация может происходить по многим типам объектов (не только спискам). Причина, почему мы можем итерироваться по объектам реализация специального протокола


>>> items = [1, 4, 5]>>> it = iter(items)>>> it.__next__()1>>> it.__next__()4>>> it.__next__()5>>> it.__next__()

Внутри под капотом обычной итерации.


for x in obj:    # statements

Происходит примерно следующее:


_iter = iter(obj) # Get iterator objectwhile 1:    try:        x = _iter.__next__() # Get next item    except StopIteration: # No more items        break    # statements

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


Например, посмотрим, как имплементировать такое:


>>> for x in Countdown(10):... print(x, end=' ')...10 9 8 7 6 5 4 3 2 1

Его реализация будет такова:


class Countdown(object):    def __init__(self,start):        self.start = start    def __iter__(self):        return CountdownIter(self.start)class CountdownIter(object):    def __init__(self, count):        self.count = count    def __next__(self):        if self.count <= 0:            raise StopIteration        r = self.count        self.count -= 1        return r

Введение в генераторы


Генератор функция, которая генерирует последовательность результатов вместо одного значения


def countdown(n):    while n > 0:        yield n        n -= 1

Вместо того, чтобы возвращать значение, мы создаём серию значений (с использованием оператора yield). Вызов функции генератора создает объект-генератор. Однако функция не запускается.


def countdown(n):    print("Counting down from", n)    while n > 0:        yield n        n -= 1>>> x = countdown(10)>>> x<generator object at 0x58490>>>>

Функция генератор выполняется только при вызове __next__().


>>> x = countdown(10)>>> x<generator object at 0x58490>>>> x.__next__()Counting down from 1010>>>

yield возвращает значение, но приостанавливает выполнение функции. Функция генератор возобновляется при следующем вызове __next__(). При завершении итератора возбуждается исключение StopIteration.


>>> x.__next__()9>>> x.__next__()8>>>...>>> x.__next__()1>>> x.__next__()Traceback (most recent call last):    File "<stdin>", line 1, in ?        StopIteration>>>

Небольшие выводы:


  • Функция генератор гораздо более удобный способ написания итератора
  • Вам не нужно беспокоиться об реализации протокола итератора (__next__, __iter__ и т. д.), т.к. при создании функции с yield магия Python уже добавляет в объект функции нужные методы.

>>> def x():...     return 1... >>> def y():...     yield 1... >>> [i for i in dir(y()) if i not in dir(x())]['__del__', '__iter__', '__name__', '__next__', '__qualname__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']

Помимо функции генератора возможно также использовать генераторное выражение, которое также возвращает объект генератора.


>>> a = [1,2,3,4]>>> b = (2*x for x in a)>>> b<generator object at 0x58760>>>> for i in b: print(b, end=' ')...2 4 6 8

Синтаксис генераторного выражения также прост


(expression for i in s if condition)# the same withfor i in s:    if condition:        yield expression

Генераторы vs итераторы


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


Генераторы как пайплайн


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


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


Каждая строка в логах выглядит примерно так:


81.107.39.38 - ... "GET /ply/ply.html HTTP/1.1" 200 97238


Число байтов находится в последней колонке:


bytes_sent = line.rsplit(None,1)[1]

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


81.107.39.38 - ... "GET /ply/ HTTP/1.1" 304 -


Сконвертируем полученный результат в число


if bytes_sent != '-':    bytes_sent = int(bytes_sent)

В итоге, может получиться примерно такой код


with open("access-log") as wwwlog:    total = 0    for line in wwwlog:        bytes_sent = line.rsplit(None,1)[1]        if bytes_sent != '-':            total += int(bytes_sent)    print("Total", total)

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


with open("access-log") as wwwlog:    bytecolumn = (line.rsplit(None,1)[1] for line in wwwlog)    bytes_sent = (int(x) for x in bytecolumn if x != '-')    print("Total", sum(bytes_sent))

Этот подход отличается от предыдущего, меньше строк, напоминает функциональный стиль Мы получили пайплайн


simple pipeline


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


Безусловно, этот генераторный подход имеет разновидности причудливой медленной магии. Файл из 1.3 ГБ код в старом стиле выполнил за 18.6 секунд, код на генераторах был выполнен за 16,7 секунд.


AWK с той же задачей справился гораздо медленее, за 70.5 секунд


awk '{ total += $NF } END { print total }' big-access-log

Небольшие выводы:


  • Это не только не медленный, но и на 10% быстрее
  • Меньше кода
  • Код относительно легко читать
  • И, честно говоря, мне он нравится в целом больше
  • Ни разу в нашем решении с генератором мы не создавали большие временные списки
  • Таким образом, это решение не только быстрее, но и может применяться к огромным файлам с данными
  • Подход конкурентоспособен с традиционными инструментами

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


Концепт yield from


'yield from' может использоваться для делегирования итерации


def countdown(n):    while n > 0:        yield n        n -= 1def countup(stop):    n = 1    while n < stop:        yield n        n += 1def up_and_down(n):    yield from countup(n)    yield from countdown(n)>>> for x in up_and_down(3):... print(x)...12321>>>

Также стоит упомянуть, что в более ранних версиях python (3.5 и ниже) yield from также использовался вместо await, пока await не стал новым ключевым зарезервированными словом, т.к. await по сути, просто передача контекста управления внутри другой корутины. Чтобы не возлагать много ответственности на yield from и было придумано отдельное зарезервированное слово await.


Маршрутизация данных на генераторах (мультиплексирование, броадкастинг)


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


multiplex broadcast pipeline


На данной диаграмме видно, что нам потребуется мультиплексирование (всё в одно) и броадкастинг (одно во всё). Что ж, не будем томить и напишем сразу решение.


# same with `tail -f`def follow(thefile):    thefile.seek(0, os.SEEK_END) # End-of-file    while True:        line = thefile.readline()        if not line:            time.sleep(0.1) # Sleep briefly            continue        yield linedef gen_cat(sources):    # В данном конкретном случае используется для распаковки вложенного списка в плоский    for src in sources:        yield from srcdef genfrom_queue(thequeue):    while True:        item = thequeue.get()        if item is StopIteration:            break        yield itemdef sendto_queue(source, thequeue):    for item in source:        thequeue.put(item)    thequeue.put(StopIteration)def multiplex(sources):    in_q = queue.Queue()    consumers = []    for src in sources:        thr = threading.Thread(target=sendto_queue, args=(src, in_q))        thr.start()        consumers.append(genfrom_queue(in_q))    return gen_cat(consumers)def broadcast(source, consumers):    for item in source:        for c in consumers:            c.send(item)class Consumer(object):    def send(self,item):        print(self, "got", item)if __name__ == '__main__':    c1 = Consumer()    c2 = Consumer()    c3 = Consumer()    log1 = follow(open("foo/access-log"))    log2 = follow(open("bar/access-log"))    log3 = follow(open("baz/access-log"))    lines = multiplex([log1, log2, log3])    broadcast(lines,[c1,c2,c3])

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


Пример трейсинга генератора


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


def trace(source):    for item in source:        print(item)        yield itemlines = follow(open("access-log"))log = trace(apache_log(lines))r404 = trace(r for r in log if r['status'] == 404)

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


Стандартные инструменты генераторы


Генераторы встречаются повсюду в стандартной библиотеке. Начиная с 3.0 их стараются внедрять повсеместно. Например, pathlib.Path.rglob, glob.iglob, os.walk, range, map, filter. Есть даже целиком библиотека на генераторах itertools.


Выводы


Плюсы:


  • Генераторы невероятно полезный инструмент для решения многообразных проблем
  • Сила исходит от способности настраивать пайплайны
  • Можно создавать компоненты, которые можно переиспользовать в разных пайплайнах
  • Небольшие компоненты, которые просто обрабатывают поток данных
  • Намного проще, чем это может быть сделано с помощью ООП шаблонов
  • Можно расширить идею пайплайнов во многих направлениях (сеть, потоки, корутины)

Минусы


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

Литература



Итоги


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


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


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


Если нашли ошибки, пишите в телеграмме Niccolum или на почту lastsal@mail.ru. Буду рад конструктивной критике.

Подробнее..

Пайплайны и частичное применения функций, зачем это в Python

04.09.2020 14:06:16 | Автор: admin


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


Кратко о ФП в Python и почему не хватает пайплайнов на примере


В Python из базовых средств есть довольно удобные map(), reduce(), filter(), лямбда-функции, итераторы и генераторы. Малознакомым с этим всем советую данную статью. В целом это оно всё позволяет быстро и естественно описывать преобразования над списками, кортежами, и тд. Очень часто(у меня и знакомых питонистов) то, что получается однострочник по сути набор последовательных преобразований, фильтраций, например:
Kata с CodeWars: Найти


$\forall n \in [a,b] : n=\sum_0^{len(n)} n_i ^ i, \text{ } n_i\text{ - i-й разряд числа n}$


Задачка довольно простая, к сожалению(но к счастью для этого поста), решений лучше чем в лоб нет.


Моё решение:


def sum_dig_pow(a, b): # range(a, b + 1) will be studied by the function    powered_sum = lambda x: sum([v**(i+1) for i,v in enumerate(map(lambda x: int(x), list(str(x))))])    return [i for i in range(a,b+1) if powered_sum(i)==i]

С использованием средств ФП как есть получается скобочный ад "изнутри наружу". Это мог бы исправить пайплайн.


Пайплайны функций


Под сим я подразумеваю такое в идеальном случае (оператор "|" личное предпочтение):


# f3(f2(f1(x)))f1 | f2 | f3 >> xpipeline = f1 | f2 | f3 pipeline(x)pipeline2 = f4 | f5pipeline3 = pipeline | pipeline2 | f6...

Тогда powered_sum может стать(код не рабочий):


powered_sum = str | list | map(lambda x: int(x), *args) | enumerate | [v**(i+1) for i,v in *args] | sum

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


from copy import deepcopyclass CreatePipeline:    def __init__(self, data=None):        self.stack = []        if data is not None:            self.args = data    def __or__(self, f):        new = deepcopy(self)        new.stack.append(f)        return new    def __rshift__(self, v):        new = deepcopy(self)        new.args = v        return new    def call_logic(self, *args):        for f in self.stack:            if type(args) is tuple:                args = f(*args)            else:                args = f(args)        return args    def __call__(self, *args):        if 'args' in self.__dict__:            return self.call_logic(self.args)        else:            return self.call_logic(*args)

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


pipe = CreatePipeline()powered_sum = pipe | str | list | (lambda l: map(lambda x: int(x), l)) | enumerate | (lambda e: [v**(i+1) for i,v in e]) | sum

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


Частичное применение функций


Рассмотрим на примере простейшей функции(код не рабочий):


def f_partitial (x,y,z):    return x+y+zv = f_partial(1,2)# type(v) = что-нибудь частично применённая функция f_partial, оставшиеся аргументы: ['z']print(v(3))# Эквивалентprint(f_partial(1,2,3))

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


powered_sum = pipe | str | list | map(lambda x: int(x)) | enumerate | (lambda e: [v**(i+1) for i,v in e]) | sum# map будет вызван ещё раз со вторым аргументом# map(lambda x: int(x))(данные) при вызове

map(lambda x: int(x)) в пайплайне выглядит более лаконично в целом и в терминах последовательных преобразований данных.
Кривенькая неполная реализация на уровне языка:


from inspect import getfullargspecfrom copy import deepcopyclass CreatePartFunction:    def __init__(self, f):        self.f = f        self.values = []    def __call__(self, *args):        args_f = getfullargspec(self.f)[0]        if len(args) + len(self.values) < len(args_f):            new = deepcopy(self)            new.values = new.values + list(args)            return new        elif len(self.values) + len(args) == len(args_f):            return self.f(*tuple(self.values + list(args)))

Реализация примера с учётом данного костыля дополнения:


# костыль для обхода поломки inspect над встроенным mapm = lambda f, l: map(f, l)# создаём частично применяемую функцию на основе обычной питоньейpmap = CreatePartFunction(m)powered_sum = pipe | str | list | pmap(lambda x: int(x)) | enumerate | (lambda e: [v**(i+1) for i,v in e]) | sum

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


def f (x,y,z):    return x+y+zf = CreatePartFunction(f)# работаетprint(f(1,2,3))# работаетprint(f(1,2)(3))print(f(1)(2,3))# не работает# 2(3) - int не callableprint(f(1)(2)(3))# работаетprint((f(1)(2))(3))

Итоги


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

Подробнее..

Категории

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

  • Имя: Макс
    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