-Поиск по дневнику

Поиск сообщений в rss_rss_hh_full

 -Подписка по e-mail

 

 -Постоянные читатели

 -Статистика

Статистика LiveInternet.ru: показано количество хитов и посетителей
Создан: 17.03.2011
Записей:
Комментариев:
Написано: 1

Habrahabr








Добавить любой RSS - источник (включая журнал LiveJournal) в свою ленту друзей вы можете на странице синдикации.

Исходная информация - http://habrahabr.ru/rss/.
Данный дневник сформирован из открытого RSS-источника по адресу http://feeds.feedburner.com/xtmb/hh-full, и дополняется в соответствии с дополнением данного источника. Он может не соответствовать содержимому оригинальной страницы. Трансляция создана автоматически по запросу читателей этой RSS ленты.
По всем вопросам о работе данного сервиса обращаться со страницы контактной информации.

[Обновить трансляцию]

Oracle фактически ликвидирует Sun

Понедельник, 11 Сентября 2017 г. 18:31 + в цитатник
temujin сегодня в 18:31 Управление

Oracle фактически ликвидирует Sun

    Избегайте этой ловушки, не следует придавать антропоморфные черты Ларри Эллисону.
    Брайэн Кантрилл


    Похоже, что в Oracle приняли решение окончательно избавиться от трудовых ресурсов, составляющих костяк Sun Microsystems. Массовые увольнения затронули около 2500 сотрудников, работающих над операционной системой Solaris, платформой SPARC и системами хранения данных ZFS Storage Appliance.





    Это не рядовая трансформация — оптимизация, а настоящая бойня. По мнению создателя системы динамической отладки Dtrace Брайэна Кантрилла (Bryan Cantrill) на сей раз нанесен непоправимый ущерб, в результате потери 90% производственных кадров подразделения Solaris, включая все руководство.


    От Solaris до illumos


    В 2009 г. Oracle приобрел испытывающую серьезнейшие трудности на рынке Sun Microsystems за 5.6 млрд. долларов США. Компания теряла позиции на рынке вследствие лавинообразного распространения Linux в качестве серверной ОС, успеха платформы amd64 и невнятной стратегии по взаимоотношениям с сообществом открытого ПО. Solaris стал открытым слишком поздно — лишь в 2005 г., причем открытым не полностью, отдельные элементы ОС, такие как локализация и некоторые драйвера, оставались проприетарными. Затем появился OpenSolaris, однако точкой сбора сообщества он не сумел стать. То ли дело была в лицензии CDDL, то ли проблема была в том, что Sun пыталась манипулировать проектом. Трудно сказать почему именно, но не взлетел.


    Kicked butt, had fun, didn't cheat, loved our customers, changed computing forever. Scott McNealy

    Эпитафия Скота МакНили как нельзя лучше отражает жизненный путь компании — отлично развлекались, не дурили головы своим заказчикам и навсегда изменили ИТ. Довольно быстро стало очевидно, что Solaris Ораклу попросту не нужен, и развивать его он не намерен. Затем 13 августе 2010 г. случилось одно из самых позорных событий в истории открытого ПО — компания втихую закрыла исходный код OS Solaris. Никаких официальных заявлений на сей счет не последовало.


    We will distribute updates to approved CDDL or other open source-licensed code following full releases of our enterprise Solaris operating system. In this manner, new technology innovations will show up in our releases before anywhere else. We will no longer distribute source code for the entirety of the Solaris operating system in real-time while it is developed, on a nightly basis.

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


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





    О принципах руководства компании. Оставшись без мудрого руководства манагеров инженеры выдали гору инноваций: ZFS, DTrace, Zones и много других.


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

    О закрытии исходного кода OpenSolaris.


    Это ОТВРАТИТЕЛЬНАЯ выходка со стороны корпорации. Вот из-за такого поведения мы становимся циничными и подозрительными.

    О последствиях закрытия исходников OpenSolaris для нового проекта ОС illumos — полностью открытого форка OpenSolaris.


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

    Вскоре после этого из Oracle ушли все разработчики DTrace, создатели ZFS, команда zones и сетевики. Вся разработка и инновация на этих направлениях далее происходила операционной системе illumos, где осела диаспора программистов из Sun Solaris. Благодаря особенностям открытых лицензий, в том числе CDDL, в рамках которой шла разработка OpenSolaris, Oracle не может претендовать на все последующие улучшения в коде illumos. То есть может, но только в рамках своего же проекта с открытым кодом. Сценарии Судного Дня работают как надо.


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


    Факты россыпью


    • Главная цель: выдоить патентные отчисления и штрафы у Google за использование Java в ОС Андроид, ставки были крупные — $8 млрд. Не срослось.
    • В целом монетизация Java не удалась, Oracle передает NetBeans Apache Foundation, верное решение.
    • Также решено не запускать платформу Sun Cloud.
    • Из-за бюрократических проволочек вокруг заплаток безопасности для MySQL, появился форк MariaDB, куда перешло значительное число разработчиков и часть сообщества. Их оказалось достаточно для новой компании.

    Выводы


    Sun Microsystems еще недавно — живая легенда и лучшее, что когда-либо было в Unix. Вот лишь небольшая часть их наследия.


    • NFS
    • RPC
    • ZFS
    • DTrace
    • Zones
    • Fault Management Architecture
    • Service Management Facility

    Компания Oracle имела все возможности для того, чтобы развивать и поддерживать OpenSolaris, но вместо этого закрыла исходники и с тех пор Solaris уже не имел будущего. Когда тяжба с компанией Google за использования Java в мобильной ОС Андроид закончилась пшиком в Oracle потеряли к активам Sun Microsystems всякий интерес. Вместо этого компания будет продавать ПО на основе собственной операционной системы — Unbreakable Linux.


    Материалы по теме


    Original source: habrahabr.ru (comments, light).

    https://habrahabr.ru/post/337682/


    Метки:  

    Blockchain Life 2017: от золотой пиццы к криптовалютной лихорадке

    Понедельник, 11 Сентября 2017 г. 18:05 + в цитатник
    О криптовалютах сейчас говорят на каждом углу. Новостные агентства мониторят курс биткойна и этериума, крупнейшие технологические гиганты активно интегрируют блокчейн-технологии в свои системы. Банковская система, презрительно фыркавшая на дерзкого новичка, взялась за голову и с осторожным интересом присматривается к нему. Читать далее

    https://habrahabr.ru/post/337396/


    Метки:  

    Blockchain Life 2017: от золотой пиццы к криптовалютной лихорадке

    Понедельник, 11 Сентября 2017 г. 18:05 + в цитатник
    О криптовалютах сейчас говорят на каждом углу. Новостные агентства мониторят курс биткойна и этериума, крупнейшие технологические гиганты активно интегрируют блокчейн-технологии в свои системы. Банковская система, презрительно фыркавшая на дерзкого новичка, взялась за голову и с осторожным интересом присматривается к нему. Читать далее

    https://habrahabr.ru/post/337396/


    Метки:  

    Apache Ignite 2.1 — теперь со вкусом Persistence

    Понедельник, 11 Сентября 2017 г. 16:04 + в цитатник

    Метки:  

    Apache Ignite 2.1 — теперь со вкусом Persistence

    Понедельник, 11 Сентября 2017 г. 16:04 + в цитатник

    Метки:  

    Эзотерический язык, транслирующийся в шаблоны C++

    Понедельник, 11 Сентября 2017 г. 15:33 + в цитатник
    sekrasoft сегодня в 15:33 Разработка

    Эзотерический язык, транслирующийся в шаблоны C++

      КПДВ с примерами кода Шаблоны C++ — полный по Тьюрингу язык, на котором можно писать compile-time программы. Только вот синтаксис рассчитан на описание параметризованных типов и слабо приспособлен к ясному выражению чего-то более сложного. В этой статье рассмотрим, как типы и шаблоны становятся значениями и функциями, а также узнаем, к чему привела попытка автора создать свой функциональный язык, транслирующийся в шаблоны C++. Для прочтения текста знания в области функционального программирования почти не требуются.

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


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

      Честно говоря, пару лет назад я порывался написать сюда пост про объяснение принципов работы compile-time программ на примере алгоритма смены приоритетов операторов в выражениях (т.е. реализовал варианты для compile-time и run-time алгоритма, с помощью которого можно было выражение $2 + 2 * 2$ «пересобрать» с альтернативными приоритетами и посчитать как $8$, а не $6$).

      Я уже почти дописал длинную статью с картинками и UML-диаграммами, но вдруг понял, что на хабре настолько часто рассказывали про метапрограммы, что это уже попросту никому не интересно. Тем более, что для практического использования добавили constexpr, а мне хотелось остаться в области извращённых развлечений и использовать минимум возможностей языка. Замечательные статьи Жизнь во время компиляции от HurrTheDurr и Интерпретация во время компиляции, или Альтернативное понимание лямбд в C++11 от ilammy (вторая — более хардкорная и без using template с constexpr) описывали практически всё, что извращённому уму нужно было знать. В итоге я оставил статью в черновиках, но не оставил желания метапрограммировать. И сегодня я возвращаюсь с заново написанным текстом и новыми идеями.

      Если, несмотря на все мои старания, читателю всё ещё будет трудно воспринимать материал, рекомендую прочитать указанные статьи, а вдогонку — Мягкое введение в Haskell (часть 1, часть 2) от Пола Хьюдака и др. в переводе Дениса Москвина и Лямбда-исчисление на JavaScript от ibessonov. Всё это в какой-то степени повлияло на автора, может и на читателя повлияет?!

      Метазначения и метафункции


      Шаблоны классов — это, в некотором смысле, функции от типов, которые принимают и возвращают типы. Соответственно, шаблоны становятся метафункциями, а типы и целые числа — метазначениями. Например, метафункция std::vector принимает метазначение T и возвращает метазначение std::vector. Важно, что у метазначения есть набор метаполей (с точки зрения C++ — вложенные классы, псевдонимы типов, статические константные поля), с помощью которых можно сделать самое интересное.

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

      template 
      struct square {
        static const int value = x * x;
      };
      

      Вызывается применяется, как говорят в ФП, метафункция следующим образом: square<5>::value.

      Но целые числа — это обычные значения, для работы с которыми достаточно и простого C++, а потому было бы несколько неспортивно использовать их в метавыражениях. Настоящее честное метазначение — это тип. Самый простой способ создать его — объявить структуру:

      struct One;
      struct Zero;
      

      С точки зрения C++ one и zero только объявлены и практически бесполезны. Достаточно ли этого? Да. Чему же тогда они равны и как их использовать? Равны они, что важно, только самим себе. Это своего рода абстрактные символы, которые можно задействовать в символьных вычислениях (почти как в Mathematica и др.). Метапрограмма будет вычислять значения метавыражений различной сложности. Рассмотрим сначала эти выражения, а несколько позже займёмся интерпретацией символов и выводом результатов на экран.

      Для нуля и единицы как булевых значений естественным будет написание функций NOT, AND и OR. Рассмотрим метафункцию отрицания:

      template 
      struct Not {
        typedef Zero value;
      };
      
      template <>
      struct Not  {
        typedef One value;
      };
      

      Not принимает некоторое значение и возвращает единицу, если это значение — ноль. Во всех остальных случаях она возвратит ноль. Таким образом, за счёт специализации шаблонов, мы имеем паттерн-матчинг (сравнение с образцом) в зачаточной стадии: можем описывать отдельно поведение функции для одного или нескольких аргументов, имеющих конкретные значения, а компилятор C++, отметив соответствие параметров шаблона одному из образцов, подставит нужную специализацию. Пользуясь этим, мы могли бы уже написать что-то рекурсивное (например факториал, разделив описание на fac<0> и fac<всё остальное>).

      Список


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

      template 
      struct Cons {
        typedef h head;
        typedef t tail;
      };
      
      struct Nil;
      

      Cons в ФП — функция, конструирующая список из первого элемента (головы) и списка остальных элементов (хвост). Обычному списку $\{one,two,three\}$ будет соответствовать Cons>>. Поскольку для работы со списком нужно уметь получать его составные части, мы сделаем Cons многозначной функцией, возвращающей голову (Cons<...,...>::head), и хвост (Cons<...,...>::tail). Любители ООП могут представить, что Cons — это конструктор, а head и tail — геттеры. В ФП все присваивания заменяются на вызов применение функции с изменёнными аргументами, поэтому сеттеров и их аналогов здесь не будет.

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

      // отрицание непустого списка
      template 
      struct negate {
      private: // инкапсулируем промежуточные вычисления
        typedef typename list::head h; // голова
        typedef typename Not::value not_head; // отрицание головы
        typedef typename list::tail t; // хвост
        typedef typename negate::value not_tail; // отрицание хвоста
      public:
        // список из отрицаний элементов - это отрицание головы,
        // соединённое с хвостом отрицаний
        typedef Cons value;
      };
      
      // отрицание пустого списка
      template <>
      struct negate  {
        // пустой список - сам себе отрицание
        typedef Nil value;
      };
      

      Тут оказывается, что Haskell не так страшен, как его малюют. Всё то же самое выглядит настолько просто, что не грех добавить для наглядности примеры на этом языке. Для неподготовленного читателя отметим основное отличие от C++ и школьной математики: в Haskell аргументы разделяются пробелами и не группируются в скобках. Т.е. $f(x+y, g(y), z)$ будет записано в Haskell как f (x+y) (g y) z.

      -- список - либо слитые голова и хвост (причём хвост - сам список),
      --          либо пустота
      data List a = Cons a List | Nil
      
      -- хелперы для получения головы и хвоста
      head (Cons x _) = x -- возвращает голову
      tail (Cons _ xs) = xs -- возвращает хвост
      
      -- отрицание списка
      negate (Cons x xs) = Cons (Not x) (negate xs)
      negate Nil = Nil
      

      В отличие от строго типизированных Haskell и C++, в языке шаблонов действует утиная типизация. negate::value, конечно, не сработает, но если One будет иметь метаполя head и tail, он вполне подойдёт. Впрочем, пока negate не «разыменовали» с помощью ::value, программа продолжает работать компилироваться.

      Функции высшего порядка


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

      -- Преобразованная голова соединяется с преобразованным хвостом
      map f (Cons x xs) = Cons (f x) (map f xs)
      -- Пустой список преобразовывать не нужно
      map f Nil = Nil
      

      STL содержит такое преобразование под именем std::transform. В нашем же языке метафункция map объявляется с помощью параметризации шаблона шаблоном:

      // преобразование непустого списка
      // f - шаблон с одним аргументом - унарная метафункция
      template 

      В качестве f сюда можем подставить описанную ранее функцию Not и посчитать список отрицаний:

      typedef map>>>::value list;
      // list эквивалентно Cons>>
      

      Операции именования выражений


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

      Начиная с C++11 можно использовать псевдонимы типов и шаблонов, чтобы задавать значения и функции через известные выражения:

      using x = Not::value;
      
      template 
      using g = Cons;
      

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

      Программа просто может уйти в бесконечную рекурсию
      Вспомним, что negate компилируется, а negate::value — нет. Пользуясь громоздкими метаполями можно написать метафункцию ветвления, которая вычисляет ::value только для одной своей ветки в зависимости от условия и возвращает это значение. Получится, что одно из выражений никогда не вычислится: все операции выполняются только при получении ::value, а его никто не трогал:
      // рассмотрим только одну из специализаций
      template 
      struct If  {
        // expr1, expr2 вычислены, а expr1::value и expr2::value - нет
        typedef typename expr1::value value;
      }
      
      // If, destroy>::value не разрушит мир
      

      В то же время, вариант с псевдонимом шаблона предусматривает, что некоторое g имеет смысл уже вычисленного f::value. И если ветвиться между двумя рекурсивными вариантами, вычисления будут бесконечными — аналог переполнения стека на этапе компиляции.

      template 
      struct If  {
        // expr1, expr2 вычислены
        typedef expr1 value;
      };
      
      // If::value>::value разрушит мир
      


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

      Замыкания


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

      Например, f x возвращает унарную функцию g, неявно использующую аргумент x функции f.

      f x = g
        where g xs = Cons x xs
      
      -- использование: (f x) xs или, что полезнее, map (f x) list
      

      Можно было бы оставить и Cons, но g как унарную можно передать в уже написанную функцию map, а как бинарную — Cons нет!

      Впрочем, обычный C++ от отсутствия замыканий не страдает
      Для этого используют функциональный объект или лямбда-функцию. То есть если передача в функцию дополнительных параметров через глобальные переменные нарушает гибкость программы, вместо функции используют объект, this которого содержит весь нужный контекст, а operator () принимает требуемые интерфейсом аргументы. Красивая концепция из ФП заменяется эквивалентной по мощности концепцией ООП. Даже стандартный шаблон std::function предусмотрен!

      // до C++11: функциональный объект, захвативший переменную
      struct f {
        f(something& x) : x(x) {}
        something_else operator () (something_else& xs) { // xs - явный аргумент
          return Cons(x, xs);
        }
      private:
        something x; // неявный аргумент
      };
      
      // C++11 и выше: лямбда-функция, захватившая переменную
      something x;
      auto f = [x](something_else& xs) { return Cons(x, xs); };
      


      При замене функций (например, int(int)) на функциональные объекты (например, std::function) программист C++ получает полноценный аналог замыканий.

      На уровне шаблонов потребности в подмене «функция -> объект», как оказывается, нет. Замыкания поддерживаются самим языком:

      template 
      struct f {
        template 
        struct g {
          typedef Cons value;
        };
      };
      
      // использование: f::g::value или map::g, list>
      

      В отличие от C++ и Haskell, здесь наружу f «вылезает» символ g, но в остальном — честное замыкание. Выражение f::g может быть использовано вместо обычной унарной функции (например, Not).

      Неограниченное число аргументов или синтаксический сахар для списков


      Вариадические шаблоны позволяют записывать функции от списков. Опять же, в некоторых случаях это удобнее, но без них тоже можно спокойно жить с Cons и Nil. Кроме того, это может оказаться даже более простым ходом. Чтобы передать в метафункцию два списка, достаточно… передать два списка: f, для вариадических же шаблонов требуется задавать обёртку, захватывающую все списки, кроме последнего: f, y1, y2, y3>, так как список аргументов произвольной длины должен быть один, и несколько списков, записанные подряд, просто «слипнутся». По завершении вычислений приходится возвращать обёртку, т.к. в C++ нельзя затайпдефить список из нескольких типов. А чтобы «выковырить» список из обёртки и передать в метафункцию (скажем, f), придётся использовать метаколлбеки: реализовать у обёртки метаметод принимающий эту метафункцию f и передающий ей содержимое списка:

      typename 
      struct list_wrapper {
        // передаём в list_wrapper<...>::call функцию,
        // которой передадут наши аргументы xs
        struct call

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

      typename 
      struct negate {
        template 
        struct f {
          typedef list_wrapper::value, ys> value;
        };
        
        typedef typename negate::template call::value;
      };
      
      typename <>
      struct negate<> {
        typedef list_wrapper<> value;
      };
      

      Вышло так, что за красивую запись вида $\{x1, x2, x3\}$ при объявлении, пришлось пострадать, запаковывая, передавая и распаковывая эти последовательности. negate выглядит более громоздко даже по сравнению с версией для Cons/Nil. Здесь требуются и более серьёзные измышления, когда как для написания «обычной» версии достаточно основ ФП и механической замены Haskell -> C++. Поэтому с помощью вариадических шаблонов лучше написать обёртку для преобразования последовательности параметров в список Cons/Nil, а затем при реализации программы пользоваться уже им. Так мы сможем и задавать списки приятным перечислением через запятую, и мыслить более простыми категориями.

      Выход во внешний мир


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

      По завершении вычислений в метапрограмме, результирующее выражение интерпретируется как сущность из реального мира и, либо сохраняется в переменную, либо выводится на экран. Для вывода можно использовать шаблонные классы. В конструкторе или других методах располагается императивный код. Он может быть как внутри самих метазначений (например, void One::print();), либо — внутри метафункции, если возможностей паттерн-матчинга хватает для рассмотрения всех вариантов. Вот, например, метафункция print, распечатывающая свой аргумент (единицу, ноль или список) на этапе конструирования экземпляра print<...>:

      template 
      struct print {
        print () {
          std::cout << "unknown number" << std::endl;
        }
      };
      
      template <>
      struct print  {
        print () {
          std::cout << "1" << std::endl;
        }
      };
      
      template <>
      struct print  {
        print () {
          std::cout << "0" << std::endl;
        }
      };
      
      // print::value>() выведет "1"
      

      Аналогично, можно вывести список и всё, что угодно. Например, мы бы могли реализовать AND, NOT, XOR, двоичный сумматор, представить числа как списки из One и Zero и построить сложение, умножение,…

      До C++11 и появления decltype никак нельзя было провести чисто функциональные вычисления над типами, создать переменные и объекты C++ соответствующих типов, провести вычисления над ними, а потом снова вернуться к вычислениям над типами.

      sum, three> // тип - корректно
      sum, three>() // значение - компилируется
      
      do_something(sum(), three()) // значения - компилируется
      sum(), three()> // нельзя посчитать,
      // т.к. нет перехода от значений к типам
      
      sum()), decltype(three())> // C++11; компилируется
      

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

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

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

      typedef sum, three> x;
      typedef decltype(one() + two() + three()) x;
      

      Выход за скобки нарушает чистоту:

      auto v = one() + two() + three(); // v выполнится как императивная конструкция
      typedef decltype(v) x;
      

      Эзотерический язык, транслирующийся в шаблоны C++


      Основная проблема шаблонов C++ как ФЯ времени компиляции — излишняя многословность. Все эти ::value, скобочки и typename сильно выматывают программиста и растягивают код программы. Конечно, логика говорит, что решивший программировать в таком стиле должен страдать, но… это одно из тех извращённых развлечений, к которым зачастую склоняется мозг айтишника. Хочется попробовать сделать так, чтобы извращённость сохранилась, но в той степени, когда страдания ещё можно терпеть.



      В общем, решил я создать свой язык, который будет транслироваться в шаблоны C++. Сделать это можно разными способами. Использовать можно даже наиболее хитроумные преобразования наподобие тех, что делает транслятор JSFuck. Но такой подход (а) породит ещё один ненужный язык, (б) оторванный от C++, (в) который ещё нужно придумать и (г) потратить силы на реализацию. А разработать и реализовать достаточно мощный и ненужный ФЯ — дело хлопотное и бесполезное. Особенно, когда в шаблонах C++ уже есть конструкции, эквивалентные функциям, замыканиям, паттерн-матчингу… Да и не спортивно это.

      Я избрал путь наибольшего соответствия. Код на моём языке должен был выглядеть просто, но оставаться идейно похожим на C++. То есть преобразование должно было наиболее дословно переводить мой язык в C++. Он должен был стать килограммом синтаксического сахара над шаблонами.

      Давайте перепишем приведённые выше куски кода на созданный язык, таким образом рассмотрев его особенности и их применение в деле. Будем двигаться в том же порядке от определения значений «ноль» и «один», функции отрицания числа, конструкторов списка к определению функции отрицания списка, ФВП map и т.д.

      Значения и функции


      Для объявления значения не требуется ключевое слово struct:

      One;
      Zero;
      

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

      Not (val x) = Zero; // общий случай
      Not (Zero) = One; // частный случай
      
      And(val x, val y); // для x,y != One,Zero And не определена
      And(val x, One) = x; // One - константа, val x - аргумент
      And(val x, Zero) = Zero;
      
      // использование: Not(One) или And(One, Zero)
      

      В C++ параметром шаблона может быть тип (typename), другой шаблон (template class) и т.д., и это следует указывать. А значит, создавая метаФВП, одним typename не обойтись и требование указания типа переходит также в мой язык. По умолчанию задан тип val, соответствующий обычному метазначению (структура или обычный тип в C++). Для описания функций можно комбенировать типы с помощью стрелки (->). Например, val -> val — унарная функция (шаблон, принимающий один параметр), (val, val) -> val — бинарная функция (шаблон, принимающий два параметра) и так далее.

      Здесь я немного отступил от дословности транслирования и ввёл псевдонимы типов, аналога которым (псевдонимов видов) в C++ нет. С помощью #type можно задавать синонимы, чтобы ещё чуть-чуть сократить запись и прояснить смысл за счёт уместного именования:

      #type number = val;
      #type list = val;
      #type unary = number -> number;
      #type map_t = (unary, list) -> list;
      // map_t раскроется в template 

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

      Список


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

      Nil;
      
      Cons(val x, val y) {
        head = x;
        tail = y;
      }
      

      Применение функции к аргументам, транслируясь в C++, автоматически раскрывает ::value. То есть f(x) эквивалентно f::value. Но у Cons сгенерируется только два метаполя head и tail. Предотвращать раскрытие ::value требуется явно с помощью апострофа: Cons(x, xs)'.

      Я долго думал над этой проблемой. С одной стороны, ::value как одна из частых конструкций должна была раскрываться автоматически, но должна была быть возможность (а) передать нераскрытое значение (см. проблему функции ветвления и бесконечной рекурсии выше под спойлером) и (б) использовать другие метаполя, кроме value. В итоге я остановился на «экранировании», записи метаполей через точку и ввёл обратный для экранирования оператор "!", раскрывающий ::value:

      Cons(x, xs)'.head; // x
      Cons(x, xs)'.tail; // xs
      Not(Zero);         // One
      Not(Zero)'!;       // One
      

      Впрочем, метаполе ::value может вполне сосуществовать со множественным возвратом. Реализуя отрицание списка negate на C++, мы создавали локальные метапеременные, которые вполне можно было возвратить. Здесь — аналогично, только все значения публичны:

      // отрицание непустого списка:
      // список из отрицаний элементов - это отрицание головы,
      // соединённое с хвостом отрицаний
      negate (list list) = Cons(not_head, not_tail)' {
        h = list.head; // голова
        not_head = Not(h); // отрицание головы
        t = list.tail; // хвост
        not_tail = negate(t); // отрицание хвоста
      } // точку с запятой можно опустить!
      
      // пустой список - сам себе отрицание
      negate (Nil) = Nil; 
      

      Функции высшего порядка


      Вспомним, что мы объявили типы унарной функции и списка и напишем map:

      // преобразование непустого списка
      map (unary f, list list) = Cons(new_head, new_tail)' {
        h = list.head; // голова
        new_head = f(h); // преобразованная голова
        t = list.tail; // хвост
        new_tail = map(f, t); // преобразованный хвост
      }
      
      // преобразование пустого списка
      map (unary f, Nil) = Nil;
      

      Разумеется, вместо unary и list можно было сразу указать val -> val и val соответственно, но запись оказалась бы более длинной и менее наглядной.

      Замыкания


      Поскольку замыкания поддерживаются самими шаблонами C++, здесь нет чего-то необычного:

      f(val x) = {
        g(val xs) = Cons(x, xs)';
      }
      
      // использование: f(x)'.g(list) или map(f(x)'.g, list)
      

      Но требуется давать имя (например, g) возвращаемой функции. Возвращаемая безымянная функция должна была бы иметь имя value и возвращать value. Одноимённый с классом член в C++ уже отдан конструктору, из-за чего задание ему нового смысла через typedef приводит к ошибке компиляции:

      template 
      struct f {
        template 
        struct value {               // value!
          typedef Cons value; // value!
        };
      };
      

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

      C++ также не позволяет переиспользовать имена параметров шаблона внутри шаблона. Поскольку эти имена в моём языке относятся к локальным метапеременным и за пределы шаблона не выходят, я автоматизировал их переименование:

      f (val x) = g(Not(x)) {
        g (val x) = x; // будет заменено на g (x1) = x1
        // т.к. x станет параметром template f и не допустит x как параметр template g
      }
      

      Такое преобразование, как мне кажется, не добавило «неспортивности» (из-за возможности тривиальной замены вручную), а жизнь несколько упростило.

      Выход во внешний мир


      На определённом этапе оказалось, что чисто функциональная часть более-менее успешно транслируется в C++, и надо как-то уметь хотя бы распечатать результат. Эта часть, выходила за рамки интересного мне чистого мира шаблонов (из-за чего в итоге продумана хуже) и, что более важно, достаточно хорошо описывалась обычным C++. В программах с компайл-тайм вычислениями на шаблонах за выход во внешний мир отвечает обычный C++ без шаблонов. То есть я должен был либо сделать мой язык надмножеством C++, либо создать свой аналог C++. Разумеется, я выбрал первый вариант.

      Оставалось только ввести тэги для вставки кода на C++ и удовлетворённо потирать руки. Но возникла логическая проблема: в исходниках нигде не упоминается, что для применения метафункций используется ::value, нигде не сказано, что f(x) — это f::value, а не call::result. Это знание хранилось внутри транслятора, и его использование в программе прорывало бы абстракцию:

      f(val x) = One;
      f(Zero) = Zero;
      
      main {
        print::value>(); // почему так?
      }
      

      Идей насчёт малоинтересной императивной части было немного, и после прикидывания нескольких вариантов я решил ввести (а) императивные блоки с возможностью использования в них обычного C++ и (б) экспорт в них значений из чистого функционального мира. Блок impure { код } может появляться вместо объявления значения/функции, а также справа после знака "=" при объявлении функции. В этих случаях код на C++ вставляется в соответствующее место программы. Для экспортирования выражения перед ним ставится ключевое слово impure. С точки зрения C++ это эквивалентно конструированию объекта типа, описываемого выражением.

      В качестве демонстрации работы impure приготовим «распечатыватель» списка:

      print(val list) {
        head = list.head;
        tail = list.tail;
        impure {
          // после typedef-ов head, tail расположится следующий блок кода:
          print() {
            impure print(head); // преобразуется в "print();"
            std::cout << ", ";
            impure print(tail);
          }
        }
      }
      
      print(Zero) = impure { // аналогично print(Zero) { impure { ..., но короче
        print() {
          std::cout << "0";
        }
      }
      
      print(One) = impure {
        print() {
          std::cout << "1";
        }
      }
      
      print(Nil) = impure {
        print() {
          std::cout << std::endl;
        }
      }
      

      Пространства имён


      Поскольку на моём языке в теории можно было бы создавать библиотеки, которые использовались бы в обычном C++, я «прокинул» в него namespace и using namespace:

      namespace ns1 {
        namespace ns2 {
          f(val x) = x;
        }
      }
      
      namespace ns3 {
        using namespace ns1.ns2;
        g(val x) = f(x);
      }
      

      using namespace также является вынужденной мерой. В С++ в некоторых случаях недостаточно записи вида f::y. Если f, x или z — не конкретные типы/шаблоны, а параметры шаблона, то ::y становится выходом чёрного ящика. Нужно указывать, что мы получаем при вычислении ::y — тип или шаблон (например, typename f::y или f::template y). Я не автоматизировал эти указания и не реализовал синтаксический сахар для более простого описания вручную, поэтому каждое использование точки вызывает появление «typename». Т.е. f::y оттранслируется во что-то некорректное вида typename typename f::value::typename y::value. В случае пространства имён это излишне, using namespace позволяет обойтись без вставки «typename».

      Лямбды


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

      f          (val x) =  y(x) { y(x) = g(x); } // было
      f = lambda (val x) -> y(x) { y(x) = g(x); } // стало
      

      Полноценная поддержка лямбд затрудняется тем, что в C++ (а) нельзя создать анонимный класс и (б) нельзя объявить нечто, эквивалентное шаблону, не раскрутив описание до эквивалентности типа типу. Т.е., выражаясь в математических терминах, $x = y$ написать можно, а вместо $g = f$ придётся использовать $g(x) = f(x)$. Пользователи императивного программирования уже привыкли к такому положению дел, но по меркам программирования функционального это довольно громоздко.

      template  struct f {}; // пусть была метафункция f
      
      using g = f; // нельзя описать эквивалентность шаблона шаблону
      
      // нужно описать эквивалентность выражений, соответствующих типам
      template  using g = f; // g - тип, f - тип
      

      Для реализации записей вида map(lambda(val x) -> y, xs) придётся модифицировать язык и генерировать шаблоны с временными именами.

      Как рассматривалось выше, value::value — это конструктор, поэтому не получается напрямую реализовать лямбду, которая возвращает лямбду. Для возврата функций из лямбд и разрешения записей вида g = f нужно использовать другие концепции и почти полностью переписать транслятор.

      Недостатки и возможные пути решения


      1. Нет сокрытия локальных переменных.
        Наивно решается вставкой impure { private: } в код или элементарной модификацией языка.
      2. При использовании метаполя автоматически подставляется «typename».
        Не знаю, можно ли избавиться от этого автоматически. В качестве варианта решения можно использовать обязательную венгерскую нотацию (скажем, fMap(fCurry(fCons).fApply(vXs), vObj.vYs) — при доступе к метаполю подставится template, typename или ничего, если первым символом имени будет «f», «v» или «n» соответственно).

        Второй путь — «динамическая типизация». То есть представление всех сущностей в виде структуры-обёртки с хранящимся типом и значением:

        // объявляем значение x
        struct x {
          typedef internal::value type; // тип значения x
        };
        
        // объявляем функцию f(x) = x
        struct f {
          typedef internal::function type; // тип функции f
          template 
          struct value {
            typedef typename x::type type; // тип результата функции f
            struct result { // обёртка для того, чтобы избежать value::value
              typedef x value;
            };
          };
        };
        

        Транслятор автоматически сгенерирует пространство имён internal со списком типов и метафункциями для работы с ними; за счёт дополнительной обёртки struct result можно будет избавиться от value::value и возвращать безымянные функции из функций. Также из-за того, что любое значение или функция будет выражаться в виде структуры (то есть просто типа, а не типа или шаблона), можно будет задавать синонимы функций (с помощью typedef), отменить обязательное описание типов аргументов функции, устранить ограничения на лямбды и возврат функций из ФВП.
      3. Есть ограничения на лямбды: они не могут возвращать функции и быть анонимными.
        Может помочь описанный выше подход.

        Также можно вместо value использовать два значения так, чтобы при увеличении глубины вложенности использовалось другое значение. Получится, что вместо value::value будет value1::value2 и никаких попыток переопределения конструктора не будет:

        template 
        struct f {
          typedef internal::True uses_value1; // использует value1
          template 
          struct value1 {
            typedef internal::False uses_value1; // использует value2
            typedef Cons value2;
          };
        };
        

        Вызвать такие метафункции можно будет с помощью отдельной метафункции, которая в зависимости от обязательного метаполя uses_value1 будет выбирать, доступаться до value1 или до value2.
      4. Не учитываются вариадические шаблоны и целые числа как параметры шаблонов.
        Для реализации требуется поддержка кроме val дополнительных типов для чисел (int, uint, ...) и массивов ([val], [int], [uint], ...). При использовании метаполей придётся решить описанную выше проблему с указанием template и typename, т.к. для чисел ничего не требуется указывать, а для типов и шаблонов — требуется.

      Пример программы


      В качестве примера построим список $\{1, 1, 0\}$, возьмём его отрицание двумя способами и распечатаем все эти значения в main:

      // строим списки:
      my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')';
      negated_list = negate(my_list);
      negated_list2 = map(Not, my_list);
      
      impure {
        int main() {
          // печатаем списки:
          std::cout << "my list is ";
          impure print(my_list);
          
          std::cout << "negated list is ";
          impure print(negated_list);
          
          std::cout << "negated list is also ";
          impure print(negated_list2);
        }
      }
      

      Программа выведет следующее:

      my list is 1, 1, 0,
      negated list is 0, 0, 1,
      negated list is also 0, 0, 1,
      

      Исходный код программы целиком
      impure {
        #include 
      }
      
      One;
      Zero;
      
      Not (val x) = Zero; // общий случай
      Not (Zero) = One; // частный случай
      
      And(val x, val y); // для x,y != One,Zero And не определена
      And(val x, One) = x; // One - константа, val x - аргумент
      And(val x, Zero) = Zero;
      
      #type number = val;
      #type list = val;
      #type unary = number -> number;
      #type map_t = (unary, list) -> list;
      
      Nil;
      
      Cons(val x, val y) {
        head = x;
        tail = y;
      }
      
      // отрицание непустого списка:
      negate (list list) = Cons(not_head, not_tail)' {
        h = list.head;
        not_head = Not(h);
        t = list.tail;
        not_tail = negate(t);
      }
      
      // пустой список - сам себе отрицание
      negate (Nil) = Nil; 
      
      // преобразование непустого списка
      map (unary f, list list) = Cons(new_head, new_tail)' {
        h = list.head;
        new_head = f(h);
        t = list.tail;
        new_tail = map(f, t);
      }
      
      // преобразование пустого списка
      map (unary f, Nil) = Nil;
      
      print(val list) {
        head = list.head;
        tail = list.tail;
        impure {
          print() {
            impure print(head);
            std::cout << ", ";
            impure print(tail);
          }
        }
      }
      
      print(Zero) = impure {
        print() {
          std::cout << "0";
        }
      }
      
      print(One) = impure {
        print() {
          std::cout << "1";
        }
      }
      
      print(Nil) = impure {
        print() {
          std::cout << std::endl;
        }
      }
      
      my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')';
      negated_list = negate(my_list);
      negated_list2 = map(Not, my_list);
      
      impure {
        int main() {
          std::cout << "my list is ";
          impure print(my_list);
          
          std::cout << "negated list is ";
          impure print(negated_list);
          
          std::cout << "negated list is also ";
          impure print(negated_list2);
        }
      }
      


      Код после трансляции в C++
      #include 
      struct One;
      struct Zero;
      template 
      struct Not {
        typedef Zero _value;
      };
      template <>
      struct Not {
        typedef One _value;
      };
      template 
      struct And;
      template 
      struct And {
        typedef x _value;
      };
      template 
      struct And {
        typedef Zero _value;
      };
      struct Nil;
      template 
      struct Cons {
        typedef x head;
        typedef y tail;
      };
      template 
      struct negate {
        typedef typename list::head h;
        typedef typename Not  ::_value not_head;
        typedef typename list::tail t;
        typedef typename negate  ::_value not_tail;
        typedef Cons   _value;
      };
      template <>
      struct negate {
        typedef Nil _value;
      };
      template 


      Транслятор


      Свой транслятор я написал на JavaScript (люблю этот язык, на нём мне легче думается). В качестве генератора парсеров использовал PEG.js.

      При запуске транслятор (см. страничку языка на GitHub) считывает файл, имя которого указано как параметр командной строки, и выдаёт в stdout результирующий текст программы на C++.

      node src/compile <исходник> # запуск

      Как только транслятор более-менее заработал и были написаны работающие программы, я разместил всё это добро на GitHub и, словно математик из анекдота, воскликнувший «Решение есть!», почти утратил интерес к развитию проекта. Порывался решить указанные выше проблемы чередованием/указанием типа/венгерской нотацией, но это требовало серьёзных изменений и повторного умственного напряжения. Думать, имея что-то готовое и работающее, было лень. Более того, мог нарушиться принцип наибольшего соответствия. Лишние обёртки и код по работе с ними убили бы красоту прямолинейности трансляции и приблизили бы программу к превышению лимита на вложенность шаблонов.

      Поэтому я остановился на достигнутом и буду рад, если кто-нибудь заинтересуется и, быть может, сделает свой вклад в развитие языка.
      Original source: habrahabr.ru (comments, light).

      https://habrahabr.ru/post/337590/


      Эзотерический язык, транслирующийся в шаблоны C++

      Понедельник, 11 Сентября 2017 г. 15:33 + в цитатник
      sekrasoft сегодня в 15:33 Разработка

      Эзотерический язык, транслирующийся в шаблоны C++

        КПДВ с примерами кода Шаблоны C++ — полный по Тьюрингу язык, на котором можно писать compile-time программы. Только вот синтаксис рассчитан на описание параметризованных типов и слабо приспособлен к ясному выражению чего-то более сложного. В этой статье рассмотрим, как типы и шаблоны становятся значениями и функциями, а также узнаем, к чему привела попытка автора создать свой функциональный язык, транслирующийся в шаблоны C++. Для прочтения текста знания в области функционального программирования почти не требуются.

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


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

        Честно говоря, пару лет назад я порывался написать сюда пост про объяснение принципов работы compile-time программ на примере алгоритма смены приоритетов операторов в выражениях (т.е. реализовал варианты для compile-time и run-time алгоритма, с помощью которого можно было выражение $2 + 2 * 2$ «пересобрать» с альтернативными приоритетами и посчитать как $8$, а не $6$).

        Я уже почти дописал длинную статью с картинками и UML-диаграммами, но вдруг понял, что на хабре настолько часто рассказывали про метапрограммы, что это уже попросту никому не интересно. Тем более, что для практического использования добавили constexpr, а мне хотелось остаться в области извращённых развлечений и использовать минимум возможностей языка. Замечательные статьи Жизнь во время компиляции от HurrTheDurr и Интерпретация во время компиляции, или Альтернативное понимание лямбд в C++11 от ilammy (вторая — более хардкорная и без using template с constexpr) описывали практически всё, что извращённому уму нужно было знать. В итоге я оставил статью в черновиках, но не оставил желания метапрограммировать. И сегодня я возвращаюсь с заново написанным текстом и новыми идеями.

        Если, несмотря на все мои старания, читателю всё ещё будет трудно воспринимать материал, рекомендую прочитать указанные статьи, а вдогонку — Мягкое введение в Haskell (часть 1, часть 2) от Пола Хьюдака и др. в переводе Дениса Москвина и Лямбда-исчисление на JavaScript от ibessonov. Всё это в какой-то степени повлияло на автора, может и на читателя повлияет?!

        Метазначения и метафункции


        Шаблоны классов — это, в некотором смысле, функции от типов, которые принимают и возвращают типы. Соответственно, шаблоны становятся метафункциями, а типы и целые числа — метазначениями. Например, метафункция std::vector принимает метазначение T и возвращает метазначение std::vector. Важно, что у метазначения есть набор метаполей (с точки зрения C++ — вложенные классы, псевдонимы типов, статические константные поля), с помощью которых можно сделать самое интересное.

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

        template 
        struct square {
          static const int value = x * x;
        };
        

        Вызывается применяется, как говорят в ФП, метафункция следующим образом: square<5>::value.

        Но целые числа — это обычные значения, для работы с которыми достаточно и простого C++, а потому было бы несколько неспортивно использовать их в метавыражениях. Настоящее честное метазначение — это тип. Самый простой способ создать его — объявить структуру:

        struct One;
        struct Zero;
        

        С точки зрения C++ one и zero только объявлены и практически бесполезны. Достаточно ли этого? Да. Чему же тогда они равны и как их использовать? Равны они, что важно, только самим себе. Это своего рода абстрактные символы, которые можно задействовать в символьных вычислениях (почти как в Mathematica и др.). Метапрограмма будет вычислять значения метавыражений различной сложности. Рассмотрим сначала эти выражения, а несколько позже займёмся интерпретацией символов и выводом результатов на экран.

        Для нуля и единицы как булевых значений естественным будет написание функций NOT, AND и OR. Рассмотрим метафункцию отрицания:

        template 
        struct Not {
          typedef Zero value;
        };
        
        template <>
        struct Not  {
          typedef One value;
        };
        

        Not принимает некоторое значение и возвращает единицу, если это значение — ноль. Во всех остальных случаях она возвратит ноль. Таким образом, за счёт специализации шаблонов, мы имеем паттерн-матчинг (сравнение с образцом) в зачаточной стадии: можем описывать отдельно поведение функции для одного или нескольких аргументов, имеющих конкретные значения, а компилятор C++, отметив соответствие параметров шаблона одному из образцов, подставит нужную специализацию. Пользуясь этим, мы могли бы уже написать что-то рекурсивное (например факториал, разделив описание на fac<0> и fac<всё остальное>).

        Список


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

        template 
        struct Cons {
          typedef h head;
          typedef t tail;
        };
        
        struct Nil;
        

        Cons в ФП — функция, конструирующая список из первого элемента (головы) и списка остальных элементов (хвост). Обычному списку $\{one,two,three\}$ будет соответствовать Cons>>. Поскольку для работы со списком нужно уметь получать его составные части, мы сделаем Cons многозначной функцией, возвращающей голову (Cons<...,...>::head), и хвост (Cons<...,...>::tail). Любители ООП могут представить, что Cons — это конструктор, а head и tail — геттеры. В ФП все присваивания заменяются на вызов применение функции с изменёнными аргументами, поэтому сеттеров и их аналогов здесь не будет.

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

        // отрицание непустого списка
        template 
        struct negate {
        private: // инкапсулируем промежуточные вычисления
          typedef typename list::head h; // голова
          typedef typename Not::value not_head; // отрицание головы
          typedef typename list::tail t; // хвост
          typedef typename negate::value not_tail; // отрицание хвоста
        public:
          // список из отрицаний элементов - это отрицание головы,
          // соединённое с хвостом отрицаний
          typedef Cons value;
        };
        
        // отрицание пустого списка
        template <>
        struct negate  {
          // пустой список - сам себе отрицание
          typedef Nil value;
        };
        

        Тут оказывается, что Haskell не так страшен, как его малюют. Всё то же самое выглядит настолько просто, что не грех добавить для наглядности примеры на этом языке. Для неподготовленного читателя отметим основное отличие от C++ и школьной математики: в Haskell аргументы разделяются пробелами и не группируются в скобках. Т.е. $f(x+y, g(y), z)$ будет записано в Haskell как f (x+y) (g y) z.

        -- список - либо слитые голова и хвост (причём хвост - сам список),
        --          либо пустота
        data List a = Cons a List | Nil
        
        -- хелперы для получения головы и хвоста
        head (Cons x _) = x -- возвращает голову
        tail (Cons _ xs) = xs -- возвращает хвост
        
        -- отрицание списка
        negate (Cons x xs) = Cons (Not x) (negate xs)
        negate Nil = Nil
        

        В отличие от строго типизированных Haskell и C++, в языке шаблонов действует утиная типизация. negate::value, конечно, не сработает, но если One будет иметь метаполя head и tail, он вполне подойдёт. Впрочем, пока negate не «разыменовали» с помощью ::value, программа продолжает работать компилироваться.

        Функции высшего порядка


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

        -- Преобразованная голова соединяется с преобразованным хвостом
        map f (Cons x xs) = Cons (f x) (map f xs)
        -- Пустой список преобразовывать не нужно
        map f Nil = Nil
        

        STL содержит такое преобразование под именем std::transform. В нашем же языке метафункция map объявляется с помощью параметризации шаблона шаблоном:

        // преобразование непустого списка
        // f - шаблон с одним аргументом - унарная метафункция
        template 

        В качестве f сюда можем подставить описанную ранее функцию Not и посчитать список отрицаний:

        typedef map>>>::value list;
        // list эквивалентно Cons>>
        

        Операции именования выражений


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

        Начиная с C++11 можно использовать псевдонимы типов и шаблонов, чтобы задавать значения и функции через известные выражения:

        using x = Not::value;
        
        template 
        using g = Cons;
        

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

        Программа просто может уйти в бесконечную рекурсию
        Вспомним, что negate компилируется, а negate::value — нет. Пользуясь громоздкими метаполями можно написать метафункцию ветвления, которая вычисляет ::value только для одной своей ветки в зависимости от условия и возвращает это значение. Получится, что одно из выражений никогда не вычислится: все операции выполняются только при получении ::value, а его никто не трогал:
        // рассмотрим только одну из специализаций
        template 
        struct If  {
          // expr1, expr2 вычислены, а expr1::value и expr2::value - нет
          typedef typename expr1::value value;
        }
        
        // If, destroy>::value не разрушит мир
        

        В то же время, вариант с псевдонимом шаблона предусматривает, что некоторое g имеет смысл уже вычисленного f::value. И если ветвиться между двумя рекурсивными вариантами, вычисления будут бесконечными — аналог переполнения стека на этапе компиляции.

        template 
        struct If  {
          // expr1, expr2 вычислены
          typedef expr1 value;
        };
        
        // If::value>::value разрушит мир
        


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

        Замыкания


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

        Например, f x возвращает унарную функцию g, неявно использующую аргумент x функции f.

        f x = g
          where g xs = Cons x xs
        
        -- использование: (f x) xs или, что полезнее, map (f x) list
        

        Можно было бы оставить и Cons, но g как унарную можно передать в уже написанную функцию map, а как бинарную — Cons нет!

        Впрочем, обычный C++ от отсутствия замыканий не страдает
        Для этого используют функциональный объект или лямбда-функцию. То есть если передача в функцию дополнительных параметров через глобальные переменные нарушает гибкость программы, вместо функции используют объект, this которого содержит весь нужный контекст, а operator () принимает требуемые интерфейсом аргументы. Красивая концепция из ФП заменяется эквивалентной по мощности концепцией ООП. Даже стандартный шаблон std::function предусмотрен!

        // до C++11: функциональный объект, захвативший переменную
        struct f {
          f(something& x) : x(x) {}
          something_else operator () (something_else& xs) { // xs - явный аргумент
            return Cons(x, xs);
          }
        private:
          something x; // неявный аргумент
        };
        
        // C++11 и выше: лямбда-функция, захватившая переменную
        something x;
        auto f = [x](something_else& xs) { return Cons(x, xs); };
        


        При замене функций (например, int(int)) на функциональные объекты (например, std::function) программист C++ получает полноценный аналог замыканий.

        На уровне шаблонов потребности в подмене «функция -> объект», как оказывается, нет. Замыкания поддерживаются самим языком:

        template 
        struct f {
          template 
          struct g {
            typedef Cons value;
          };
        };
        
        // использование: f::g::value или map::g, list>
        

        В отличие от C++ и Haskell, здесь наружу f «вылезает» символ g, но в остальном — честное замыкание. Выражение f::g может быть использовано вместо обычной унарной функции (например, Not).

        Неограниченное число аргументов или синтаксический сахар для списков


        Вариадические шаблоны позволяют записывать функции от списков. Опять же, в некоторых случаях это удобнее, но без них тоже можно спокойно жить с Cons и Nil. Кроме того, это может оказаться даже более простым ходом. Чтобы передать в метафункцию два списка, достаточно… передать два списка: f, для вариадических же шаблонов требуется задавать обёртку, захватывающую все списки, кроме последнего: f, y1, y2, y3>, так как список аргументов произвольной длины должен быть один, и несколько списков, записанные подряд, просто «слипнутся». По завершении вычислений приходится возвращать обёртку, т.к. в C++ нельзя затайпдефить список из нескольких типов. А чтобы «выковырить» список из обёртки и передать в метафункцию (скажем, f), придётся использовать метаколлбеки: реализовать у обёртки метаметод принимающий эту метафункцию f и передающий ей содержимое списка:

        typename 
        struct list_wrapper {
          // передаём в list_wrapper<...>::call функцию,
          // которой передадут наши аргументы xs
          struct call

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

        typename 
        struct negate {
          template 
          struct f {
            typedef list_wrapper::value, ys> value;
          };
          
          typedef typename negate::template call::value;
        };
        
        typename <>
        struct negate<> {
          typedef list_wrapper<> value;
        };
        

        Вышло так, что за красивую запись вида $\{x1, x2, x3\}$ при объявлении, пришлось пострадать, запаковывая, передавая и распаковывая эти последовательности. negate выглядит более громоздко даже по сравнению с версией для Cons/Nil. Здесь требуются и более серьёзные измышления, когда как для написания «обычной» версии достаточно основ ФП и механической замены Haskell -> C++. Поэтому с помощью вариадических шаблонов лучше написать обёртку для преобразования последовательности параметров в список Cons/Nil, а затем при реализации программы пользоваться уже им. Так мы сможем и задавать списки приятным перечислением через запятую, и мыслить более простыми категориями.

        Выход во внешний мир


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

        По завершении вычислений в метапрограмме, результирующее выражение интерпретируется как сущность из реального мира и, либо сохраняется в переменную, либо выводится на экран. Для вывода можно использовать шаблонные классы. В конструкторе или других методах располагается императивный код. Он может быть как внутри самих метазначений (например, void One::print();), либо — внутри метафункции, если возможностей паттерн-матчинга хватает для рассмотрения всех вариантов. Вот, например, метафункция print, распечатывающая свой аргумент (единицу, ноль или список) на этапе конструирования экземпляра print<...>:

        template 
        struct print {
          print () {
            std::cout << "unknown number" << std::endl;
          }
        };
        
        template <>
        struct print  {
          print () {
            std::cout << "1" << std::endl;
          }
        };
        
        template <>
        struct print  {
          print () {
            std::cout << "0" << std::endl;
          }
        };
        
        // print::value>() выведет "1"
        

        Аналогично, можно вывести список и всё, что угодно. Например, мы бы могли реализовать AND, NOT, XOR, двоичный сумматор, представить числа как списки из One и Zero и построить сложение, умножение,…

        До C++11 и появления decltype никак нельзя было провести чисто функциональные вычисления над типами, создать переменные и объекты C++ соответствующих типов, провести вычисления над ними, а потом снова вернуться к вычислениям над типами.

        sum, three> // тип - корректно
        sum, three>() // значение - компилируется
        
        do_something(sum(), three()) // значения - компилируется
        sum(), three()> // нельзя посчитать,
        // т.к. нет перехода от значений к типам
        
        sum()), decltype(three())> // C++11; компилируется
        

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

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

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

        typedef sum, three> x;
        typedef decltype(one() + two() + three()) x;
        

        Выход за скобки нарушает чистоту:

        auto v = one() + two() + three(); // v выполнится как императивная конструкция
        typedef decltype(v) x;
        

        Эзотерический язык, транслирующийся в шаблоны C++


        Основная проблема шаблонов C++ как ФЯ времени компиляции — излишняя многословность. Все эти ::value, скобочки и typename сильно выматывают программиста и растягивают код программы. Конечно, логика говорит, что решивший программировать в таком стиле должен страдать, но… это одно из тех извращённых развлечений, к которым зачастую склоняется мозг айтишника. Хочется попробовать сделать так, чтобы извращённость сохранилась, но в той степени, когда страдания ещё можно терпеть.



        В общем, решил я создать свой язык, который будет транслироваться в шаблоны C++. Сделать это можно разными способами. Использовать можно даже наиболее хитроумные преобразования наподобие тех, что делает транслятор JSFuck. Но такой подход (а) породит ещё один ненужный язык, (б) оторванный от C++, (в) который ещё нужно придумать и (г) потратить силы на реализацию. А разработать и реализовать достаточно мощный и ненужный ФЯ — дело хлопотное и бесполезное. Особенно, когда в шаблонах C++ уже есть конструкции, эквивалентные функциям, замыканиям, паттерн-матчингу… Да и не спортивно это.

        Я избрал путь наибольшего соответствия. Код на моём языке должен был выглядеть просто, но оставаться идейно похожим на C++. То есть преобразование должно было наиболее дословно переводить мой язык в C++. Он должен был стать килограммом синтаксического сахара над шаблонами.

        Давайте перепишем приведённые выше куски кода на созданный язык, таким образом рассмотрев его особенности и их применение в деле. Будем двигаться в том же порядке от определения значений «ноль» и «один», функции отрицания числа, конструкторов списка к определению функции отрицания списка, ФВП map и т.д.

        Значения и функции


        Для объявления значения не требуется ключевое слово struct:

        One;
        Zero;
        

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

        Not (val x) = Zero; // общий случай
        Not (Zero) = One; // частный случай
        
        And(val x, val y); // для x,y != One,Zero And не определена
        And(val x, One) = x; // One - константа, val x - аргумент
        And(val x, Zero) = Zero;
        
        // использование: Not(One) или And(One, Zero)
        

        В C++ параметром шаблона может быть тип (typename), другой шаблон (template class) и т.д., и это следует указывать. А значит, создавая метаФВП, одним typename не обойтись и требование указания типа переходит также в мой язык. По умолчанию задан тип val, соответствующий обычному метазначению (структура или обычный тип в C++). Для описания функций можно комбенировать типы с помощью стрелки (->). Например, val -> val — унарная функция (шаблон, принимающий один параметр), (val, val) -> val — бинарная функция (шаблон, принимающий два параметра) и так далее.

        Здесь я немного отступил от дословности транслирования и ввёл псевдонимы типов, аналога которым (псевдонимов видов) в C++ нет. С помощью #type можно задавать синонимы, чтобы ещё чуть-чуть сократить запись и прояснить смысл за счёт уместного именования:

        #type number = val;
        #type list = val;
        #type unary = number -> number;
        #type map_t = (unary, list) -> list;
        // map_t раскроется в template 

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

        Список


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

        Nil;
        
        Cons(val x, val y) {
          head = x;
          tail = y;
        }
        

        Применение функции к аргументам, транслируясь в C++, автоматически раскрывает ::value. То есть f(x) эквивалентно f::value. Но у Cons сгенерируется только два метаполя head и tail. Предотвращать раскрытие ::value требуется явно с помощью апострофа: Cons(x, xs)'.

        Я долго думал над этой проблемой. С одной стороны, ::value как одна из частых конструкций должна была раскрываться автоматически, но должна была быть возможность (а) передать нераскрытое значение (см. проблему функции ветвления и бесконечной рекурсии выше под спойлером) и (б) использовать другие метаполя, кроме value. В итоге я остановился на «экранировании», записи метаполей через точку и ввёл обратный для экранирования оператор "!", раскрывающий ::value:

        Cons(x, xs)'.head; // x
        Cons(x, xs)'.tail; // xs
        Not(Zero);         // One
        Not(Zero)'!;       // One
        

        Впрочем, метаполе ::value может вполне сосуществовать со множественным возвратом. Реализуя отрицание списка negate на C++, мы создавали локальные метапеременные, которые вполне можно было возвратить. Здесь — аналогично, только все значения публичны:

        // отрицание непустого списка:
        // список из отрицаний элементов - это отрицание головы,
        // соединённое с хвостом отрицаний
        negate (list list) = Cons(not_head, not_tail)' {
          h = list.head; // голова
          not_head = Not(h); // отрицание головы
          t = list.tail; // хвост
          not_tail = negate(t); // отрицание хвоста
        } // точку с запятой можно опустить!
        
        // пустой список - сам себе отрицание
        negate (Nil) = Nil; 
        

        Функции высшего порядка


        Вспомним, что мы объявили типы унарной функции и списка и напишем map:

        // преобразование непустого списка
        map (unary f, list list) = Cons(new_head, new_tail)' {
          h = list.head; // голова
          new_head = f(h); // преобразованная голова
          t = list.tail; // хвост
          new_tail = map(f, t); // преобразованный хвост
        }
        
        // преобразование пустого списка
        map (unary f, Nil) = Nil;
        

        Разумеется, вместо unary и list можно было сразу указать val -> val и val соответственно, но запись оказалась бы более длинной и менее наглядной.

        Замыкания


        Поскольку замыкания поддерживаются самими шаблонами C++, здесь нет чего-то необычного:

        f(val x) = {
          g(val xs) = Cons(x, xs)';
        }
        
        // использование: f(x)'.g(list) или map(f(x)'.g, list)
        

        Но требуется давать имя (например, g) возвращаемой функции. Возвращаемая безымянная функция должна была бы иметь имя value и возвращать value. Одноимённый с классом член в C++ уже отдан конструктору, из-за чего задание ему нового смысла через typedef приводит к ошибке компиляции:

        template 
        struct f {
          template 
          struct value {               // value!
            typedef Cons value; // value!
          };
        };
        

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

        C++ также не позволяет переиспользовать имена параметров шаблона внутри шаблона. Поскольку эти имена в моём языке относятся к локальным метапеременным и за пределы шаблона не выходят, я автоматизировал их переименование:

        f (val x) = g(Not(x)) {
          g (val x) = x; // будет заменено на g (x1) = x1
          // т.к. x станет параметром template f и не допустит x как параметр template g
        }
        

        Такое преобразование, как мне кажется, не добавило «неспортивности» (из-за возможности тривиальной замены вручную), а жизнь несколько упростило.

        Выход во внешний мир


        На определённом этапе оказалось, что чисто функциональная часть более-менее успешно транслируется в C++, и надо как-то уметь хотя бы распечатать результат. Эта часть, выходила за рамки интересного мне чистого мира шаблонов (из-за чего в итоге продумана хуже) и, что более важно, достаточно хорошо описывалась обычным C++. В программах с компайл-тайм вычислениями на шаблонах за выход во внешний мир отвечает обычный C++ без шаблонов. То есть я должен был либо сделать мой язык надмножеством C++, либо создать свой аналог C++. Разумеется, я выбрал первый вариант.

        Оставалось только ввести тэги для вставки кода на C++ и удовлетворённо потирать руки. Но возникла логическая проблема: в исходниках нигде не упоминается, что для применения метафункций используется ::value, нигде не сказано, что f(x) — это f::value, а не call::result. Это знание хранилось внутри транслятора, и его использование в программе прорывало бы абстракцию:

        f(val x) = One;
        f(Zero) = Zero;
        
        main {
          print::value>(); // почему так?
        }
        

        Идей насчёт малоинтересной императивной части было немного, и после прикидывания нескольких вариантов я решил ввести (а) императивные блоки с возможностью использования в них обычного C++ и (б) экспорт в них значений из чистого функционального мира. Блок impure { код } может появляться вместо объявления значения/функции, а также справа после знака "=" при объявлении функции. В этих случаях код на C++ вставляется в соответствующее место программы. Для экспортирования выражения перед ним ставится ключевое слово impure. С точки зрения C++ это эквивалентно конструированию объекта типа, описываемого выражением.

        В качестве демонстрации работы impure приготовим «распечатыватель» списка:

        print(val list) {
          head = list.head;
          tail = list.tail;
          impure {
            // после typedef-ов head, tail расположится следующий блок кода:
            print() {
              impure print(head); // преобразуется в "print();"
              std::cout << ", ";
              impure print(tail);
            }
          }
        }
        
        print(Zero) = impure { // аналогично print(Zero) { impure { ..., но короче
          print() {
            std::cout << "0";
          }
        }
        
        print(One) = impure {
          print() {
            std::cout << "1";
          }
        }
        
        print(Nil) = impure {
          print() {
            std::cout << std::endl;
          }
        }
        

        Пространства имён


        Поскольку на моём языке в теории можно было бы создавать библиотеки, которые использовались бы в обычном C++, я «прокинул» в него namespace и using namespace:

        namespace ns1 {
          namespace ns2 {
            f(val x) = x;
          }
        }
        
        namespace ns3 {
          using namespace ns1.ns2;
          g(val x) = f(x);
        }
        

        using namespace также является вынужденной мерой. В С++ в некоторых случаях недостаточно записи вида f::y. Если f, x или z — не конкретные типы/шаблоны, а параметры шаблона, то ::y становится выходом чёрного ящика. Нужно указывать, что мы получаем при вычислении ::y — тип или шаблон (например, typename f::y или f::template y). Я не автоматизировал эти указания и не реализовал синтаксический сахар для более простого описания вручную, поэтому каждое использование точки вызывает появление «typename». Т.е. f::y оттранслируется во что-то некорректное вида typename typename f::value::typename y::value. В случае пространства имён это излишне, using namespace позволяет обойтись без вставки «typename».

        Лямбды


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

        f          (val x) =  y(x) { y(x) = g(x); } // было
        f = lambda (val x) -> y(x) { y(x) = g(x); } // стало
        

        Полноценная поддержка лямбд затрудняется тем, что в C++ (а) нельзя создать анонимный класс и (б) нельзя объявить нечто, эквивалентное шаблону, не раскрутив описание до эквивалентности типа типу. Т.е., выражаясь в математических терминах, $x = y$ написать можно, а вместо $g = f$ придётся использовать $g(x) = f(x)$. Пользователи императивного программирования уже привыкли к такому положению дел, но по меркам программирования функционального это довольно громоздко.

        template  struct f {}; // пусть была метафункция f
        
        using g = f; // нельзя описать эквивалентность шаблона шаблону
        
        // нужно описать эквивалентность выражений, соответствующих типам
        template  using g = f; // g - тип, f - тип
        

        Для реализации записей вида map(lambda(val x) -> y, xs) придётся модифицировать язык и генерировать шаблоны с временными именами.

        Как рассматривалось выше, value::value — это конструктор, поэтому не получается напрямую реализовать лямбду, которая возвращает лямбду. Для возврата функций из лямбд и разрешения записей вида g = f нужно использовать другие концепции и почти полностью переписать транслятор.

        Недостатки и возможные пути решения


        1. Нет сокрытия локальных переменных.
          Наивно решается вставкой impure { private: } в код или элементарной модификацией языка.
        2. При использовании метаполя автоматически подставляется «typename».
          Не знаю, можно ли избавиться от этого автоматически. В качестве варианта решения можно использовать обязательную венгерскую нотацию (скажем, fMap(fCurry(fCons).fApply(vXs), vObj.vYs) — при доступе к метаполю подставится template, typename или ничего, если первым символом имени будет «f», «v» или «n» соответственно).

          Второй путь — «динамическая типизация». То есть представление всех сущностей в виде структуры-обёртки с хранящимся типом и значением:

          // объявляем значение x
          struct x {
            typedef internal::value type; // тип значения x
          };
          
          // объявляем функцию f(x) = x
          struct f {
            typedef internal::function type; // тип функции f
            template 
            struct value {
              typedef typename x::type type; // тип результата функции f
              struct result { // обёртка для того, чтобы избежать value::value
                typedef x value;
              };
            };
          };
          

          Транслятор автоматически сгенерирует пространство имён internal со списком типов и метафункциями для работы с ними; за счёт дополнительной обёртки struct result можно будет избавиться от value::value и возвращать безымянные функции из функций. Также из-за того, что любое значение или функция будет выражаться в виде структуры (то есть просто типа, а не типа или шаблона), можно будет задавать синонимы функций (с помощью typedef), отменить обязательное описание типов аргументов функции, устранить ограничения на лямбды и возврат функций из ФВП.
        3. Есть ограничения на лямбды: они не могут возвращать функции и быть анонимными.
          Может помочь описанный выше подход.

          Также можно вместо value использовать два значения так, чтобы при увеличении глубины вложенности использовалось другое значение. Получится, что вместо value::value будет value1::value2 и никаких попыток переопределения конструктора не будет:

          template 
          struct f {
            typedef internal::True uses_value1; // использует value1
            template 
            struct value1 {
              typedef internal::False uses_value1; // использует value2
              typedef Cons value2;
            };
          };
          

          Вызвать такие метафункции можно будет с помощью отдельной метафункции, которая в зависимости от обязательного метаполя uses_value1 будет выбирать, доступаться до value1 или до value2.
        4. Не учитываются вариадические шаблоны и целые числа как параметры шаблонов.
          Для реализации требуется поддержка кроме val дополнительных типов для чисел (int, uint, ...) и массивов ([val], [int], [uint], ...). При использовании метаполей придётся решить описанную выше проблему с указанием template и typename, т.к. для чисел ничего не требуется указывать, а для типов и шаблонов — требуется.

        Пример программы


        В качестве примера построим список $\{1, 1, 0\}$, возьмём его отрицание двумя способами и распечатаем все эти значения в main:

        // строим списки:
        my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')';
        negated_list = negate(my_list);
        negated_list2 = map(Not, my_list);
        
        impure {
          int main() {
            // печатаем списки:
            std::cout << "my list is ";
            impure print(my_list);
            
            std::cout << "negated list is ";
            impure print(negated_list);
            
            std::cout << "negated list is also ";
            impure print(negated_list2);
          }
        }
        

        Программа выведет следующее:

        my list is 1, 1, 0,
        negated list is 0, 0, 1,
        negated list is also 0, 0, 1,
        

        Исходный код программы целиком
        impure {
          #include 
        }
        
        One;
        Zero;
        
        Not (val x) = Zero; // общий случай
        Not (Zero) = One; // частный случай
        
        And(val x, val y); // для x,y != One,Zero And не определена
        And(val x, One) = x; // One - константа, val x - аргумент
        And(val x, Zero) = Zero;
        
        #type number = val;
        #type list = val;
        #type unary = number -> number;
        #type map_t = (unary, list) -> list;
        
        Nil;
        
        Cons(val x, val y) {
          head = x;
          tail = y;
        }
        
        // отрицание непустого списка:
        negate (list list) = Cons(not_head, not_tail)' {
          h = list.head;
          not_head = Not(h);
          t = list.tail;
          not_tail = negate(t);
        }
        
        // пустой список - сам себе отрицание
        negate (Nil) = Nil; 
        
        // преобразование непустого списка
        map (unary f, list list) = Cons(new_head, new_tail)' {
          h = list.head;
          new_head = f(h);
          t = list.tail;
          new_tail = map(f, t);
        }
        
        // преобразование пустого списка
        map (unary f, Nil) = Nil;
        
        print(val list) {
          head = list.head;
          tail = list.tail;
          impure {
            print() {
              impure print(head);
              std::cout << ", ";
              impure print(tail);
            }
          }
        }
        
        print(Zero) = impure {
          print() {
            std::cout << "0";
          }
        }
        
        print(One) = impure {
          print() {
            std::cout << "1";
          }
        }
        
        print(Nil) = impure {
          print() {
            std::cout << std::endl;
          }
        }
        
        my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')';
        negated_list = negate(my_list);
        negated_list2 = map(Not, my_list);
        
        impure {
          int main() {
            std::cout << "my list is ";
            impure print(my_list);
            
            std::cout << "negated list is ";
            impure print(negated_list);
            
            std::cout << "negated list is also ";
            impure print(negated_list2);
          }
        }
        


        Код после трансляции в C++
        #include 
        struct One;
        struct Zero;
        template 
        struct Not {
          typedef Zero _value;
        };
        template <>
        struct Not {
          typedef One _value;
        };
        template 
        struct And;
        template 
        struct And {
          typedef x _value;
        };
        template 
        struct And {
          typedef Zero _value;
        };
        struct Nil;
        template 
        struct Cons {
          typedef x head;
          typedef y tail;
        };
        template 
        struct negate {
          typedef typename list::head h;
          typedef typename Not  ::_value not_head;
          typedef typename list::tail t;
          typedef typename negate  ::_value not_tail;
          typedef Cons   _value;
        };
        template <>
        struct negate {
          typedef Nil _value;
        };
        template 


        Транслятор


        Свой транслятор я написал на JavaScript (люблю этот язык, на нём мне легче думается). В качестве генератора парсеров использовал PEG.js.

        При запуске транслятор (см. страничку языка на GitHub) считывает файл, имя которого указано как параметр командной строки, и выдаёт в stdout результирующий текст программы на C++.

        node src/compile <исходник> # запуск

        Как только транслятор более-менее заработал и были написаны работающие программы, я разместил всё это добро на GitHub и, словно математик из анекдота, воскликнувший «Решение есть!», почти утратил интерес к развитию проекта. Порывался решить указанные выше проблемы чередованием/указанием типа/венгерской нотацией, но это требовало серьёзных изменений и повторного умственного напряжения. Думать, имея что-то готовое и работающее, было лень. Более того, мог нарушиться принцип наибольшего соответствия. Лишние обёртки и код по работе с ними убили бы красоту прямолинейности трансляции и приблизили бы программу к превышению лимита на вложенность шаблонов.

        Поэтому я остановился на достигнутом и буду рад, если кто-нибудь заинтересуется и, быть может, сделает свой вклад в развитие языка.
        Original source: habrahabr.ru (comments, light).

        https://habrahabr.ru/post/337590/


        Эзотерический язык, транслирующийся в шаблоны C++

        Понедельник, 11 Сентября 2017 г. 15:33 + в цитатник
        sekrasoft сегодня в 15:33 Разработка

        Эзотерический язык, транслирующийся в шаблоны C++

          КПДВ с примерами кода Шаблоны C++ — полный по Тьюрингу язык, на котором можно писать compile-time программы. Только вот синтаксис рассчитан на описание параметризованных типов и слабо приспособлен к ясному выражению чего-то более сложного. В этой статье рассмотрим, как типы и шаблоны становятся значениями и функциями, а также узнаем, к чему привела попытка автора создать свой функциональный язык, транслирующийся в шаблоны C++. Для прочтения текста знания в области функционального программирования почти не требуются.

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


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

          Честно говоря, пару лет назад я порывался написать сюда пост про объяснение принципов работы compile-time программ на примере алгоритма смены приоритетов операторов в выражениях (т.е. реализовал варианты для compile-time и run-time алгоритма, с помощью которого можно было выражение $2 + 2 * 2$ «пересобрать» с альтернативными приоритетами и посчитать как $8$, а не $6$).

          Я уже почти дописал длинную статью с картинками и UML-диаграммами, но вдруг понял, что на хабре настолько часто рассказывали про метапрограммы, что это уже попросту никому не интересно. Тем более, что для практического использования добавили constexpr, а мне хотелось остаться в области извращённых развлечений и использовать минимум возможностей языка. Замечательные статьи Жизнь во время компиляции от HurrTheDurr и Интерпретация во время компиляции, или Альтернативное понимание лямбд в C++11 от ilammy (вторая — более хардкорная и без using template с constexpr) описывали практически всё, что извращённому уму нужно было знать. В итоге я оставил статью в черновиках, но не оставил желания метапрограммировать. И сегодня я возвращаюсь с заново написанным текстом и новыми идеями.

          Если, несмотря на все мои старания, читателю всё ещё будет трудно воспринимать материал, рекомендую прочитать указанные статьи, а вдогонку — Мягкое введение в Haskell (часть 1, часть 2) от Пола Хьюдака и др. в переводе Дениса Москвина и Лямбда-исчисление на JavaScript от ibessonov. Всё это в какой-то степени повлияло на автора, может и на читателя повлияет?!

          Метазначения и метафункции


          Шаблоны классов — это, в некотором смысле, функции от типов, которые принимают и возвращают типы. Соответственно, шаблоны становятся метафункциями, а типы и целые числа — метазначениями. Например, метафункция std::vector принимает метазначение T и возвращает метазначение std::vector. Важно, что у метазначения есть набор метаполей (с точки зрения C++ — вложенные классы, псевдонимы типов, статические константные поля), с помощью которых можно сделать самое интересное.

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

          template 
          struct square {
            static const int value = x * x;
          };
          

          Вызывается применяется, как говорят в ФП, метафункция следующим образом: square<5>::value.

          Но целые числа — это обычные значения, для работы с которыми достаточно и простого C++, а потому было бы несколько неспортивно использовать их в метавыражениях. Настоящее честное метазначение — это тип. Самый простой способ создать его — объявить структуру:

          struct One;
          struct Zero;
          

          С точки зрения C++ one и zero только объявлены и практически бесполезны. Достаточно ли этого? Да. Чему же тогда они равны и как их использовать? Равны они, что важно, только самим себе. Это своего рода абстрактные символы, которые можно задействовать в символьных вычислениях (почти как в Mathematica и др.). Метапрограмма будет вычислять значения метавыражений различной сложности. Рассмотрим сначала эти выражения, а несколько позже займёмся интерпретацией символов и выводом результатов на экран.

          Для нуля и единицы как булевых значений естественным будет написание функций NOT, AND и OR. Рассмотрим метафункцию отрицания:

          template 
          struct Not {
            typedef Zero value;
          };
          
          template <>
          struct Not  {
            typedef One value;
          };
          

          Not принимает некоторое значение и возвращает единицу, если это значение — ноль. Во всех остальных случаях она возвратит ноль. Таким образом, за счёт специализации шаблонов, мы имеем паттерн-матчинг (сравнение с образцом) в зачаточной стадии: можем описывать отдельно поведение функции для одного или нескольких аргументов, имеющих конкретные значения, а компилятор C++, отметив соответствие параметров шаблона одному из образцов, подставит нужную специализацию. Пользуясь этим, мы могли бы уже написать что-то рекурсивное (например факториал, разделив описание на fac<0> и fac<всё остальное>).

          Список


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

          template 
          struct Cons {
            typedef h head;
            typedef t tail;
          };
          
          struct Nil;
          

          Cons в ФП — функция, конструирующая список из первого элемента (головы) и списка остальных элементов (хвост). Обычному списку $\{one,two,three\}$ будет соответствовать Cons>>. Поскольку для работы со списком нужно уметь получать его составные части, мы сделаем Cons многозначной функцией, возвращающей голову (Cons<...,...>::head), и хвост (Cons<...,...>::tail). Любители ООП могут представить, что Cons — это конструктор, а head и tail — геттеры. В ФП все присваивания заменяются на вызов применение функции с изменёнными аргументами, поэтому сеттеров и их аналогов здесь не будет.

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

          // отрицание непустого списка
          template 
          struct negate {
          private: // инкапсулируем промежуточные вычисления
            typedef typename list::head h; // голова
            typedef typename Not::value not_head; // отрицание головы
            typedef typename list::tail t; // хвост
            typedef typename negate::value not_tail; // отрицание хвоста
          public:
            // список из отрицаний элементов - это отрицание головы,
            // соединённое с хвостом отрицаний
            typedef Cons value;
          };
          
          // отрицание пустого списка
          template <>
          struct negate  {
            // пустой список - сам себе отрицание
            typedef Nil value;
          };
          

          Тут оказывается, что Haskell не так страшен, как его малюют. Всё то же самое выглядит настолько просто, что не грех добавить для наглядности примеры на этом языке. Для неподготовленного читателя отметим основное отличие от C++ и школьной математики: в Haskell аргументы разделяются пробелами и не группируются в скобках. Т.е. $f(x+y, g(y), z)$ будет записано в Haskell как f (x+y) (g y) z.

          -- список - либо слитые голова и хвост (причём хвост - сам список),
          --          либо пустота
          data List a = Cons a List | Nil
          
          -- хелперы для получения головы и хвоста
          head (Cons x _) = x -- возвращает голову
          tail (Cons _ xs) = xs -- возвращает хвост
          
          -- отрицание списка
          negate (Cons x xs) = Cons (Not x) (negate xs)
          negate Nil = Nil
          

          В отличие от строго типизированных Haskell и C++, в языке шаблонов действует утиная типизация. negate::value, конечно, не сработает, но если One будет иметь метаполя head и tail, он вполне подойдёт. Впрочем, пока negate не «разыменовали» с помощью ::value, программа продолжает работать компилироваться.

          Функции высшего порядка


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

          -- Преобразованная голова соединяется с преобразованным хвостом
          map f (Cons x xs) = Cons (f x) (map f xs)
          -- Пустой список преобразовывать не нужно
          map f Nil = Nil
          

          STL содержит такое преобразование под именем std::transform. В нашем же языке метафункция map объявляется с помощью параметризации шаблона шаблоном:

          // преобразование непустого списка
          // f - шаблон с одним аргументом - унарная метафункция
          template 

          В качестве f сюда можем подставить описанную ранее функцию Not и посчитать список отрицаний:

          typedef map>>>::value list;
          // list эквивалентно Cons>>
          

          Операции именования выражений


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

          Начиная с C++11 можно использовать псевдонимы типов и шаблонов, чтобы задавать значения и функции через известные выражения:

          using x = Not::value;
          
          template 
          using g = Cons;
          

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

          Программа просто может уйти в бесконечную рекурсию
          Вспомним, что negate компилируется, а negate::value — нет. Пользуясь громоздкими метаполями можно написать метафункцию ветвления, которая вычисляет ::value только для одной своей ветки в зависимости от условия и возвращает это значение. Получится, что одно из выражений никогда не вычислится: все операции выполняются только при получении ::value, а его никто не трогал:
          // рассмотрим только одну из специализаций
          template 
          struct If  {
            // expr1, expr2 вычислены, а expr1::value и expr2::value - нет
            typedef typename expr1::value value;
          }
          
          // If, destroy>::value не разрушит мир
          

          В то же время, вариант с псевдонимом шаблона предусматривает, что некоторое g имеет смысл уже вычисленного f::value. И если ветвиться между двумя рекурсивными вариантами, вычисления будут бесконечными — аналог переполнения стека на этапе компиляции.

          template 
          struct If  {
            // expr1, expr2 вычислены
            typedef expr1 value;
          };
          
          // If::value>::value разрушит мир
          


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

          Замыкания


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

          Например, f x возвращает унарную функцию g, неявно использующую аргумент x функции f.

          f x = g
            where g xs = Cons x xs
          
          -- использование: (f x) xs или, что полезнее, map (f x) list
          

          Можно было бы оставить и Cons, но g как унарную можно передать в уже написанную функцию map, а как бинарную — Cons нет!

          Впрочем, обычный C++ от отсутствия замыканий не страдает
          Для этого используют функциональный объект или лямбда-функцию. То есть если передача в функцию дополнительных параметров через глобальные переменные нарушает гибкость программы, вместо функции используют объект, this которого содержит весь нужный контекст, а operator () принимает требуемые интерфейсом аргументы. Красивая концепция из ФП заменяется эквивалентной по мощности концепцией ООП. Даже стандартный шаблон std::function предусмотрен!

          // до C++11: функциональный объект, захвативший переменную
          struct f {
            f(something& x) : x(x) {}
            something_else operator () (something_else& xs) { // xs - явный аргумент
              return Cons(x, xs);
            }
          private:
            something x; // неявный аргумент
          };
          
          // C++11 и выше: лямбда-функция, захватившая переменную
          something x;
          auto f = [x](something_else& xs) { return Cons(x, xs); };
          


          При замене функций (например, int(int)) на функциональные объекты (например, std::function) программист C++ получает полноценный аналог замыканий.

          На уровне шаблонов потребности в подмене «функция -> объект», как оказывается, нет. Замыкания поддерживаются самим языком:

          template 
          struct f {
            template 
            struct g {
              typedef Cons value;
            };
          };
          
          // использование: f::g::value или map::g, list>
          

          В отличие от C++ и Haskell, здесь наружу f «вылезает» символ g, но в остальном — честное замыкание. Выражение f::g может быть использовано вместо обычной унарной функции (например, Not).

          Неограниченное число аргументов или синтаксический сахар для списков


          Вариадические шаблоны позволяют записывать функции от списков. Опять же, в некоторых случаях это удобнее, но без них тоже можно спокойно жить с Cons и Nil. Кроме того, это может оказаться даже более простым ходом. Чтобы передать в метафункцию два списка, достаточно… передать два списка: f, для вариадических же шаблонов требуется задавать обёртку, захватывающую все списки, кроме последнего: f, y1, y2, y3>, так как список аргументов произвольной длины должен быть один, и несколько списков, записанные подряд, просто «слипнутся». По завершении вычислений приходится возвращать обёртку, т.к. в C++ нельзя затайпдефить список из нескольких типов. А чтобы «выковырить» список из обёртки и передать в метафункцию (скажем, f), придётся использовать метаколлбеки: реализовать у обёртки метаметод принимающий эту метафункцию f и передающий ей содержимое списка:

          typename 
          struct list_wrapper {
            // передаём в list_wrapper<...>::call функцию,
            // которой передадут наши аргументы xs
            struct call

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

          typename 
          struct negate {
            template 
            struct f {
              typedef list_wrapper::value, ys> value;
            };
            
            typedef typename negate::template call::value;
          };
          
          typename <>
          struct negate<> {
            typedef list_wrapper<> value;
          };
          

          Вышло так, что за красивую запись вида $\{x1, x2, x3\}$ при объявлении, пришлось пострадать, запаковывая, передавая и распаковывая эти последовательности. negate выглядит более громоздко даже по сравнению с версией для Cons/Nil. Здесь требуются и более серьёзные измышления, когда как для написания «обычной» версии достаточно основ ФП и механической замены Haskell -> C++. Поэтому с помощью вариадических шаблонов лучше написать обёртку для преобразования последовательности параметров в список Cons/Nil, а затем при реализации программы пользоваться уже им. Так мы сможем и задавать списки приятным перечислением через запятую, и мыслить более простыми категориями.

          Выход во внешний мир


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

          По завершении вычислений в метапрограмме, результирующее выражение интерпретируется как сущность из реального мира и, либо сохраняется в переменную, либо выводится на экран. Для вывода можно использовать шаблонные классы. В конструкторе или других методах располагается императивный код. Он может быть как внутри самих метазначений (например, void One::print();), либо — внутри метафункции, если возможностей паттерн-матчинга хватает для рассмотрения всех вариантов. Вот, например, метафункция print, распечатывающая свой аргумент (единицу, ноль или список) на этапе конструирования экземпляра print<...>:

          template 
          struct print {
            print () {
              std::cout << "unknown number" << std::endl;
            }
          };
          
          template <>
          struct print  {
            print () {
              std::cout << "1" << std::endl;
            }
          };
          
          template <>
          struct print  {
            print () {
              std::cout << "0" << std::endl;
            }
          };
          
          // print::value>() выведет "1"
          

          Аналогично, можно вывести список и всё, что угодно. Например, мы бы могли реализовать AND, NOT, XOR, двоичный сумматор, представить числа как списки из One и Zero и построить сложение, умножение,…

          До C++11 и появления decltype никак нельзя было провести чисто функциональные вычисления над типами, создать переменные и объекты C++ соответствующих типов, провести вычисления над ними, а потом снова вернуться к вычислениям над типами.

          sum, three> // тип - корректно
          sum, three>() // значение - компилируется
          
          do_something(sum(), three()) // значения - компилируется
          sum(), three()> // нельзя посчитать,
          // т.к. нет перехода от значений к типам
          
          sum()), decltype(three())> // C++11; компилируется
          

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

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

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

          typedef sum, three> x;
          typedef decltype(one() + two() + three()) x;
          

          Выход за скобки нарушает чистоту:

          auto v = one() + two() + three(); // v выполнится как императивная конструкция
          typedef decltype(v) x;
          

          Эзотерический язык, транслирующийся в шаблоны C++


          Основная проблема шаблонов C++ как ФЯ времени компиляции — излишняя многословность. Все эти ::value, скобочки и typename сильно выматывают программиста и растягивают код программы. Конечно, логика говорит, что решивший программировать в таком стиле должен страдать, но… это одно из тех извращённых развлечений, к которым зачастую склоняется мозг айтишника. Хочется попробовать сделать так, чтобы извращённость сохранилась, но в той степени, когда страдания ещё можно терпеть.



          В общем, решил я создать свой язык, который будет транслироваться в шаблоны C++. Сделать это можно разными способами. Использовать можно даже наиболее хитроумные преобразования наподобие тех, что делает транслятор JSFuck. Но такой подход (а) породит ещё один ненужный язык, (б) оторванный от C++, (в) который ещё нужно придумать и (г) потратить силы на реализацию. А разработать и реализовать достаточно мощный и ненужный ФЯ — дело хлопотное и бесполезное. Особенно, когда в шаблонах C++ уже есть конструкции, эквивалентные функциям, замыканиям, паттерн-матчингу… Да и не спортивно это.

          Я избрал путь наибольшего соответствия. Код на моём языке должен был выглядеть просто, но оставаться идейно похожим на C++. То есть преобразование должно было наиболее дословно переводить мой язык в C++. Он должен был стать килограммом синтаксического сахара над шаблонами.

          Давайте перепишем приведённые выше куски кода на созданный язык, таким образом рассмотрев его особенности и их применение в деле. Будем двигаться в том же порядке от определения значений «ноль» и «один», функции отрицания числа, конструкторов списка к определению функции отрицания списка, ФВП map и т.д.

          Значения и функции


          Для объявления значения не требуется ключевое слово struct:

          One;
          Zero;
          

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

          Not (val x) = Zero; // общий случай
          Not (Zero) = One; // частный случай
          
          And(val x, val y); // для x,y != One,Zero And не определена
          And(val x, One) = x; // One - константа, val x - аргумент
          And(val x, Zero) = Zero;
          
          // использование: Not(One) или And(One, Zero)
          

          В C++ параметром шаблона может быть тип (typename), другой шаблон (template class) и т.д., и это следует указывать. А значит, создавая метаФВП, одним typename не обойтись и требование указания типа переходит также в мой язык. По умолчанию задан тип val, соответствующий обычному метазначению (структура или обычный тип в C++). Для описания функций можно комбенировать типы с помощью стрелки (->). Например, val -> val — унарная функция (шаблон, принимающий один параметр), (val, val) -> val — бинарная функция (шаблон, принимающий два параметра) и так далее.

          Здесь я немного отступил от дословности транслирования и ввёл псевдонимы типов, аналога которым (псевдонимов видов) в C++ нет. С помощью #type можно задавать синонимы, чтобы ещё чуть-чуть сократить запись и прояснить смысл за счёт уместного именования:

          #type number = val;
          #type list = val;
          #type unary = number -> number;
          #type map_t = (unary, list) -> list;
          // map_t раскроется в template 

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

          Список


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

          Nil;
          
          Cons(val x, val y) {
            head = x;
            tail = y;
          }
          

          Применение функции к аргументам, транслируясь в C++, автоматически раскрывает ::value. То есть f(x) эквивалентно f::value. Но у Cons сгенерируется только два метаполя head и tail. Предотвращать раскрытие ::value требуется явно с помощью апострофа: Cons(x, xs)'.

          Я долго думал над этой проблемой. С одной стороны, ::value как одна из частых конструкций должна была раскрываться автоматически, но должна была быть возможность (а) передать нераскрытое значение (см. проблему функции ветвления и бесконечной рекурсии выше под спойлером) и (б) использовать другие метаполя, кроме value. В итоге я остановился на «экранировании», записи метаполей через точку и ввёл обратный для экранирования оператор "!", раскрывающий ::value:

          Cons(x, xs)'.head; // x
          Cons(x, xs)'.tail; // xs
          Not(Zero);         // One
          Not(Zero)'!;       // One
          

          Впрочем, метаполе ::value может вполне сосуществовать со множественным возвратом. Реализуя отрицание списка negate на C++, мы создавали локальные метапеременные, которые вполне можно было возвратить. Здесь — аналогично, только все значения публичны:

          // отрицание непустого списка:
          // список из отрицаний элементов - это отрицание головы,
          // соединённое с хвостом отрицаний
          negate (list list) = Cons(not_head, not_tail)' {
            h = list.head; // голова
            not_head = Not(h); // отрицание головы
            t = list.tail; // хвост
            not_tail = negate(t); // отрицание хвоста
          } // точку с запятой можно опустить!
          
          // пустой список - сам себе отрицание
          negate (Nil) = Nil; 
          

          Функции высшего порядка


          Вспомним, что мы объявили типы унарной функции и списка и напишем map:

          // преобразование непустого списка
          map (unary f, list list) = Cons(new_head, new_tail)' {
            h = list.head; // голова
            new_head = f(h); // преобразованная голова
            t = list.tail; // хвост
            new_tail = map(f, t); // преобразованный хвост
          }
          
          // преобразование пустого списка
          map (unary f, Nil) = Nil;
          

          Разумеется, вместо unary и list можно было сразу указать val -> val и val соответственно, но запись оказалась бы более длинной и менее наглядной.

          Замыкания


          Поскольку замыкания поддерживаются самими шаблонами C++, здесь нет чего-то необычного:

          f(val x) = {
            g(val xs) = Cons(x, xs)';
          }
          
          // использование: f(x)'.g(list) или map(f(x)'.g, list)
          

          Но требуется давать имя (например, g) возвращаемой функции. Возвращаемая безымянная функция должна была бы иметь имя value и возвращать value. Одноимённый с классом член в C++ уже отдан конструктору, из-за чего задание ему нового смысла через typedef приводит к ошибке компиляции:

          template 
          struct f {
            template 
            struct value {               // value!
              typedef Cons value; // value!
            };
          };
          

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

          C++ также не позволяет переиспользовать имена параметров шаблона внутри шаблона. Поскольку эти имена в моём языке относятся к локальным метапеременным и за пределы шаблона не выходят, я автоматизировал их переименование:

          f (val x) = g(Not(x)) {
            g (val x) = x; // будет заменено на g (x1) = x1
            // т.к. x станет параметром template f и не допустит x как параметр template g
          }
          

          Такое преобразование, как мне кажется, не добавило «неспортивности» (из-за возможности тривиальной замены вручную), а жизнь несколько упростило.

          Выход во внешний мир


          На определённом этапе оказалось, что чисто функциональная часть более-менее успешно транслируется в C++, и надо как-то уметь хотя бы распечатать результат. Эта часть, выходила за рамки интересного мне чистого мира шаблонов (из-за чего в итоге продумана хуже) и, что более важно, достаточно хорошо описывалась обычным C++. В программах с компайл-тайм вычислениями на шаблонах за выход во внешний мир отвечает обычный C++ без шаблонов. То есть я должен был либо сделать мой язык надмножеством C++, либо создать свой аналог C++. Разумеется, я выбрал первый вариант.

          Оставалось только ввести тэги для вставки кода на C++ и удовлетворённо потирать руки. Но возникла логическая проблема: в исходниках нигде не упоминается, что для применения метафункций используется ::value, нигде не сказано, что f(x) — это f::value, а не call::result. Это знание хранилось внутри транслятора, и его использование в программе прорывало бы абстракцию:

          f(val x) = One;
          f(Zero) = Zero;
          
          main {
            print::value>(); // почему так?
          }
          

          Идей насчёт малоинтересной императивной части было немного, и после прикидывания нескольких вариантов я решил ввести (а) императивные блоки с возможностью использования в них обычного C++ и (б) экспорт в них значений из чистого функционального мира. Блок impure { код } может появляться вместо объявления значения/функции, а также справа после знака "=" при объявлении функции. В этих случаях код на C++ вставляется в соответствующее место программы. Для экспортирования выражения перед ним ставится ключевое слово impure. С точки зрения C++ это эквивалентно конструированию объекта типа, описываемого выражением.

          В качестве демонстрации работы impure приготовим «распечатыватель» списка:

          print(val list) {
            head = list.head;
            tail = list.tail;
            impure {
              // после typedef-ов head, tail расположится следующий блок кода:
              print() {
                impure print(head); // преобразуется в "print();"
                std::cout << ", ";
                impure print(tail);
              }
            }
          }
          
          print(Zero) = impure { // аналогично print(Zero) { impure { ..., но короче
            print() {
              std::cout << "0";
            }
          }
          
          print(One) = impure {
            print() {
              std::cout << "1";
            }
          }
          
          print(Nil) = impure {
            print() {
              std::cout << std::endl;
            }
          }
          

          Пространства имён


          Поскольку на моём языке в теории можно было бы создавать библиотеки, которые использовались бы в обычном C++, я «прокинул» в него namespace и using namespace:

          namespace ns1 {
            namespace ns2 {
              f(val x) = x;
            }
          }
          
          namespace ns3 {
            using namespace ns1.ns2;
            g(val x) = f(x);
          }
          

          using namespace также является вынужденной мерой. В С++ в некоторых случаях недостаточно записи вида f::y. Если f, x или z — не конкретные типы/шаблоны, а параметры шаблона, то ::y становится выходом чёрного ящика. Нужно указывать, что мы получаем при вычислении ::y — тип или шаблон (например, typename f::y или f::template y). Я не автоматизировал эти указания и не реализовал синтаксический сахар для более простого описания вручную, поэтому каждое использование точки вызывает появление «typename». Т.е. f::y оттранслируется во что-то некорректное вида typename typename f::value::typename y::value. В случае пространства имён это излишне, using namespace позволяет обойтись без вставки «typename».

          Лямбды


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

          f          (val x) =  y(x) { y(x) = g(x); } // было
          f = lambda (val x) -> y(x) { y(x) = g(x); } // стало
          

          Полноценная поддержка лямбд затрудняется тем, что в C++ (а) нельзя создать анонимный класс и (б) нельзя объявить нечто, эквивалентное шаблону, не раскрутив описание до эквивалентности типа типу. Т.е., выражаясь в математических терминах, $x = y$ написать можно, а вместо $g = f$ придётся использовать $g(x) = f(x)$. Пользователи императивного программирования уже привыкли к такому положению дел, но по меркам программирования функционального это довольно громоздко.

          template  struct f {}; // пусть была метафункция f
          
          using g = f; // нельзя описать эквивалентность шаблона шаблону
          
          // нужно описать эквивалентность выражений, соответствующих типам
          template  using g = f; // g - тип, f - тип
          

          Для реализации записей вида map(lambda(val x) -> y, xs) придётся модифицировать язык и генерировать шаблоны с временными именами.

          Как рассматривалось выше, value::value — это конструктор, поэтому не получается напрямую реализовать лямбду, которая возвращает лямбду. Для возврата функций из лямбд и разрешения записей вида g = f нужно использовать другие концепции и почти полностью переписать транслятор.

          Недостатки и возможные пути решения


          1. Нет сокрытия локальных переменных.
            Наивно решается вставкой impure { private: } в код или элементарной модификацией языка.
          2. При использовании метаполя автоматически подставляется «typename».
            Не знаю, можно ли избавиться от этого автоматически. В качестве варианта решения можно использовать обязательную венгерскую нотацию (скажем, fMap(fCurry(fCons).fApply(vXs), vObj.vYs) — при доступе к метаполю подставится template, typename или ничего, если первым символом имени будет «f», «v» или «n» соответственно).

            Второй путь — «динамическая типизация». То есть представление всех сущностей в виде структуры-обёртки с хранящимся типом и значением:

            // объявляем значение x
            struct x {
              typedef internal::value type; // тип значения x
            };
            
            // объявляем функцию f(x) = x
            struct f {
              typedef internal::function type; // тип функции f
              template 
              struct value {
                typedef typename x::type type; // тип результата функции f
                struct result { // обёртка для того, чтобы избежать value::value
                  typedef x value;
                };
              };
            };
            

            Транслятор автоматически сгенерирует пространство имён internal со списком типов и метафункциями для работы с ними; за счёт дополнительной обёртки struct result можно будет избавиться от value::value и возвращать безымянные функции из функций. Также из-за того, что любое значение или функция будет выражаться в виде структуры (то есть просто типа, а не типа или шаблона), можно будет задавать синонимы функций (с помощью typedef), отменить обязательное описание типов аргументов функции, устранить ограничения на лямбды и возврат функций из ФВП.
          3. Есть ограничения на лямбды: они не могут возвращать функции и быть анонимными.
            Может помочь описанный выше подход.

            Также можно вместо value использовать два значения так, чтобы при увеличении глубины вложенности использовалось другое значение. Получится, что вместо value::value будет value1::value2 и никаких попыток переопределения конструктора не будет:

            template 
            struct f {
              typedef internal::True uses_value1; // использует value1
              template 
              struct value1 {
                typedef internal::False uses_value1; // использует value2
                typedef Cons value2;
              };
            };
            

            Вызвать такие метафункции можно будет с помощью отдельной метафункции, которая в зависимости от обязательного метаполя uses_value1 будет выбирать, доступаться до value1 или до value2.
          4. Не учитываются вариадические шаблоны и целые числа как параметры шаблонов.
            Для реализации требуется поддержка кроме val дополнительных типов для чисел (int, uint, ...) и массивов ([val], [int], [uint], ...). При использовании метаполей придётся решить описанную выше проблему с указанием template и typename, т.к. для чисел ничего не требуется указывать, а для типов и шаблонов — требуется.

          Пример программы


          В качестве примера построим список $\{1, 1, 0\}$, возьмём его отрицание двумя способами и распечатаем все эти значения в main:

          // строим списки:
          my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')';
          negated_list = negate(my_list);
          negated_list2 = map(Not, my_list);
          
          impure {
            int main() {
              // печатаем списки:
              std::cout << "my list is ";
              impure print(my_list);
              
              std::cout << "negated list is ";
              impure print(negated_list);
              
              std::cout << "negated list is also ";
              impure print(negated_list2);
            }
          }
          

          Программа выведет следующее:

          my list is 1, 1, 0,
          negated list is 0, 0, 1,
          negated list is also 0, 0, 1,
          

          Исходный код программы целиком
          impure {
            #include 
          }
          
          One;
          Zero;
          
          Not (val x) = Zero; // общий случай
          Not (Zero) = One; // частный случай
          
          And(val x, val y); // для x,y != One,Zero And не определена
          And(val x, One) = x; // One - константа, val x - аргумент
          And(val x, Zero) = Zero;
          
          #type number = val;
          #type list = val;
          #type unary = number -> number;
          #type map_t = (unary, list) -> list;
          
          Nil;
          
          Cons(val x, val y) {
            head = x;
            tail = y;
          }
          
          // отрицание непустого списка:
          negate (list list) = Cons(not_head, not_tail)' {
            h = list.head;
            not_head = Not(h);
            t = list.tail;
            not_tail = negate(t);
          }
          
          // пустой список - сам себе отрицание
          negate (Nil) = Nil; 
          
          // преобразование непустого списка
          map (unary f, list list) = Cons(new_head, new_tail)' {
            h = list.head;
            new_head = f(h);
            t = list.tail;
            new_tail = map(f, t);
          }
          
          // преобразование пустого списка
          map (unary f, Nil) = Nil;
          
          print(val list) {
            head = list.head;
            tail = list.tail;
            impure {
              print() {
                impure print(head);
                std::cout << ", ";
                impure print(tail);
              }
            }
          }
          
          print(Zero) = impure {
            print() {
              std::cout << "0";
            }
          }
          
          print(One) = impure {
            print() {
              std::cout << "1";
            }
          }
          
          print(Nil) = impure {
            print() {
              std::cout << std::endl;
            }
          }
          
          my_list = Cons(One, Cons(One, Cons(Zero, Nil)')')';
          negated_list = negate(my_list);
          negated_list2 = map(Not, my_list);
          
          impure {
            int main() {
              std::cout << "my list is ";
              impure print(my_list);
              
              std::cout << "negated list is ";
              impure print(negated_list);
              
              std::cout << "negated list is also ";
              impure print(negated_list2);
            }
          }
          


          Код после трансляции в C++
          #include 
          struct One;
          struct Zero;
          template 
          struct Not {
            typedef Zero _value;
          };
          template <>
          struct Not {
            typedef One _value;
          };
          template 
          struct And;
          template 
          struct And {
            typedef x _value;
          };
          template 
          struct And {
            typedef Zero _value;
          };
          struct Nil;
          template 
          struct Cons {
            typedef x head;
            typedef y tail;
          };
          template 
          struct negate {
            typedef typename list::head h;
            typedef typename Not  ::_value not_head;
            typedef typename list::tail t;
            typedef typename negate  ::_value not_tail;
            typedef Cons   _value;
          };
          template <>
          struct negate {
            typedef Nil _value;
          };
          template 


          Транслятор


          Свой транслятор я написал на JavaScript (люблю этот язык, на нём мне легче думается). В качестве генератора парсеров использовал PEG.js.

          При запуске транслятор (см. страничку языка на GitHub) считывает файл, имя которого указано как параметр командной строки, и выдаёт в stdout результирующий текст программы на C++.

          node src/compile <исходник> # запуск

          Как только транслятор более-менее заработал и были написаны работающие программы, я разместил всё это добро на GitHub и, словно математик из анекдота, воскликнувший «Решение есть!», почти утратил интерес к развитию проекта. Порывался решить указанные выше проблемы чередованием/указанием типа/венгерской нотацией, но это требовало серьёзных изменений и повторного умственного напряжения. Думать, имея что-то готовое и работающее, было лень. Более того, мог нарушиться принцип наибольшего соответствия. Лишние обёртки и код по работе с ними убили бы красоту прямолинейности трансляции и приблизили бы программу к превышению лимита на вложенность шаблонов.

          Поэтому я остановился на достигнутом и буду рад, если кто-нибудь заинтересуется и, быть может, сделает свой вклад в развитие языка.
          Original source: habrahabr.ru (comments, light).

          https://habrahabr.ru/post/337590/


          [Перевод] JavaScript: методы асинхронного программирования

          Понедельник, 11 Сентября 2017 г. 15:20 + в цитатник
          ru_vds сегодня в 15:20 Разработка

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

          • Перевод
          Синхронный код на JavaScript, автор которого не стремился сбить с толку тех, кто этот код будет читать, обычно выглядит просто и понятно. Команды, из которых он состоит, выполняются в том порядке, в котором они следуют в тексте программы. Немного путаницы может внести поднятие объявлений переменных и функций, но чтобы превратить эту особенность JS в проблему, надо очень постараться. У синхронного кода на JavaScript есть лишь один серьёзный недостаток: на нём одном далеко не уехать.



          Практически каждая полезная JS-программа написана с привлечением асинхронных методов разработки. Здесь в дело вступают функции обратного вызова, в просторечии — «коллбэки». Здесь в ходу «обещания», или Promise-объекты, называемые обычно промисами. Тут можно столкнуться с генераторами и с конструкциями async/await. Асинхронный код, в сравнении с синхронным, обычно сложнее писать, читать и поддерживать. Иногда он превращается в совершенно жуткие структуры вроде ада коллбэков. Однако, без него не обойтись.

          Сегодня предлагаем поговорить об особенностях коллбэков, промисов, генераторов и конструкций async/await, и подумать о том, как писать простой, понятный и эффективный асинхронный код.

          О синхронном и асинхронном коде


          Начнём с рассмотрения фрагментов синхронного и асинхронного JS-кода. Вот, например, обычный синхронный код:

          console.log('1')
          console.log('2')
          console.log('3')

          Он, без особых сложностей, выводит в консоль числа от 1 до 3.

          Теперь — код асинхронный:

          console.log('1')
          setTimeout(function afterTwoSeconds() {
            console.log('2')
          }, 2000)
          console.log('3')

          Тут уже будет выведена последовательность 1, 3, 2. Число 2 выводится из коллбэка, который обрабатывает событие срабатывания таймера, заданного при вызове функции setTimeout. Коллбэк будет вызвана, в данном примере, через 2 секунды. Приложение при этом не остановится, ожидая, пока истекут эти две секунды. Вместо этого его исполнение продолжится, а когда сработает таймер, будет вызвана функция afterTwoSeconds.

          Возможно, если вы только начинаете путь JS-разработчика, вы зададитесь вопросами: «Зачем это всё? Может быть, можно переделать асинхронный код в синхронный?». Поищем ответы на эти вопросы.

          Постановка задачи


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

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


          Простой интерфейс поиска пользователей GitHub и соответствующих им репозиториев

          В примерах выполнение запросов будет выполнено средствами XMLHttpRequest (XHR), но вы вполне можете использовать тут jQuery ($.ajax), или более современный стандартный подход, основанный на использовании функции fetch. И то и другое сводится к использованию промисов. Код, в зависимости от похода, будет меняться, но вот, для начала, такой пример:

          // аргумент url может быть чем-то вроде 'https://api.github.com/users/daspinola/repos'
          function request(url) {
            const xhr = new XMLHttpRequest();
            xhr.timeout = 2000;
            xhr.onreadystatechange = function(e) {
              if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                 // Код обработки успешного завершения запроса
                } else {
                 // Обрабатываем ответ с сообщением об ошибке
                }
              }
            }
            xhr.ontimeout = function () {
              // Ожидание ответа заняло слишком много времени, тут будет код, который обрабатывает подобную ситуацию
            }
            xhr.open('get', url, true)
            xhr.send();
          }

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

          Функции обратного вызова


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

          // Вызовем функцию "doThis" с другой функцией в качестве параметра, в данном случае - это функция "andThenThis". Функция "doThis" исполнит код, находящийся в ней, после чего, в нужный момент, вызовет функцию "andThenThis".
          doThis(andThenThis)
          // Внутри "doThis" обращение к переданной ей функции осуществляется через параметр "callback" , фактически, это просто переменная, которая хранит ссылку на функцию
          function andThenThis() {
            console.log('and then this')
          }
          // Назвать параметр, в котором окажется функция обратного вызова, можно как угодно, "callback" - это просто распространённый вариант
          function doThis(callback) {
            console.log('this first')
            
            // Для того, чтобы функция, ссылка на которую хранится в переменной, была вызвана, нужно поместить после имени переменной скобки, '()', иначе ничего не получится
            callback()
          }

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

          function request(url, callback) {
            const xhr = new XMLHttpRequest();
            xhr.timeout = 2000;
            xhr.onreadystatechange = function(e) {
              if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                 callback(null, xhr.response)
                } else {
                 callback(xhr.status, null)
                }
              }
            }
            xhr.ontimeout = function () {
             console.log('Timeout')
            }
            xhr.open('get', url, true)
            xhr.send();
          }

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

          const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
          request(userGet, function handleUsersList(error, users) {
            if (error) throw error
            const list = JSON.parse(users).items
            list.forEach(function(user) {
              request(user.repos_url, function handleReposList(err, repos) {
                if (err) throw err
                //Здесь обработаем список репозиториев
              })
            })
          })

          Разберём то, что здесь происходит:

          • Выполняется запрос для получения репозиториев пользователя (в данном случае я загружаю собственные репозитории);
          • После завершения запроса вызывается коллбэк handleUsersList;
          • Если не было ошибок, разбираем ответ сервера c помощью JSON.parse, преобразовываем его, для удобства, в объект;
          • После этого перебираем список пользователей, так как в нём может быть больше одного элемента, и для каждого из них запрашиваем список репозиториев, используя URL, возвращённый для каждого пользователя после выполнения первого запроса. Подразумевается, что repos_url — это URL для наших следующих запросов, и получили мы его из первого запроса.
          • Когда запрос, направленный на загрузку данных о репозиториях, завершён, вызывается коллбэк, теперь это handleReposList. Здесь, так же как и при загрузке списка пользователей, можно обработать ошибки или полезные данные, в которых содержится список репозиториев пользователя.

          Обратите внимание на то, что использование в качестве первого параметра объекта ошибки — это широко распространённая практика, в частности, для разработки с использованием Node.js.

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

          try {
            request(userGet, handleUsersList)
          } catch (e) {
            console.error('Request boom! ', e)
          }
          function handleUsersList(error, users) {
            if (error) throw error
            const list = JSON.parse(users).items
            list.forEach(function(user) {
              request(user.repos_url, handleReposList)
            })
          }
          function handleReposList(err, repos) {
            if (err) throw err
            
            // Здесь обрабатываем список репозиториев
            console.log('My very few repos', repos)
          }

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


          Ад коллбэков во всей красе. Изображение взято отсюда.

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

          Промисы


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

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

          const myPromise = new Promise(function(resolve, reject) {
            
            // Здесь будет код
            
            if (codeIsFine) {
              resolve('fine')
            } else {
              reject('error')
            }
          })
          myPromise
            .then(function whenOk(response) {
              console.log(response)
              return response
            })
            .catch(function notOk(err) {
              console.error(err)
            })

          Разберём этот пример:

          • Промис инициализируется с помощью функции, в которой есть вызовы методов resolve и reject;
          • Асинхронный код помещают внутри функции, созданной с помощью конструктора Promise. Если код будет выполнен успешно, вызывают метод resolve, если нет — reject;
          • Если функция вызовет resolve, будет исполнен метод .then для объекта Promise, аналогично, если будет вызван reject, будет исполнен метод .catch.

          Вот что стоит помнить, работая с промисами:

          • Методы resolve и reject принимают только один параметр, в результате, например, при выполнении команды вида resolve('yey', 'works'), коллбэку .then будет передано лишь 'yey';
          • Если объединить в цепочку несколько вызовов .then, в конце соответствующих коллбэков следует всегда использовать return, иначе все они будут выполнены одновременно, а это, очевидно, не то, чего вы хотите достичь;
          • При выполнении команды reject, если следующим в цепочке идёт .then, он будет выполнен (вы можете считать .then выражением, которое выполняется в любом случае);
          • Если в цепочке из вызовов .then в каком-то из них возникнет ошибка, следующие за ним будут пропущены до тех пор, пока не будет найдено выражение .catch;
          • У промисов есть три состояния: «pending» — состояние ожидания вызова resolve или reject, а также состояния «resolved» и «rejected», которые соответствуют успешному, с вызовом resolve, и неуспешному, с вызовом reject, завершению работы промиса. Когда промис оказывается в состоянии «resolved» или «rejected», оно уже не может быть изменено.

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

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

          function request(url) {
            return new Promise(function (resolve, reject) {
              const xhr = new XMLHttpRequest();
              xhr.timeout = 2000;
              xhr.onreadystatechange = function(e) {
                if (xhr.readyState === 4) {
                  if (xhr.status === 200) {
                    resolve(xhr.response)
                  } else {
                    reject(xhr.status)
                  }
                }
              }
              xhr.ontimeout = function () {
                reject('timeout')
              }
              xhr.open('get', url, true)
              xhr.send();
            })
          }

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


          Это — промис в состоянии ожидания. Он может быть либо успешно разрешён, либо отклонён

          Теперь, воспользовавшись новой функцией request, перепишем остальной код.

          const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
          const myPromise = request(userGet)
          console.log('will be pending when logged', myPromise)
          myPromise
            .then(function handleUsersList(users) {
              console.log('when resolve is found it comes here with the response, in this case users ', users)
              const list = JSON.parse(users).items
              return Promise.all(list.map(function(user) {
                return request(user.repos_url)
              }))
            })
            .then(function handleReposList(repos) {
              console.log('All users repos in an array', repos)
            })
            .catch(function handleErrors(error) {
              console.log('when a reject is executed it will come here ignoring the then statement ', error)
            })

          Здесь мы оказываемся в первом выражении .then при успешном разрешении промиса. У нас имеется список пользователей. Во второе выражение .then мы передаём массив с репозиториями. Если что-то пошло не так, мы окажемся в выражении .catch.

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

          const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
          const userRequest = request(userGet)
          // Если просто прочитать эту часть программы вслух, можно сразу понять что именно делает код
          userRequest
            .then(handleUsersList)
            .then(repoRequest)
            .then(handleReposList)
            .catch(handleErrors)
          function handleUsersList(users) {
            return JSON.parse(users).items
          }
          function repoRequest(users) {
            return Promise.all(users.map(function(user) {
              return request(user.repos_url)
            }))
          }
          function handleReposList(repos) {
            console.log('All users repos in an array', repos)
          }
          function handleErrors(error) {
            console.error('Something went wrong ', error)
          }

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

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

          Генераторы


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

          Для того, чтобы определить функцию-генератор, можно воспользоваться знаком звёздочки, «*», после ключевого слова function. С помощью генераторов асинхронный код можно сделать очень похожим на синхронный. Например, выглядеть это может так:

          function* foo() {
            yield 1
            const args = yield 2
            console.log(args)
          }
          var fooIterator = foo()
          console.log(fooIterator.next().value) // выведет 1
          console.log(fooIterator.next().value) // выведет 2
          fooIterator.next('aParam') // приведёт к вызову console.log внутри генератора и к выводу 'aParam'

          Дело тут в том, что генераторы, вместо return, используют выражение yield, которое останавливает выполнение функции до следующего вызова .next итератора. Это похоже на выражение .then в промисах, которое выполняется при разрешении промиса.

          Посмотрим теперь, как это всё применить к нашей задаче. Итак, вот функция request:

          function request(url) {
            return function(callback) {
              const xhr = new XMLHttpRequest();
              xhr.onreadystatechange = function(e) {
                if (xhr.readyState === 4) {
                  if (xhr.status === 200) {
                    callback(null, xhr.response)
                  } else {
                    callback(xhr.status, null)
                  }
                }
              }
              xhr.ontimeout = function () {
                console.log('timeout')
              }
              xhr.open('get', url, true)
              xhr.send()
            }
          }

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

          Генератор будет выглядеть так:

          function* list() {
            const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
           
            const users = yield request(userGet)
            
            yield
            
            for (let i = 0; i<=users.length; i++) {
              yield request(users[i].repos_url)
            }
          }

          Вот что здесь происходит:

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

          Использование этого всего будет выглядеть так:

          try {
            const iterator = list()
            iterator.next().value(function handleUsersList(err, users) {
              if (err) throw err
              const list = JSON.parse(users).items
              
              // Отправляем список пользователей итератору
              iterator.next(list)
              
              list.forEach(function(user) {
                iterator.next().value(function userRepos(error, repos) {
                  if (error) throw repos
                  // Здесь обрабатываем информацию о репозиториях каждого пользователя
                  console.log(user, JSON.parse(repos))
                })
              })
            })  
          } catch (e) {
            console.error(e)
          }

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

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

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

          Async/await


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

          sumTwentyAfterTwoSeconds(10)
            .then(result => console.log('after 2 seconds', result))
          async function sumTwentyAfterTwoSeconds(value) {
            const remainder = afterTwoSeconds(20)
            return value + await remainder
          }
          function afterTwoSeconds(value) {
            return new Promise(resolve => {
              setTimeout(() => { resolve(value) }, 2000);
            });
          }

          Здесь происходит следующее:

          • Имеется асинхронная функция sumTwentyAfterTwoSeconds;
          • Мы предлагаем коду подождать разрешения промиса afterTwoSeconds, который может завершиться вызовом resolve или reject;
          • Выполнение кода заканчивается в .then, где завершается операция, отмеченная ключевым словом await, в данном случае — это всего одна операция.

          Подготовим функцию request к использовании в конструкции async/await:

          function request(url) {
            return new Promise(function(resolve, reject) {
              const xhr = new XMLHttpRequest();
              xhr.onreadystatechange = function(e) {
                if (xhr.readyState === 4) {
                  if (xhr.status === 200) {
                    resolve(xhr.response)
                  } else {
                    reject(xhr.status)
                  }
                }
              }
              xhr.ontimeout = function () {
                reject('timeout')
              }
              xhr.open('get', url, true)
              xhr.send()
            })
          }

          Теперь создаём функцию с ключевым словом async, в которой используем ключевое слово await:

          async function list() {
            const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
            
            const users = await request(userGet)
            const usersList = JSON.parse(users).items
            
            usersList.forEach(async function (user) {
              const repos = await request(user.repos_url)
              
              handleRepoList(user, repos)
            })
          }
          function handleRepoList(user, repos) {
            const userRepos = JSON.parse(repos)
            
            // Обрабатываем тут репозитории для каждого пользователя
            console.log(user, userRepos)
          }

          Итак, у нас имеется асинхронная функция list, которая обработает запрос. Ещё конструкция async/await нам понадобится в цикле forEach, чтобы сформировать список репозиториев. Вызвать всё это очень просто:

          list()
            .catch(e => console.error(e))

          Этот подход и использование промисов — мои любимые методы асинхронного программирования. Код, написанный с их использованием, удобно и читать и править. Подробности об async/await можно почитать здесь.

          Минус async/await, как и минус генераторов, заключается в том, что эту конструкцию не поддерживают старые браузеры, а для её использования в серверной разработке нужно пользоваться Node 8. В подобной ситуации, опять же, поможет транспилятор, например — babel.

          Итоги


          Здесь можно посмотреть код проекта, который решает поставленную в начале материала задачу с использованием async/await. Если вы хотите как следует разобраться с тем, о чём мы говорили — поэкспериментируйте с этим кодом и со всеми рассмотренными технологиями.

          Обратите внимание на то, что наши примеры можно улучшить, сделать лаконичнее, если переписать их с использованием альтернативных способов выполнения запросов, вроде $.ajax и fetch. Если у вас есть идеи о том, как улучшить качество кода при использовании вышеописанных методик — буду благодарен, если расскажете об этом мне.

          В зависимости от особенностей поставленной перед вами задачи, может оказаться так, что вы будете пользоваться async/await, коллбэками, или некоей смесью из разных технологий. На самом деле, ответ на вопрос о том, какую именно методику асинхронной разработки выбрать, зависит от особенностей проекта. Если некий подход позволяет решить задачу с помощью читабельного кода, который легко поддерживать, который понятен (и будет понятен через некоторое время) вам и другим членам команды, значит этот подход — то, что вам нужно.

          Уважаемые читатели! Какими методиками написания асинхронного кода на JavaScript вы пользуетесь?
          Original source: habrahabr.ru (comments, light).

          https://habrahabr.ru/post/337662/


          Метки:  

          [Перевод] JavaScript: методы асинхронного программирования

          Понедельник, 11 Сентября 2017 г. 15:20 + в цитатник
          ru_vds сегодня в 15:20 Разработка

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

          • Перевод
          Синхронный код на JavaScript, автор которого не стремился сбить с толку тех, кто этот код будет читать, обычно выглядит просто и понятно. Команды, из которых он состоит, выполняются в том порядке, в котором они следуют в тексте программы. Немного путаницы может внести поднятие объявлений переменных и функций, но чтобы превратить эту особенность JS в проблему, надо очень постараться. У синхронного кода на JavaScript есть лишь один серьёзный недостаток: на нём одном далеко не уехать.



          Практически каждая полезная JS-программа написана с привлечением асинхронных методов разработки. Здесь в дело вступают функции обратного вызова, в просторечии — «коллбэки». Здесь в ходу «обещания», или Promise-объекты, называемые обычно промисами. Тут можно столкнуться с генераторами и с конструкциями async/await. Асинхронный код, в сравнении с синхронным, обычно сложнее писать, читать и поддерживать. Иногда он превращается в совершенно жуткие структуры вроде ада коллбэков. Однако, без него не обойтись.

          Сегодня предлагаем поговорить об особенностях коллбэков, промисов, генераторов и конструкций async/await, и подумать о том, как писать простой, понятный и эффективный асинхронный код.

          О синхронном и асинхронном коде


          Начнём с рассмотрения фрагментов синхронного и асинхронного JS-кода. Вот, например, обычный синхронный код:

          console.log('1')
          console.log('2')
          console.log('3')

          Он, без особых сложностей, выводит в консоль числа от 1 до 3.

          Теперь — код асинхронный:

          console.log('1')
          setTimeout(function afterTwoSeconds() {
            console.log('2')
          }, 2000)
          console.log('3')

          Тут уже будет выведена последовательность 1, 3, 2. Число 2 выводится из коллбэка, который обрабатывает событие срабатывания таймера, заданного при вызове функции setTimeout. Коллбэк будет вызвана, в данном примере, через 2 секунды. Приложение при этом не остановится, ожидая, пока истекут эти две секунды. Вместо этого его исполнение продолжится, а когда сработает таймер, будет вызвана функция afterTwoSeconds.

          Возможно, если вы только начинаете путь JS-разработчика, вы зададитесь вопросами: «Зачем это всё? Может быть, можно переделать асинхронный код в синхронный?». Поищем ответы на эти вопросы.

          Постановка задачи


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

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


          Простой интерфейс поиска пользователей GitHub и соответствующих им репозиториев

          В примерах выполнение запросов будет выполнено средствами XMLHttpRequest (XHR), но вы вполне можете использовать тут jQuery ($.ajax), или более современный стандартный подход, основанный на использовании функции fetch. И то и другое сводится к использованию промисов. Код, в зависимости от похода, будет меняться, но вот, для начала, такой пример:

          // аргумент url может быть чем-то вроде 'https://api.github.com/users/daspinola/repos'
          function request(url) {
            const xhr = new XMLHttpRequest();
            xhr.timeout = 2000;
            xhr.onreadystatechange = function(e) {
              if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                 // Код обработки успешного завершения запроса
                } else {
                 // Обрабатываем ответ с сообщением об ошибке
                }
              }
            }
            xhr.ontimeout = function () {
              // Ожидание ответа заняло слишком много времени, тут будет код, который обрабатывает подобную ситуацию
            }
            xhr.open('get', url, true)
            xhr.send();
          }

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

          Функции обратного вызова


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

          // Вызовем функцию "doThis" с другой функцией в качестве параметра, в данном случае - это функция "andThenThis". Функция "doThis" исполнит код, находящийся в ней, после чего, в нужный момент, вызовет функцию "andThenThis".
          doThis(andThenThis)
          // Внутри "doThis" обращение к переданной ей функции осуществляется через параметр "callback" , фактически, это просто переменная, которая хранит ссылку на функцию
          function andThenThis() {
            console.log('and then this')
          }
          // Назвать параметр, в котором окажется функция обратного вызова, можно как угодно, "callback" - это просто распространённый вариант
          function doThis(callback) {
            console.log('this first')
            
            // Для того, чтобы функция, ссылка на которую хранится в переменной, была вызвана, нужно поместить после имени переменной скобки, '()', иначе ничего не получится
            callback()
          }

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

          function request(url, callback) {
            const xhr = new XMLHttpRequest();
            xhr.timeout = 2000;
            xhr.onreadystatechange = function(e) {
              if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                 callback(null, xhr.response)
                } else {
                 callback(xhr.status, null)
                }
              }
            }
            xhr.ontimeout = function () {
             console.log('Timeout')
            }
            xhr.open('get', url, true)
            xhr.send();
          }

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

          const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
          request(userGet, function handleUsersList(error, users) {
            if (error) throw error
            const list = JSON.parse(users).items
            list.forEach(function(user) {
              request(user.repos_url, function handleReposList(err, repos) {
                if (err) throw err
                //Здесь обработаем список репозиториев
              })
            })
          })

          Разберём то, что здесь происходит:

          • Выполняется запрос для получения репозиториев пользователя (в данном случае я загружаю собственные репозитории);
          • После завершения запроса вызывается коллбэк handleUsersList;
          • Если не было ошибок, разбираем ответ сервера c помощью JSON.parse, преобразовываем его, для удобства, в объект;
          • После этого перебираем список пользователей, так как в нём может быть больше одного элемента, и для каждого из них запрашиваем список репозиториев, используя URL, возвращённый для каждого пользователя после выполнения первого запроса. Подразумевается, что repos_url — это URL для наших следующих запросов, и получили мы его из первого запроса.
          • Когда запрос, направленный на загрузку данных о репозиториях, завершён, вызывается коллбэк, теперь это handleReposList. Здесь, так же как и при загрузке списка пользователей, можно обработать ошибки или полезные данные, в которых содержится список репозиториев пользователя.

          Обратите внимание на то, что использование в качестве первого параметра объекта ошибки — это широко распространённая практика, в частности, для разработки с использованием Node.js.

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

          try {
            request(userGet, handleUsersList)
          } catch (e) {
            console.error('Request boom! ', e)
          }
          function handleUsersList(error, users) {
            if (error) throw error
            const list = JSON.parse(users).items
            list.forEach(function(user) {
              request(user.repos_url, handleReposList)
            })
          }
          function handleReposList(err, repos) {
            if (err) throw err
            
            // Здесь обрабатываем список репозиториев
            console.log('My very few repos', repos)
          }

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


          Ад коллбэков во всей красе. Изображение взято отсюда.

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

          Промисы


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

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

          const myPromise = new Promise(function(resolve, reject) {
            
            // Здесь будет код
            
            if (codeIsFine) {
              resolve('fine')
            } else {
              reject('error')
            }
          })
          myPromise
            .then(function whenOk(response) {
              console.log(response)
              return response
            })
            .catch(function notOk(err) {
              console.error(err)
            })

          Разберём этот пример:

          • Промис инициализируется с помощью функции, в которой есть вызовы методов resolve и reject;
          • Асинхронный код помещают внутри функции, созданной с помощью конструктора Promise. Если код будет выполнен успешно, вызывают метод resolve, если нет — reject;
          • Если функция вызовет resolve, будет исполнен метод .then для объекта Promise, аналогично, если будет вызван reject, будет исполнен метод .catch.

          Вот что стоит помнить, работая с промисами:

          • Методы resolve и reject принимают только один параметр, в результате, например, при выполнении команды вида resolve('yey', 'works'), коллбэку .then будет передано лишь 'yey';
          • Если объединить в цепочку несколько вызовов .then, в конце соответствующих коллбэков следует всегда использовать return, иначе все они будут выполнены одновременно, а это, очевидно, не то, чего вы хотите достичь;
          • При выполнении команды reject, если следующим в цепочке идёт .then, он будет выполнен (вы можете считать .then выражением, которое выполняется в любом случае);
          • Если в цепочке из вызовов .then в каком-то из них возникнет ошибка, следующие за ним будут пропущены до тех пор, пока не будет найдено выражение .catch;
          • У промисов есть три состояния: «pending» — состояние ожидания вызова resolve или reject, а также состояния «resolved» и «rejected», которые соответствуют успешному, с вызовом resolve, и неуспешному, с вызовом reject, завершению работы промиса. Когда промис оказывается в состоянии «resolved» или «rejected», оно уже не может быть изменено.

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

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

          function request(url) {
            return new Promise(function (resolve, reject) {
              const xhr = new XMLHttpRequest();
              xhr.timeout = 2000;
              xhr.onreadystatechange = function(e) {
                if (xhr.readyState === 4) {
                  if (xhr.status === 200) {
                    resolve(xhr.response)
                  } else {
                    reject(xhr.status)
                  }
                }
              }
              xhr.ontimeout = function () {
                reject('timeout')
              }
              xhr.open('get', url, true)
              xhr.send();
            })
          }

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


          Это — промис в состоянии ожидания. Он может быть либо успешно разрешён, либо отклонён

          Теперь, воспользовавшись новой функцией request, перепишем остальной код.

          const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
          const myPromise = request(userGet)
          console.log('will be pending when logged', myPromise)
          myPromise
            .then(function handleUsersList(users) {
              console.log('when resolve is found it comes here with the response, in this case users ', users)
              const list = JSON.parse(users).items
              return Promise.all(list.map(function(user) {
                return request(user.repos_url)
              }))
            })
            .then(function handleReposList(repos) {
              console.log('All users repos in an array', repos)
            })
            .catch(function handleErrors(error) {
              console.log('when a reject is executed it will come here ignoring the then statement ', error)
            })

          Здесь мы оказываемся в первом выражении .then при успешном разрешении промиса. У нас имеется список пользователей. Во второе выражение .then мы передаём массив с репозиториями. Если что-то пошло не так, мы окажемся в выражении .catch.

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

          const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
          const userRequest = request(userGet)
          // Если просто прочитать эту часть программы вслух, можно сразу понять что именно делает код
          userRequest
            .then(handleUsersList)
            .then(repoRequest)
            .then(handleReposList)
            .catch(handleErrors)
          function handleUsersList(users) {
            return JSON.parse(users).items
          }
          function repoRequest(users) {
            return Promise.all(users.map(function(user) {
              return request(user.repos_url)
            }))
          }
          function handleReposList(repos) {
            console.log('All users repos in an array', repos)
          }
          function handleErrors(error) {
            console.error('Something went wrong ', error)
          }

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

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

          Генераторы


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

          Для того, чтобы определить функцию-генератор, можно воспользоваться знаком звёздочки, «*», после ключевого слова function. С помощью генераторов асинхронный код можно сделать очень похожим на синхронный. Например, выглядеть это может так:

          function* foo() {
            yield 1
            const args = yield 2
            console.log(args)
          }
          var fooIterator = foo()
          console.log(fooIterator.next().value) // выведет 1
          console.log(fooIterator.next().value) // выведет 2
          fooIterator.next('aParam') // приведёт к вызову console.log внутри генератора и к выводу 'aParam'

          Дело тут в том, что генераторы, вместо return, используют выражение yield, которое останавливает выполнение функции до следующего вызова .next итератора. Это похоже на выражение .then в промисах, которое выполняется при разрешении промиса.

          Посмотрим теперь, как это всё применить к нашей задаче. Итак, вот функция request:

          function request(url) {
            return function(callback) {
              const xhr = new XMLHttpRequest();
              xhr.onreadystatechange = function(e) {
                if (xhr.readyState === 4) {
                  if (xhr.status === 200) {
                    callback(null, xhr.response)
                  } else {
                    callback(xhr.status, null)
                  }
                }
              }
              xhr.ontimeout = function () {
                console.log('timeout')
              }
              xhr.open('get', url, true)
              xhr.send()
            }
          }

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

          Генератор будет выглядеть так:

          function* list() {
            const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
           
            const users = yield request(userGet)
            
            yield
            
            for (let i = 0; i<=users.length; i++) {
              yield request(users[i].repos_url)
            }
          }

          Вот что здесь происходит:

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

          Использование этого всего будет выглядеть так:

          try {
            const iterator = list()
            iterator.next().value(function handleUsersList(err, users) {
              if (err) throw err
              const list = JSON.parse(users).items
              
              // Отправляем список пользователей итератору
              iterator.next(list)
              
              list.forEach(function(user) {
                iterator.next().value(function userRepos(error, repos) {
                  if (error) throw repos
                  // Здесь обрабатываем информацию о репозиториях каждого пользователя
                  console.log(user, JSON.parse(repos))
                })
              })
            })  
          } catch (e) {
            console.error(e)
          }

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

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

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

          Async/await


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

          sumTwentyAfterTwoSeconds(10)
            .then(result => console.log('after 2 seconds', result))
          async function sumTwentyAfterTwoSeconds(value) {
            const remainder = afterTwoSeconds(20)
            return value + await remainder
          }
          function afterTwoSeconds(value) {
            return new Promise(resolve => {
              setTimeout(() => { resolve(value) }, 2000);
            });
          }

          Здесь происходит следующее:

          • Имеется асинхронная функция sumTwentyAfterTwoSeconds;
          • Мы предлагаем коду подождать разрешения промиса afterTwoSeconds, который может завершиться вызовом resolve или reject;
          • Выполнение кода заканчивается в .then, где завершается операция, отмеченная ключевым словом await, в данном случае — это всего одна операция.

          Подготовим функцию request к использовании в конструкции async/await:

          function request(url) {
            return new Promise(function(resolve, reject) {
              const xhr = new XMLHttpRequest();
              xhr.onreadystatechange = function(e) {
                if (xhr.readyState === 4) {
                  if (xhr.status === 200) {
                    resolve(xhr.response)
                  } else {
                    reject(xhr.status)
                  }
                }
              }
              xhr.ontimeout = function () {
                reject('timeout')
              }
              xhr.open('get', url, true)
              xhr.send()
            })
          }

          Теперь создаём функцию с ключевым словом async, в которой используем ключевое слово await:

          async function list() {
            const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
            
            const users = await request(userGet)
            const usersList = JSON.parse(users).items
            
            usersList.forEach(async function (user) {
              const repos = await request(user.repos_url)
              
              handleRepoList(user, repos)
            })
          }
          function handleRepoList(user, repos) {
            const userRepos = JSON.parse(repos)
            
            // Обрабатываем тут репозитории для каждого пользователя
            console.log(user, userRepos)
          }

          Итак, у нас имеется асинхронная функция list, которая обработает запрос. Ещё конструкция async/await нам понадобится в цикле forEach, чтобы сформировать список репозиториев. Вызвать всё это очень просто:

          list()
            .catch(e => console.error(e))

          Этот подход и использование промисов — мои любимые методы асинхронного программирования. Код, написанный с их использованием, удобно и читать и править. Подробности об async/await можно почитать здесь.

          Минус async/await, как и минус генераторов, заключается в том, что эту конструкцию не поддерживают старые браузеры, а для её использования в серверной разработке нужно пользоваться Node 8. В подобной ситуации, опять же, поможет транспилятор, например — babel.

          Итоги


          Здесь можно посмотреть код проекта, который решает поставленную в начале материала задачу с использованием async/await. Если вы хотите как следует разобраться с тем, о чём мы говорили — поэкспериментируйте с этим кодом и со всеми рассмотренными технологиями.

          Обратите внимание на то, что наши примеры можно улучшить, сделать лаконичнее, если переписать их с использованием альтернативных способов выполнения запросов, вроде $.ajax и fetch. Если у вас есть идеи о том, как улучшить качество кода при использовании вышеописанных методик — буду благодарен, если расскажете об этом мне.

          В зависимости от особенностей поставленной перед вами задачи, может оказаться так, что вы будете пользоваться async/await, коллбэками, или некоей смесью из разных технологий. На самом деле, ответ на вопрос о том, какую именно методику асинхронной разработки выбрать, зависит от особенностей проекта. Если некий подход позволяет решить задачу с помощью читабельного кода, который легко поддерживать, который понятен (и будет понятен через некоторое время) вам и другим членам команды, значит этот подход — то, что вам нужно.

          Уважаемые читатели! Какими методиками написания асинхронного кода на JavaScript вы пользуетесь?
          Original source: habrahabr.ru (comments, light).

          https://habrahabr.ru/post/337662/


          Метки:  

          [Перевод] JavaScript: методы асинхронного программирования

          Понедельник, 11 Сентября 2017 г. 15:20 + в цитатник
          ru_vds сегодня в 15:20 Разработка

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

          • Перевод
          Синхронный код на JavaScript, автор которого не стремился сбить с толку тех, кто этот код будет читать, обычно выглядит просто и понятно. Команды, из которых он состоит, выполняются в том порядке, в котором они следуют в тексте программы. Немного путаницы может внести поднятие объявлений переменных и функций, но чтобы превратить эту особенность JS в проблему, надо очень постараться. У синхронного кода на JavaScript есть лишь один серьёзный недостаток: на нём одном далеко не уехать.



          Практически каждая полезная JS-программа написана с привлечением асинхронных методов разработки. Здесь в дело вступают функции обратного вызова, в просторечии — «коллбэки». Здесь в ходу «обещания», или Promise-объекты, называемые обычно промисами. Тут можно столкнуться с генераторами и с конструкциями async/await. Асинхронный код, в сравнении с синхронным, обычно сложнее писать, читать и поддерживать. Иногда он превращается в совершенно жуткие структуры вроде ада коллбэков. Однако, без него не обойтись.

          Сегодня предлагаем поговорить об особенностях коллбэков, промисов, генераторов и конструкций async/await, и подумать о том, как писать простой, понятный и эффективный асинхронный код.

          О синхронном и асинхронном коде


          Начнём с рассмотрения фрагментов синхронного и асинхронного JS-кода. Вот, например, обычный синхронный код:

          console.log('1')
          console.log('2')
          console.log('3')

          Он, без особых сложностей, выводит в консоль числа от 1 до 3.

          Теперь — код асинхронный:

          console.log('1')
          setTimeout(function afterTwoSeconds() {
            console.log('2')
          }, 2000)
          console.log('3')

          Тут уже будет выведена последовательность 1, 3, 2. Число 2 выводится из коллбэка, который обрабатывает событие срабатывания таймера, заданного при вызове функции setTimeout. Коллбэк будет вызвана, в данном примере, через 2 секунды. Приложение при этом не остановится, ожидая, пока истекут эти две секунды. Вместо этого его исполнение продолжится, а когда сработает таймер, будет вызвана функция afterTwoSeconds.

          Возможно, если вы только начинаете путь JS-разработчика, вы зададитесь вопросами: «Зачем это всё? Может быть, можно переделать асинхронный код в синхронный?». Поищем ответы на эти вопросы.

          Постановка задачи


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

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


          Простой интерфейс поиска пользователей GitHub и соответствующих им репозиториев

          В примерах выполнение запросов будет выполнено средствами XMLHttpRequest (XHR), но вы вполне можете использовать тут jQuery ($.ajax), или более современный стандартный подход, основанный на использовании функции fetch. И то и другое сводится к использованию промисов. Код, в зависимости от похода, будет меняться, но вот, для начала, такой пример:

          // аргумент url может быть чем-то вроде 'https://api.github.com/users/daspinola/repos'
          function request(url) {
            const xhr = new XMLHttpRequest();
            xhr.timeout = 2000;
            xhr.onreadystatechange = function(e) {
              if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                 // Код обработки успешного завершения запроса
                } else {
                 // Обрабатываем ответ с сообщением об ошибке
                }
              }
            }
            xhr.ontimeout = function () {
              // Ожидание ответа заняло слишком много времени, тут будет код, который обрабатывает подобную ситуацию
            }
            xhr.open('get', url, true)
            xhr.send();
          }

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

          Функции обратного вызова


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

          // Вызовем функцию "doThis" с другой функцией в качестве параметра, в данном случае - это функция "andThenThis". Функция "doThis" исполнит код, находящийся в ней, после чего, в нужный момент, вызовет функцию "andThenThis".
          doThis(andThenThis)
          // Внутри "doThis" обращение к переданной ей функции осуществляется через параметр "callback" , фактически, это просто переменная, которая хранит ссылку на функцию
          function andThenThis() {
            console.log('and then this')
          }
          // Назвать параметр, в котором окажется функция обратного вызова, можно как угодно, "callback" - это просто распространённый вариант
          function doThis(callback) {
            console.log('this first')
            
            // Для того, чтобы функция, ссылка на которую хранится в переменной, была вызвана, нужно поместить после имени переменной скобки, '()', иначе ничего не получится
            callback()
          }

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

          function request(url, callback) {
            const xhr = new XMLHttpRequest();
            xhr.timeout = 2000;
            xhr.onreadystatechange = function(e) {
              if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                 callback(null, xhr.response)
                } else {
                 callback(xhr.status, null)
                }
              }
            }
            xhr.ontimeout = function () {
             console.log('Timeout')
            }
            xhr.open('get', url, true)
            xhr.send();
          }

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

          const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
          request(userGet, function handleUsersList(error, users) {
            if (error) throw error
            const list = JSON.parse(users).items
            list.forEach(function(user) {
              request(user.repos_url, function handleReposList(err, repos) {
                if (err) throw err
                //Здесь обработаем список репозиториев
              })
            })
          })

          Разберём то, что здесь происходит:

          • Выполняется запрос для получения репозиториев пользователя (в данном случае я загружаю собственные репозитории);
          • После завершения запроса вызывается коллбэк handleUsersList;
          • Если не было ошибок, разбираем ответ сервера c помощью JSON.parse, преобразовываем его, для удобства, в объект;
          • После этого перебираем список пользователей, так как в нём может быть больше одного элемента, и для каждого из них запрашиваем список репозиториев, используя URL, возвращённый для каждого пользователя после выполнения первого запроса. Подразумевается, что repos_url — это URL для наших следующих запросов, и получили мы его из первого запроса.
          • Когда запрос, направленный на загрузку данных о репозиториях, завершён, вызывается коллбэк, теперь это handleReposList. Здесь, так же как и при загрузке списка пользователей, можно обработать ошибки или полезные данные, в которых содержится список репозиториев пользователя.

          Обратите внимание на то, что использование в качестве первого параметра объекта ошибки — это широко распространённая практика, в частности, для разработки с использованием Node.js.

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

          try {
            request(userGet, handleUsersList)
          } catch (e) {
            console.error('Request boom! ', e)
          }
          function handleUsersList(error, users) {
            if (error) throw error
            const list = JSON.parse(users).items
            list.forEach(function(user) {
              request(user.repos_url, handleReposList)
            })
          }
          function handleReposList(err, repos) {
            if (err) throw err
            
            // Здесь обрабатываем список репозиториев
            console.log('My very few repos', repos)
          }

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


          Ад коллбэков во всей красе. Изображение взято отсюда.

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

          Промисы


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

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

          const myPromise = new Promise(function(resolve, reject) {
            
            // Здесь будет код
            
            if (codeIsFine) {
              resolve('fine')
            } else {
              reject('error')
            }
          })
          myPromise
            .then(function whenOk(response) {
              console.log(response)
              return response
            })
            .catch(function notOk(err) {
              console.error(err)
            })

          Разберём этот пример:

          • Промис инициализируется с помощью функции, в которой есть вызовы методов resolve и reject;
          • Асинхронный код помещают внутри функции, созданной с помощью конструктора Promise. Если код будет выполнен успешно, вызывают метод resolve, если нет — reject;
          • Если функция вызовет resolve, будет исполнен метод .then для объекта Promise, аналогично, если будет вызван reject, будет исполнен метод .catch.

          Вот что стоит помнить, работая с промисами:

          • Методы resolve и reject принимают только один параметр, в результате, например, при выполнении команды вида resolve('yey', 'works'), коллбэку .then будет передано лишь 'yey';
          • Если объединить в цепочку несколько вызовов .then, в конце соответствующих коллбэков следует всегда использовать return, иначе все они будут выполнены одновременно, а это, очевидно, не то, чего вы хотите достичь;
          • При выполнении команды reject, если следующим в цепочке идёт .then, он будет выполнен (вы можете считать .then выражением, которое выполняется в любом случае);
          • Если в цепочке из вызовов .then в каком-то из них возникнет ошибка, следующие за ним будут пропущены до тех пор, пока не будет найдено выражение .catch;
          • У промисов есть три состояния: «pending» — состояние ожидания вызова resolve или reject, а также состояния «resolved» и «rejected», которые соответствуют успешному, с вызовом resolve, и неуспешному, с вызовом reject, завершению работы промиса. Когда промис оказывается в состоянии «resolved» или «rejected», оно уже не может быть изменено.

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

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

          function request(url) {
            return new Promise(function (resolve, reject) {
              const xhr = new XMLHttpRequest();
              xhr.timeout = 2000;
              xhr.onreadystatechange = function(e) {
                if (xhr.readyState === 4) {
                  if (xhr.status === 200) {
                    resolve(xhr.response)
                  } else {
                    reject(xhr.status)
                  }
                }
              }
              xhr.ontimeout = function () {
                reject('timeout')
              }
              xhr.open('get', url, true)
              xhr.send();
            })
          }

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


          Это — промис в состоянии ожидания. Он может быть либо успешно разрешён, либо отклонён

          Теперь, воспользовавшись новой функцией request, перепишем остальной код.

          const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
          const myPromise = request(userGet)
          console.log('will be pending when logged', myPromise)
          myPromise
            .then(function handleUsersList(users) {
              console.log('when resolve is found it comes here with the response, in this case users ', users)
              const list = JSON.parse(users).items
              return Promise.all(list.map(function(user) {
                return request(user.repos_url)
              }))
            })
            .then(function handleReposList(repos) {
              console.log('All users repos in an array', repos)
            })
            .catch(function handleErrors(error) {
              console.log('when a reject is executed it will come here ignoring the then statement ', error)
            })

          Здесь мы оказываемся в первом выражении .then при успешном разрешении промиса. У нас имеется список пользователей. Во второе выражение .then мы передаём массив с репозиториями. Если что-то пошло не так, мы окажемся в выражении .catch.

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

          const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
          const userRequest = request(userGet)
          // Если просто прочитать эту часть программы вслух, можно сразу понять что именно делает код
          userRequest
            .then(handleUsersList)
            .then(repoRequest)
            .then(handleReposList)
            .catch(handleErrors)
          function handleUsersList(users) {
            return JSON.parse(users).items
          }
          function repoRequest(users) {
            return Promise.all(users.map(function(user) {
              return request(user.repos_url)
            }))
          }
          function handleReposList(repos) {
            console.log('All users repos in an array', repos)
          }
          function handleErrors(error) {
            console.error('Something went wrong ', error)
          }

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

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

          Генераторы


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

          Для того, чтобы определить функцию-генератор, можно воспользоваться знаком звёздочки, «*», после ключевого слова function. С помощью генераторов асинхронный код можно сделать очень похожим на синхронный. Например, выглядеть это может так:

          function* foo() {
            yield 1
            const args = yield 2
            console.log(args)
          }
          var fooIterator = foo()
          console.log(fooIterator.next().value) // выведет 1
          console.log(fooIterator.next().value) // выведет 2
          fooIterator.next('aParam') // приведёт к вызову console.log внутри генератора и к выводу 'aParam'

          Дело тут в том, что генераторы, вместо return, используют выражение yield, которое останавливает выполнение функции до следующего вызова .next итератора. Это похоже на выражение .then в промисах, которое выполняется при разрешении промиса.

          Посмотрим теперь, как это всё применить к нашей задаче. Итак, вот функция request:

          function request(url) {
            return function(callback) {
              const xhr = new XMLHttpRequest();
              xhr.onreadystatechange = function(e) {
                if (xhr.readyState === 4) {
                  if (xhr.status === 200) {
                    callback(null, xhr.response)
                  } else {
                    callback(xhr.status, null)
                  }
                }
              }
              xhr.ontimeout = function () {
                console.log('timeout')
              }
              xhr.open('get', url, true)
              xhr.send()
            }
          }

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

          Генератор будет выглядеть так:

          function* list() {
            const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
           
            const users = yield request(userGet)
            
            yield
            
            for (let i = 0; i<=users.length; i++) {
              yield request(users[i].repos_url)
            }
          }

          Вот что здесь происходит:

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

          Использование этого всего будет выглядеть так:

          try {
            const iterator = list()
            iterator.next().value(function handleUsersList(err, users) {
              if (err) throw err
              const list = JSON.parse(users).items
              
              // Отправляем список пользователей итератору
              iterator.next(list)
              
              list.forEach(function(user) {
                iterator.next().value(function userRepos(error, repos) {
                  if (error) throw repos
                  // Здесь обрабатываем информацию о репозиториях каждого пользователя
                  console.log(user, JSON.parse(repos))
                })
              })
            })  
          } catch (e) {
            console.error(e)
          }

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

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

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

          Async/await


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

          sumTwentyAfterTwoSeconds(10)
            .then(result => console.log('after 2 seconds', result))
          async function sumTwentyAfterTwoSeconds(value) {
            const remainder = afterTwoSeconds(20)
            return value + await remainder
          }
          function afterTwoSeconds(value) {
            return new Promise(resolve => {
              setTimeout(() => { resolve(value) }, 2000);
            });
          }

          Здесь происходит следующее:

          • Имеется асинхронная функция sumTwentyAfterTwoSeconds;
          • Мы предлагаем коду подождать разрешения промиса afterTwoSeconds, который может завершиться вызовом resolve или reject;
          • Выполнение кода заканчивается в .then, где завершается операция, отмеченная ключевым словом await, в данном случае — это всего одна операция.

          Подготовим функцию request к использовании в конструкции async/await:

          function request(url) {
            return new Promise(function(resolve, reject) {
              const xhr = new XMLHttpRequest();
              xhr.onreadystatechange = function(e) {
                if (xhr.readyState === 4) {
                  if (xhr.status === 200) {
                    resolve(xhr.response)
                  } else {
                    reject(xhr.status)
                  }
                }
              }
              xhr.ontimeout = function () {
                reject('timeout')
              }
              xhr.open('get', url, true)
              xhr.send()
            })
          }

          Теперь создаём функцию с ключевым словом async, в которой используем ключевое слово await:

          async function list() {
            const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
            
            const users = await request(userGet)
            const usersList = JSON.parse(users).items
            
            usersList.forEach(async function (user) {
              const repos = await request(user.repos_url)
              
              handleRepoList(user, repos)
            })
          }
          function handleRepoList(user, repos) {
            const userRepos = JSON.parse(repos)
            
            // Обрабатываем тут репозитории для каждого пользователя
            console.log(user, userRepos)
          }

          Итак, у нас имеется асинхронная функция list, которая обработает запрос. Ещё конструкция async/await нам понадобится в цикле forEach, чтобы сформировать список репозиториев. Вызвать всё это очень просто:

          list()
            .catch(e => console.error(e))

          Этот подход и использование промисов — мои любимые методы асинхронного программирования. Код, написанный с их использованием, удобно и читать и править. Подробности об async/await можно почитать здесь.

          Минус async/await, как и минус генераторов, заключается в том, что эту конструкцию не поддерживают старые браузеры, а для её использования в серверной разработке нужно пользоваться Node 8. В подобной ситуации, опять же, поможет транспилятор, например — babel.

          Итоги


          Здесь можно посмотреть код проекта, который решает поставленную в начале материала задачу с использованием async/await. Если вы хотите как следует разобраться с тем, о чём мы говорили — поэкспериментируйте с этим кодом и со всеми рассмотренными технологиями.

          Обратите внимание на то, что наши примеры можно улучшить, сделать лаконичнее, если переписать их с использованием альтернативных способов выполнения запросов, вроде $.ajax и fetch. Если у вас есть идеи о том, как улучшить качество кода при использовании вышеописанных методик — буду благодарен, если расскажете об этом мне.

          В зависимости от особенностей поставленной перед вами задачи, может оказаться так, что вы будете пользоваться async/await, коллбэками, или некоей смесью из разных технологий. На самом деле, ответ на вопрос о том, какую именно методику асинхронной разработки выбрать, зависит от особенностей проекта. Если некий подход позволяет решить задачу с помощью читабельного кода, который легко поддерживать, который понятен (и будет понятен через некоторое время) вам и другим членам команды, значит этот подход — то, что вам нужно.

          Уважаемые читатели! Какими методиками написания асинхронного кода на JavaScript вы пользуетесь?
          Original source: habrahabr.ru (comments, light).

          https://habrahabr.ru/post/337662/


          Метки:  

          Метавычисления и глубокие свёрточные сети: интервью с профессором ИТМО

          Понедельник, 11 Сентября 2017 г. 15:16 + в цитатник
          ARG89 сегодня в 15:16 Разработка

          Метавычисления и глубокие свёрточные сети: интервью с профессором ИТМО



            После победы AlphaGo в марте 2016 года над одним из сильнейших игроков Go в мире Ли Седолем о методах глубокого обучения заговорили практически везде. И даже Google не упустил случая назвать себя компанией машинного обучения и искусственного интеллекта.

            Что стоит за термином «глубокое обучение»? Какими бывают модели машинного обучения и на чём они пишутся? Ответить на эти и многие другие вопросы, связанные с МО и, в частности, с глубоким обучением (deep learning), мы попросили Алексея Потапова, профессора кафедры компьютерной фотоники и видеоинформатики ИТМО.

            Алексей Потапов
            Профессор кафедры компьютерной фотоники и видеоинформатики ИТМО. Победитель конкурса грантов для молодых научно-педагогических работников вузов Санкт-Петербурга. Победитель конкурса грантов Президента Российской Федерации для государственной поддержки молодых российских ученых — докторов наук в области «Информационно-телекоммуникационных систем и технологий».



            — Сегодня мы практически отовсюду слышим о буме машинного обучения и особенно глубокого обучения. Как и в какой ситуации это может понадобиться простому IT-шнику?

            Алексей Потапов: Честно говоря, у меня в голове нет образа «простого IT-шника», так что мне непросто ответить на этот вопрос. Можно ли считать «простым IT-шником» того, кто сможет применить методы глубокого обучения для автоматизации своей деятельности? Не уверен. Наверное, это всё же будет «сложный IT-шник». Тогда простому особо вникать, наверное, и не надо — за исключением того, чтобы поддерживать актуальную картину мира и в качестве демотиватора: рано или поздно «роботы отберут работу» не только у сотрудников Макдональдса, но и у IT-шников.

            — А если у человека есть техническое образование, и он решил погрузиться в тему — с чего ему лучше начать и куда пойти?

            Алексей Потапов: Здесь очень сильно зависит от того, насколько глубоко и с какой целью человек решил погрузиться в тему, а также от уровня его подготовки. Одно дело — использовать имеющиеся предобученные сети или обучать известные модели с реализацией на высокоуровневых библиотеках, таких как Keras. Это может делать и не самый продвинутый школьник, не особо вникая в математику. Достаточно изучить документацию соответствующей библиотеки и на примерах научиться комбинировать разные слои, активационные функции и т.д. — работать как с «чёрными ящиками».

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

            Если человеку не подходит такой путь или он хочет копнуть глубже, он может начать с поверхностной теории — с описания базовых моделей (автоэнкодеров, машин Больцмана, свёрточных сетей, рекуррентных блоков — tanh-RNN, LSTM, GRU и т.д.) AS-IS. Кому-то, может, придётся начать с логистической регрессии и перцептронов. Несложно обо всём этом почитать в интернете. На этом уровне погружения можно пользоваться более низкоуровневыми библиотеками (такими как Tensorflow и Theano) и реализовывать модели не из готовых блоков, а на уровне операций с тензорами.

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

            Вот, например, несколько неплохих курсов:
            Introduction into Deep Learning
            Neural Networks and Deep Learning
            Convolutional Neural Networks for Visual Recognition

            — В докладе Вы рассматриваете дискриминантные и генеративные модели, можно ли на простом примере объяснить, в чём между ними разница и для чего они нужны?

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

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

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

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

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

            — Почему глубокое обучение и вероятностный подход выстрелили именно сейчас, а не 20-30 лет назад?

            Алексей Потапов: Если отвечать максимально упрощённо, то дело, очевидно, в вычислительных ресурсах, особенно в развитии GPGPU, а также в доступности данных для обучения. Всё, конечно, чуть сложнее, и это не единственная причина, но всё же основная. Конечно, дело и в развитии представлений исследователей.

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

            Тогда были популярными машины опорных векторов. Многие успешно занимались ими. Сейчас, кстати, то же самое с глубоким обучением. Все занимаются им, а не другими безнадежно тяжелыми подходами, которые, однако, также могут выстрелить через какой-то десяток лет. Но всё же дело во многом связано с вычислительными ресурсами. Тот же LSTM был известен и 20 лет назад. Но если перенести его современное использование на компьютеры тех времен, то это сразу станет никому не нужно…

            Перейдём к техническим вопросам. Нейровероятностное программирование на Edward — каким образом две парадигмы объединены и что это позволяет моделировать?

            Алексей Потапов: В Tensorflow всё строится на тензорах и операциях с ними и, главное, добавляется автоматическое дифференцирование, которое используется для оптимизации заданной функции потерь градиентным спуском. В Edward все операции с тензорами остаются, но поверх или внутрь тензоров добавляются распределения вероятностей, а также специальные функции потерь для распределений, скажем, дивергенция Кульбака-Лейблера, и методы их оптимизации. Это даёт возможность просто и компактно задавать и обучать генеративные глубокие сети, такие как Generative Adversarial Networks (вариант перевода: генеративные состязающиеся сети), вариационные автоэнкодеры, а также писать свои аналогичные модели и делать байесовский вывод над любыми сетями.

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

            — При каких практических условиях Вы бы точно не рекомендовали использовать глубокое обучения?

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

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

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

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

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

            Алексей Потапов: Можно построить более сложную метасеть, которая будет «понимать» более простую сеть, но это вычислительно слишком дорого. Вопрос, скорее, в том, что вы хотите узнать о сети. Скажем, «байесовизация» сети даст вам информацию о неопределённости. Есть техники для генерации «плохих» примеров. В случае генеративных сетей вы можете визуализировать выученное многообразие, меняя одну скрытую переменную. То есть отдельные техники для анализа есть. Но, конечно, всей картины они вам не покажут — если вы используете типичные сети глубокого обучения, а не какие-то специальные компактные модели, то просто описать работу сети вы не сможете, так как такая сеть в принципе сложная.

            Классические алгоритмы машинного обучения с учителем (supervised), как, например SVM, пытаются определить класс объекта, а без учителя (unsupervised) — разбить набор объектов по классам без меток. С этой точки зрения автоэнкодеры (autoencoder) делают что-то очень странное: они пытаются воссоздать сами данные — зачем и почему это вообще может быть нужно?

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

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

            Многие исследователи (например, Демис Хассабис и Андрэ Ын) отмечают, что машинное обучение началось с определения параметров (классическое машинное обучение SVM, C4.5), перешло к построению признаков (e.g., CNN) и сейчас переходит в стадию нахождения и построения архитектуры. Как технически такое метообучение вообще может быть реализовано и насколько результаты сравнимы с традиционными методами?

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



            Если темы Big Data и машинного обучения вам близки так же, как и нам, хотим обратить ваше внимание на ряд ключевых докладов на грядущей конференции SmartData 2017, которая пройдёт 21 октября в Санкт-Петербурге:

            Original source: habrahabr.ru (comments, light).

            https://habrahabr.ru/post/337392/


            Метки:  

            Метавычисления и глубокие свёрточные сети: интервью с профессором ИТМО

            Понедельник, 11 Сентября 2017 г. 15:16 + в цитатник
            ARG89 сегодня в 15:16 Разработка

            Метавычисления и глубокие свёрточные сети: интервью с профессором ИТМО



              После победы AlphaGo в марте 2016 года над одним из сильнейших игроков Go в мире Ли Седолем о методах глубокого обучения заговорили практически везде. И даже Google не упустил случая назвать себя компанией машинного обучения и искусственного интеллекта.

              Что стоит за термином «глубокое обучение»? Какими бывают модели машинного обучения и на чём они пишутся? Ответить на эти и многие другие вопросы, связанные с МО и, в частности, с глубоким обучением (deep learning), мы попросили Алексея Потапова, профессора кафедры компьютерной фотоники и видеоинформатики ИТМО.

              Алексей Потапов
              Профессор кафедры компьютерной фотоники и видеоинформатики ИТМО. Победитель конкурса грантов для молодых научно-педагогических работников вузов Санкт-Петербурга. Победитель конкурса грантов Президента Российской Федерации для государственной поддержки молодых российских ученых — докторов наук в области «Информационно-телекоммуникационных систем и технологий».



              — Сегодня мы практически отовсюду слышим о буме машинного обучения и особенно глубокого обучения. Как и в какой ситуации это может понадобиться простому IT-шнику?

              Алексей Потапов: Честно говоря, у меня в голове нет образа «простого IT-шника», так что мне непросто ответить на этот вопрос. Можно ли считать «простым IT-шником» того, кто сможет применить методы глубокого обучения для автоматизации своей деятельности? Не уверен. Наверное, это всё же будет «сложный IT-шник». Тогда простому особо вникать, наверное, и не надо — за исключением того, чтобы поддерживать актуальную картину мира и в качестве демотиватора: рано или поздно «роботы отберут работу» не только у сотрудников Макдональдса, но и у IT-шников.

              — А если у человека есть техническое образование, и он решил погрузиться в тему — с чего ему лучше начать и куда пойти?

              Алексей Потапов: Здесь очень сильно зависит от того, насколько глубоко и с какой целью человек решил погрузиться в тему, а также от уровня его подготовки. Одно дело — использовать имеющиеся предобученные сети или обучать известные модели с реализацией на высокоуровневых библиотеках, таких как Keras. Это может делать и не самый продвинутый школьник, не особо вникая в математику. Достаточно изучить документацию соответствующей библиотеки и на примерах научиться комбинировать разные слои, активационные функции и т.д. — работать как с «чёрными ящиками».

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

              Если человеку не подходит такой путь или он хочет копнуть глубже, он может начать с поверхностной теории — с описания базовых моделей (автоэнкодеров, машин Больцмана, свёрточных сетей, рекуррентных блоков — tanh-RNN, LSTM, GRU и т.д.) AS-IS. Кому-то, может, придётся начать с логистической регрессии и перцептронов. Несложно обо всём этом почитать в интернете. На этом уровне погружения можно пользоваться более низкоуровневыми библиотеками (такими как Tensorflow и Theano) и реализовывать модели не из готовых блоков, а на уровне операций с тензорами.

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

              Вот, например, несколько неплохих курсов:
              Introduction into Deep Learning
              Neural Networks and Deep Learning
              Convolutional Neural Networks for Visual Recognition

              — В докладе Вы рассматриваете дискриминантные и генеративные модели, можно ли на простом примере объяснить, в чём между ними разница и для чего они нужны?

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

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

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

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

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

              — Почему глубокое обучение и вероятностный подход выстрелили именно сейчас, а не 20-30 лет назад?

              Алексей Потапов: Если отвечать максимально упрощённо, то дело, очевидно, в вычислительных ресурсах, особенно в развитии GPGPU, а также в доступности данных для обучения. Всё, конечно, чуть сложнее, и это не единственная причина, но всё же основная. Конечно, дело и в развитии представлений исследователей.

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

              Тогда были популярными машины опорных векторов. Многие успешно занимались ими. Сейчас, кстати, то же самое с глубоким обучением. Все занимаются им, а не другими безнадежно тяжелыми подходами, которые, однако, также могут выстрелить через какой-то десяток лет. Но всё же дело во многом связано с вычислительными ресурсами. Тот же LSTM был известен и 20 лет назад. Но если перенести его современное использование на компьютеры тех времен, то это сразу станет никому не нужно…

              Перейдём к техническим вопросам. Нейровероятностное программирование на Edward — каким образом две парадигмы объединены и что это позволяет моделировать?

              Алексей Потапов: В Tensorflow всё строится на тензорах и операциях с ними и, главное, добавляется автоматическое дифференцирование, которое используется для оптимизации заданной функции потерь градиентным спуском. В Edward все операции с тензорами остаются, но поверх или внутрь тензоров добавляются распределения вероятностей, а также специальные функции потерь для распределений, скажем, дивергенция Кульбака-Лейблера, и методы их оптимизации. Это даёт возможность просто и компактно задавать и обучать генеративные глубокие сети, такие как Generative Adversarial Networks (вариант перевода: генеративные состязающиеся сети), вариационные автоэнкодеры, а также писать свои аналогичные модели и делать байесовский вывод над любыми сетями.

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

              — При каких практических условиях Вы бы точно не рекомендовали использовать глубокое обучения?

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

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

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

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

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

              Алексей Потапов: Можно построить более сложную метасеть, которая будет «понимать» более простую сеть, но это вычислительно слишком дорого. Вопрос, скорее, в том, что вы хотите узнать о сети. Скажем, «байесовизация» сети даст вам информацию о неопределённости. Есть техники для генерации «плохих» примеров. В случае генеративных сетей вы можете визуализировать выученное многообразие, меняя одну скрытую переменную. То есть отдельные техники для анализа есть. Но, конечно, всей картины они вам не покажут — если вы используете типичные сети глубокого обучения, а не какие-то специальные компактные модели, то просто описать работу сети вы не сможете, так как такая сеть в принципе сложная.

              Классические алгоритмы машинного обучения с учителем (supervised), как, например SVM, пытаются определить класс объекта, а без учителя (unsupervised) — разбить набор объектов по классам без меток. С этой точки зрения автоэнкодеры (autoencoder) делают что-то очень странное: они пытаются воссоздать сами данные — зачем и почему это вообще может быть нужно?

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

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

              Многие исследователи (например, Демис Хассабис и Андрэ Ын) отмечают, что машинное обучение началось с определения параметров (классическое машинное обучение SVM, C4.5), перешло к построению признаков (e.g., CNN) и сейчас переходит в стадию нахождения и построения архитектуры. Как технически такое метообучение вообще может быть реализовано и насколько результаты сравнимы с традиционными методами?

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



              Если темы Big Data и машинного обучения вам близки так же, как и нам, хотим обратить ваше внимание на ряд ключевых докладов на грядущей конференции SmartData 2017, которая пройдёт 21 октября в Санкт-Петербурге:

              Original source: habrahabr.ru (comments, light).

              https://habrahabr.ru/post/337392/


              Метки:  

              Метавычисления и глубокие свёрточные сети: интервью с профессором ИТМО

              Понедельник, 11 Сентября 2017 г. 15:16 + в цитатник
              ARG89 сегодня в 15:16 Разработка

              Метавычисления и глубокие свёрточные сети: интервью с профессором ИТМО



                После победы AlphaGo в марте 2016 года над одним из сильнейших игроков Go в мире Ли Седолем о методах глубокого обучения заговорили практически везде. И даже Google не упустил случая назвать себя компанией машинного обучения и искусственного интеллекта.

                Что стоит за термином «глубокое обучение»? Какими бывают модели машинного обучения и на чём они пишутся? Ответить на эти и многие другие вопросы, связанные с МО и, в частности, с глубоким обучением (deep learning), мы попросили Алексея Потапова, профессора кафедры компьютерной фотоники и видеоинформатики ИТМО.

                Алексей Потапов
                Профессор кафедры компьютерной фотоники и видеоинформатики ИТМО. Победитель конкурса грантов для молодых научно-педагогических работников вузов Санкт-Петербурга. Победитель конкурса грантов Президента Российской Федерации для государственной поддержки молодых российских ученых — докторов наук в области «Информационно-телекоммуникационных систем и технологий».



                — Сегодня мы практически отовсюду слышим о буме машинного обучения и особенно глубокого обучения. Как и в какой ситуации это может понадобиться простому IT-шнику?

                Алексей Потапов: Честно говоря, у меня в голове нет образа «простого IT-шника», так что мне непросто ответить на этот вопрос. Можно ли считать «простым IT-шником» того, кто сможет применить методы глубокого обучения для автоматизации своей деятельности? Не уверен. Наверное, это всё же будет «сложный IT-шник». Тогда простому особо вникать, наверное, и не надо — за исключением того, чтобы поддерживать актуальную картину мира и в качестве демотиватора: рано или поздно «роботы отберут работу» не только у сотрудников Макдональдса, но и у IT-шников.

                — А если у человека есть техническое образование, и он решил погрузиться в тему — с чего ему лучше начать и куда пойти?

                Алексей Потапов: Здесь очень сильно зависит от того, насколько глубоко и с какой целью человек решил погрузиться в тему, а также от уровня его подготовки. Одно дело — использовать имеющиеся предобученные сети или обучать известные модели с реализацией на высокоуровневых библиотеках, таких как Keras. Это может делать и не самый продвинутый школьник, не особо вникая в математику. Достаточно изучить документацию соответствующей библиотеки и на примерах научиться комбинировать разные слои, активационные функции и т.д. — работать как с «чёрными ящиками».

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

                Если человеку не подходит такой путь или он хочет копнуть глубже, он может начать с поверхностной теории — с описания базовых моделей (автоэнкодеров, машин Больцмана, свёрточных сетей, рекуррентных блоков — tanh-RNN, LSTM, GRU и т.д.) AS-IS. Кому-то, может, придётся начать с логистической регрессии и перцептронов. Несложно обо всём этом почитать в интернете. На этом уровне погружения можно пользоваться более низкоуровневыми библиотеками (такими как Tensorflow и Theano) и реализовывать модели не из готовых блоков, а на уровне операций с тензорами.

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

                Вот, например, несколько неплохих курсов:
                Introduction into Deep Learning
                Neural Networks and Deep Learning
                Convolutional Neural Networks for Visual Recognition

                — В докладе Вы рассматриваете дискриминантные и генеративные модели, можно ли на простом примере объяснить, в чём между ними разница и для чего они нужны?

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

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

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

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

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

                — Почему глубокое обучение и вероятностный подход выстрелили именно сейчас, а не 20-30 лет назад?

                Алексей Потапов: Если отвечать максимально упрощённо, то дело, очевидно, в вычислительных ресурсах, особенно в развитии GPGPU, а также в доступности данных для обучения. Всё, конечно, чуть сложнее, и это не единственная причина, но всё же основная. Конечно, дело и в развитии представлений исследователей.

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

                Тогда были популярными машины опорных векторов. Многие успешно занимались ими. Сейчас, кстати, то же самое с глубоким обучением. Все занимаются им, а не другими безнадежно тяжелыми подходами, которые, однако, также могут выстрелить через какой-то десяток лет. Но всё же дело во многом связано с вычислительными ресурсами. Тот же LSTM был известен и 20 лет назад. Но если перенести его современное использование на компьютеры тех времен, то это сразу станет никому не нужно…

                Перейдём к техническим вопросам. Нейровероятностное программирование на Edward — каким образом две парадигмы объединены и что это позволяет моделировать?

                Алексей Потапов: В Tensorflow всё строится на тензорах и операциях с ними и, главное, добавляется автоматическое дифференцирование, которое используется для оптимизации заданной функции потерь градиентным спуском. В Edward все операции с тензорами остаются, но поверх или внутрь тензоров добавляются распределения вероятностей, а также специальные функции потерь для распределений, скажем, дивергенция Кульбака-Лейблера, и методы их оптимизации. Это даёт возможность просто и компактно задавать и обучать генеративные глубокие сети, такие как Generative Adversarial Networks (вариант перевода: генеративные состязающиеся сети), вариационные автоэнкодеры, а также писать свои аналогичные модели и делать байесовский вывод над любыми сетями.

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

                — При каких практических условиях Вы бы точно не рекомендовали использовать глубокое обучения?

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

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

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

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

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

                Алексей Потапов: Можно построить более сложную метасеть, которая будет «понимать» более простую сеть, но это вычислительно слишком дорого. Вопрос, скорее, в том, что вы хотите узнать о сети. Скажем, «байесовизация» сети даст вам информацию о неопределённости. Есть техники для генерации «плохих» примеров. В случае генеративных сетей вы можете визуализировать выученное многообразие, меняя одну скрытую переменную. То есть отдельные техники для анализа есть. Но, конечно, всей картины они вам не покажут — если вы используете типичные сети глубокого обучения, а не какие-то специальные компактные модели, то просто описать работу сети вы не сможете, так как такая сеть в принципе сложная.

                Классические алгоритмы машинного обучения с учителем (supervised), как, например SVM, пытаются определить класс объекта, а без учителя (unsupervised) — разбить набор объектов по классам без меток. С этой точки зрения автоэнкодеры (autoencoder) делают что-то очень странное: они пытаются воссоздать сами данные — зачем и почему это вообще может быть нужно?

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

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

                Многие исследователи (например, Демис Хассабис и Андрэ Ын) отмечают, что машинное обучение началось с определения параметров (классическое машинное обучение SVM, C4.5), перешло к построению признаков (e.g., CNN) и сейчас переходит в стадию нахождения и построения архитектуры. Как технически такое метообучение вообще может быть реализовано и насколько результаты сравнимы с традиционными методами?

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



                Если темы Big Data и машинного обучения вам близки так же, как и нам, хотим обратить ваше внимание на ряд ключевых докладов на грядущей конференции SmartData 2017, которая пройдёт 21 октября в Санкт-Петербурге:

                Original source: habrahabr.ru (comments, light).

                https://habrahabr.ru/post/337392/


                Метки:  

                Технокубок 2017-2018

                Понедельник, 11 Сентября 2017 г. 15:01 + в цитатник
                sat2707 сегодня в 15:01 Разработка

                Технокубок 2017-2018

                  Каждый год Министерство образования и науки РФ публикует перечень школьных олимпиад, дающих льготы при поступлении в вузы. С 2015 года в этот список входит и Технокубок — олимпиада по программированию под эгидой Mail.Ru Group.


                  image


                  Поучаствовать в Технокубке могут ученики 8—11-х классов. Олимпиада позволяет ребятам оценить свои силы и пообщаться с профессионалами IT-отрасли, а главное — дает шанс поступить в ведущие профильные вузы России.


                  В 2017/18 учебном году Технокубок стал олимпиадой II уровня. Каждый вуз определяет льготу самостоятельно — это может быть максимальный балл по информатике или зачисление без вступительных испытаний. Судить о льготах на текущий год можно исходя из прошлогодних привилегий: как правило, эти сведения доступны на сайтах вузов. Также информация есть здесь.


                  Зарегистрироваться на Технокубок можно до 12 ноября. Заочные отборочные раунды пройдут 17 сентября, 15 октября и 12 ноября. Для того чтобы попасть в финал, достаточно победить в одном из трех отборочных раундов. Для интересующихся — примеры и разборы задач прошлых лет:


                  Технокубок 2015/16
                  1-й и 2-й отборочный раунд
                  Финал


                  Технокубок 2016/17
                  1-й отборочный раунд
                  2-й отборочный раунд
                  3-й отборочный раунд
                  Финал


                  image


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


                  Очный финал состоится в марте 2018-го на двух московских площадках: в МГТУ им. Н. Э. Баумана и в МФТИ. Награждение победителей пройдет в офисе Mail.Ru Group.


                  image


                  Каждый год в Технокубке участвует около 3 тысяч школьников из России и стран СНГ. Всего за время существования олимпиады в ней приняли участие более 7 тысяч человек.


                  Участникам, занявшим первые три места, помимо льгот при поступлении достанутся ценные призы. Кроме того, финалисты, которые попадут в МГТУ им. Н. Э. Баумана и в МФТИ, получат привилегии при поступлении в Технопарк и Технотрек — бесплатные практико-ориентированные образовательные проекты Mail.Ru Group.


                  image


                  В составе оргкомитета олимпиады — председатель совета директоров и сооснователь Mail.Ru Group Дмитрий Гришин. Сопредседатели оргкомитета — ректор МФТИ Н. Н. Кудрявцев и ректор МГТУ им. Н. Э. Баумана А. А. Александров. Председатель жюри — Михаил Мирзаянов, старший преподаватель кафедры математических основ информатики и олимпиадного программирования Саратовского государственного университета, основатель и генеральный директор Codeforces, дважды серебряный призер чемпионата мира по программированию ACM-ICPC.


                  image


                  Зарегистрироваться, узнать подробности и задать вопросы можно на сайте олимпиады и в группе в ВКонтакте. Технокубок-2018 ждет новых героев!

                  Original source: habrahabr.ru (comments, light).

                  https://habrahabr.ru/post/337646/


                  Метки:  

                  Технокубок 2017-2018

                  Понедельник, 11 Сентября 2017 г. 15:01 + в цитатник
                  sat2707 сегодня в 15:01 Разработка

                  Технокубок 2017-2018

                    Каждый год Министерство образования и науки РФ публикует перечень школьных олимпиад, дающих льготы при поступлении в вузы. С 2015 года в этот список входит и Технокубок — олимпиада по программированию под эгидой Mail.Ru Group.


                    image


                    Поучаствовать в Технокубке могут ученики 8—11-х классов. Олимпиада позволяет ребятам оценить свои силы и пообщаться с профессионалами IT-отрасли, а главное — дает шанс поступить в ведущие профильные вузы России.


                    В 2017/18 учебном году Технокубок стал олимпиадой II уровня. Каждый вуз определяет льготу самостоятельно — это может быть максимальный балл по информатике или зачисление без вступительных испытаний. Судить о льготах на текущий год можно исходя из прошлогодних привилегий: как правило, эти сведения доступны на сайтах вузов. Также информация есть здесь.


                    Зарегистрироваться на Технокубок можно до 12 ноября. Заочные отборочные раунды пройдут 17 сентября, 15 октября и 12 ноября. Для того чтобы попасть в финал, достаточно победить в одном из трех отборочных раундов. Для интересующихся — примеры и разборы задач прошлых лет:


                    Технокубок 2015/16
                    1-й и 2-й отборочный раунд
                    Финал


                    Технокубок 2016/17
                    1-й отборочный раунд
                    2-й отборочный раунд
                    3-й отборочный раунд
                    Финал


                    image


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


                    Очный финал состоится в марте 2018-го на двух московских площадках: в МГТУ им. Н. Э. Баумана и в МФТИ. Награждение победителей пройдет в офисе Mail.Ru Group.


                    image


                    Каждый год в Технокубке участвует около 3 тысяч школьников из России и стран СНГ. Всего за время существования олимпиады в ней приняли участие более 7 тысяч человек.


                    Участникам, занявшим первые три места, помимо льгот при поступлении достанутся ценные призы. Кроме того, финалисты, которые попадут в МГТУ им. Н. Э. Баумана и в МФТИ, получат привилегии при поступлении в Технопарк и Технотрек — бесплатные практико-ориентированные образовательные проекты Mail.Ru Group.


                    image


                    В составе оргкомитета олимпиады — председатель совета директоров и сооснователь Mail.Ru Group Дмитрий Гришин. Сопредседатели оргкомитета — ректор МФТИ Н. Н. Кудрявцев и ректор МГТУ им. Н. Э. Баумана А. А. Александров. Председатель жюри — Михаил Мирзаянов, старший преподаватель кафедры математических основ информатики и олимпиадного программирования Саратовского государственного университета, основатель и генеральный директор Codeforces, дважды серебряный призер чемпионата мира по программированию ACM-ICPC.


                    image


                    Зарегистрироваться, узнать подробности и задать вопросы можно на сайте олимпиады и в группе в ВКонтакте. Технокубок-2018 ждет новых героев!

                    Original source: habrahabr.ru (comments, light).

                    https://habrahabr.ru/post/337646/


                    Метки:  

                    Технокубок 2017-2018

                    Понедельник, 11 Сентября 2017 г. 15:01 + в цитатник
                    sat2707 сегодня в 15:01 Разработка

                    Технокубок 2017-2018

                      Каждый год Министерство образования и науки РФ публикует перечень школьных олимпиад, дающих льготы при поступлении в вузы. С 2015 года в этот список входит и Технокубок — олимпиада по программированию под эгидой Mail.Ru Group.


                      image


                      Поучаствовать в Технокубке могут ученики 8—11-х классов. Олимпиада позволяет ребятам оценить свои силы и пообщаться с профессионалами IT-отрасли, а главное — дает шанс поступить в ведущие профильные вузы России.


                      В 2017/18 учебном году Технокубок стал олимпиадой II уровня. Каждый вуз определяет льготу самостоятельно — это может быть максимальный балл по информатике или зачисление без вступительных испытаний. Судить о льготах на текущий год можно исходя из прошлогодних привилегий: как правило, эти сведения доступны на сайтах вузов. Также информация есть здесь.


                      Зарегистрироваться на Технокубок можно до 12 ноября. Заочные отборочные раунды пройдут 17 сентября, 15 октября и 12 ноября. Для того чтобы попасть в финал, достаточно победить в одном из трех отборочных раундов. Для интересующихся — примеры и разборы задач прошлых лет:


                      Технокубок 2015/16
                      1-й и 2-й отборочный раунд
                      Финал


                      Технокубок 2016/17
                      1-й отборочный раунд
                      2-й отборочный раунд
                      3-й отборочный раунд
                      Финал


                      image


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


                      Очный финал состоится в марте 2018-го на двух московских площадках: в МГТУ им. Н. Э. Баумана и в МФТИ. Награждение победителей пройдет в офисе Mail.Ru Group.


                      image


                      Каждый год в Технокубке участвует около 3 тысяч школьников из России и стран СНГ. Всего за время существования олимпиады в ней приняли участие более 7 тысяч человек.


                      Участникам, занявшим первые три места, помимо льгот при поступлении достанутся ценные призы. Кроме того, финалисты, которые попадут в МГТУ им. Н. Э. Баумана и в МФТИ, получат привилегии при поступлении в Технопарк и Технотрек — бесплатные практико-ориентированные образовательные проекты Mail.Ru Group.


                      image


                      В составе оргкомитета олимпиады — председатель совета директоров и сооснователь Mail.Ru Group Дмитрий Гришин. Сопредседатели оргкомитета — ректор МФТИ Н. Н. Кудрявцев и ректор МГТУ им. Н. Э. Баумана А. А. Александров. Председатель жюри — Михаил Мирзаянов, старший преподаватель кафедры математических основ информатики и олимпиадного программирования Саратовского государственного университета, основатель и генеральный директор Codeforces, дважды серебряный призер чемпионата мира по программированию ACM-ICPC.


                      image


                      Зарегистрироваться, узнать подробности и задать вопросы можно на сайте олимпиады и в группе в ВКонтакте. Технокубок-2018 ждет новых героев!

                      Original source: habrahabr.ru (comments, light).

                      https://habrahabr.ru/post/337646/


                      Метки:  

                      Сегментация лица на селфи без нейросетей

                      Понедельник, 11 Сентября 2017 г. 14:08 + в цитатник
                      mephistopheies сегодня в 14:08 Разработка

                      Сегментация лица на селфи без нейросетей

                        Приветствую вас, коллеги. Оказывается, не все компьютерное зрение сегодня делается с использованием нейронных сетей. Хотя многие стартапы и заявляют, что у них дип лернинг везде, спешу вас разочаровать, они просто хотят хайпануть немножечко. Рассмотрим, например, задачу сегментации. В нашем слаке развернулась целая драма. Одна богатая и высокотехнологичная селфи-компания собрала датасет для сегментации селфи с помощью нейросетей (а это непростое и недешевое занятие). А другая, более бедная и не очень развитая решила, что можно подкупить людей, размечающих фотки, и спполучить базу. В общем, страсти в этих ваших Интернетах еще те. Недавно я наткнулся на статью, где без всяких нейросетей на устройстве делают очень даже хорошую сегментацию. Для сегментации от пользователя требуется дать алгоритму несколько подсказок, но с помощью dlib и opencv такие подсказки легко автоматизируются. В качестве бонуса мы так же сгладим вырезанное лицо и перенесем на какого-нибудь рандомного человека, тем самым поймем, как работают маски во всех этих снапчятах и маскарадах. В общем, классика еще жива, и если вы хотите немного окунуться в классическое компьютерное зрение на питоне, то добро пожаловать под кат.


                        Алгоритм


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



                        Далее делаем следующие шаги:


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

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


                        import
                        %matplotlib inline
                        import matplotlib
                        import numpy as np
                        import matplotlib.pyplot as plt
                        import seaborn as sns
                        sns.set_style("dark")
                        plt.rcParams['figure.figsize'] = 16, 12
                        import pandas as pd
                        from PIL import Image
                        from tqdm import tqdm_notebook
                        from skimage import transform
                        import itertools as it
                        from sklearn.neighbors.kde import KernelDensity
                        import matplotlib.cm as cm
                        import queue
                        from skimage import morphology
                        import dlib
                        import cv2
                        from imutils import face_utils
                        from scipy.spatial import Delaunay




                        Автоматизируем штрихи


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



                        img_input = np.array(Image.open('./../data/input2.jpg'))[:500, 400:, :]
                        print(img_input.shape)
                        plt.imshow(img_input)


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


                        # инстанцируем класс для детекции лиц (рамка)
                        detector = dlib.get_frontal_face_detector()
                        # инстанцируем класс для детекции ключевых точек
                        predictor = dlib.shape_predictor('./../data/shape_predictor_68_face_landmarks.dat')
                        
                        # конвертируем изображение в много оттенков серого
                        img_gray = cv2.cvtColor(img_input, cv2.COLOR_BGR2GRAY)
                        # вычисляем список рамок на каждое найденное лицо
                        rects = detector(img_gray, 0)
                        # вычисляем ключевые точки
                        shape = predictor(img_gray, rects[0])
                        shape = face_utils.shape_to_np(shape)

                        отрисуем ключевые точки
                        img_tmp = img_input.copy()
                        for x, y in shape:
                            cv2.circle(img_tmp, (x, y), 1, (0, 0, 255), -1)
                        plt.imshow(img_tmp)


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


                        # оригинальная рамка
                        face_origin = sorted([(t.width()*t.height(), 
                                               (t.left(), t.top(), t.width(), t.height())) 
                                              for t in rects], 
                                             key=lambda t: t[0], reverse=True)[0][1]
                        
                        # коэффициенты расширения рамки
                        rescale = (1.3, 2.2, 1.3, 1.3)
                        # расширение рамки, так чтобы она не вылезла за края
                        (x, y, w, h) = face_origin
                        cx = x + w/2
                        cy = y + h/2
                        w = min(img_input.shape[1] - x, int(w/2 + rescale[2]*w/2))
                        h = min(img_input.shape[0] - y, int(h/2 + rescale[3]*h/2))
                        fx = max(0, int(x + w/2*(1 - rescale[0])))
                        fy = max(0, int(y + h/2*(1 - rescale[1])))
                        fw = min(img_input.shape[1] - fx, int(w - w/2*(1 - rescale[0])))
                        fh = min(img_input.shape[0] - fy, int(h - h/2*(1 - rescale[1])))
                        
                        face = (fx, fy, fw, fh)

                        отрисовываем рамки
                        img_tmp = cv2.rectangle(img_input.copy(), (face[0], face[1]), 
                                                (face[0] + face[2], face[1] + face[3]),
                                                (255, 0, 0), thickness=3, lineType=8, shift=0)
                        img_tmp = cv2.rectangle(img_tmp, (face_origin[0], face_origin[1]), 
                                                (face_origin[0] + face_origin[2], face_origin[1] + face_origin[3]),
                                                (0, 255, 0), thickness=3, lineType=8, shift=0)
                        plt.imshow(img_tmp)
                        


                        Теперь у нас имеется область, которая точно не относится к лицу — всё, что вне красной рамки. Выберем оттуда некоторое количество случайных точек и будем считать их штрихами фона. Также у нас имеются 62 точки, которые точно расположены на лице. Для упрощения задачи я выберу 5 из них: по одной на уровне глаз на краю лица, по одной на уровне рта на краю лица и одну внизу посередине подбородка. Все точки внутри этого пятиугольника будут принадлежать только лицу. Опять же для простоты будем считать, что лицо вертикально расположено на изображении и потому мы можем отразить полученный пятиугольник по оси $y$, тем самым получив восьмиугольник. Все, что внутри восьмиугольника будем считать штрихом объекта.


                        # выбираем вышеописанные пять точек
                        points = [shape[0].tolist(), shape[16].tolist()]
                        for ix in [4, 12, 8]:
                            x, y = shape[ix].tolist()
                            points.append((x, y))
                            points.append((x, points[0][1] + points[0][1] - y))
                        
                        # я не особо в прототипе запариваюсь над производительностью
                        # так что вызываю триангуляцию Делоне,
                        # чтобы использовать ее как тест на то, что точка внутри полигона
                        # все это можно делать быстрее, т.к. точный тест не нужен
                        # для прототипа :good-enough: 
                        hull = Delaunay(points)
                        xy_fg = []
                        for x, y in it.product(range(img_input.shape[0]), range(img_input.shape[1])):
                            if hull.find_simplex([y, x]) >= 0:
                                xy_fg.append((x, y))
                        print('xy_fg%:', len(xy_fg)/np.prod(img_input.shape))
                        
                        # вычисляем количество точек для фона
                        # примерно равно что бы было тому, что на лице
                        r = face[1]*face[3]/np.prod(img_input.shape[:2])
                        print(r)
                        k = 0.1
                        xy_bg_n = int(k*np.prod(img_input.shape[:2]))
                        print(xy_bg_n)
                        
                        # накидываем случайные точки
                        xy_bg = zip(np.random.uniform(0, img_input.shape[0], size=xy_bg_n).astype(np.int),
                                    np.random.uniform(0, img_input.shape[1], size=xy_bg_n).astype(np.int))
                        xy_bg = list(xy_bg)
                        xy_bg = [(x, y) for (x, y) in xy_bg 
                                 if y < face[0] or y > face[0] + face[2] or x < face[1] or x > face[1] + face[3]]
                        print(len(xy_bg)/np.prod(img_input.shape[:2]))

                        отрисовываем штрихи
                        img_tmp = img_input/255
                        for x, y in xy_fg:
                            img_tmp[x, y, :] = img_tmp[x, y, :]*0.5 + np.array([1, 0, 0]) * 0.5
                        
                        for x, y in xy_bg:
                            img_tmp[x, y, :] = img_tmp[x, y, :]*0.5 + np.array([0, 0, 1]) * 0.5
                        
                        plt.imshow(img_tmp)





                        Нечеткое разделение фона и объекта


                        Теперь у нас есть два набора данных: точки объекта $D_f$ и точки фона $D_b$.


                        points_fg = np.array([img_input[x, y, :] for (x, y) in xy_fg])
                        points_bg = np.array([img_input[x, y, :] for (x, y) in xy_bg])

                        Посмотрим на распределение цветов по RGB каналам в каждом из множеств. Первая гистограмма — для объекта, вторая — для фона.


                        отрисовка распределений
                        fig, axes = plt.subplots(nrows=2, ncols=1)
                        sns.distplot(points_fg[:, 0], ax=axes[0], color='r')
                        sns.distplot(points_fg[:, 1], ax=axes[0], color='g')
                        sns.distplot(points_fg[:, 2], ax=axes[0], color='b')
                        sns.distplot(points_bg[:, 0], ax=axes[1], color='r')
                        sns.distplot(points_bg[:, 1], ax=axes[1], color='g')
                        sns.distplot(points_bg[:, 2], ax=axes[1], color='b')


                        Радует, что распределения отличаются. Это значит, что если мы сможем получить функции, оценивающие вероятность принадлежности точки к нужному распределению, то мы получим нечеткие маски. И оказывается такой способ есть — kernel density estimation. Для заданного набора точек, можно построить функцию оценки плотности для новой точки $x$ следующим образом (для простоты пример для одномерного распределения):


                        $F\left(x\right) = \frac{1}{h\cdot\left|D\right|} \sum_{i =1}^{\left|D\right|} K\left(\frac{x - x_i}{h}\right)$


                        где:


                        • $h$ — параметр сглаживания
                        • $K$ — некоторое ядро

                        Мы для простоты будем использовать Гауссово ядро:


                        $K(u)={\frac {1}{\sqrt {2\pi }}}e^{-{\frac {1}{2}}u^{2}}$


                        Хотя для скорости Гауссово ядро не лучший выбор и если взять ядро Епанечникова, то все будет считаться быстрее. Так же я буду использовать KernelDensity из sklearn, что в итоге выльется в 5 минут скоринга. Авторы этой статьи утверждают, что замена KDE на оптимальную реализацию сокращает расчеты на устройстве до одной секунды.


                        # инстанцируем классы KDE для объекта и фона
                        kde_fg = KernelDensity(kernel='gaussian', 
                                               bandwidth=1, 
                                               algorithm='kd_tree', 
                                               leaf_size=100).fit(points_fg)
                        kde_bg = KernelDensity(kernel='gaussian', 
                                               bandwidth=1, 
                                               algorithm='kd_tree', 
                                               leaf_size=100).fit(points_bg)
                        
                        # инициализируем и вычисляем маски
                        score_kde_fg = np.zeros(img_input.shape[:2])
                        score_kde_bg = np.zeros(img_input.shape[:2])
                        likelihood_fg = np.zeros(img_input.shape[:2])
                        coodinates = it.product(range(score_kde_fg.shape[0]), 
                                                range(score_kde_fg.shape[1]))
                        for x, y in tqdm_notebook(coodinates, 
                                                  total=np.prod(score_kde_fg.shape)):
                            score_kde_fg[x, y] = np.exp(kde_fg.score(img_input[x, y, :].reshape(1, -1)))
                            score_kde_bg[x, y] = np.exp(kde_bg.score(img_input[x, y, :].reshape(1, -1)))
                            n = score_kde_fg[x, y] + score_kde_bg[x, y]
                            if n == 0:
                                n = 1
                            likelihood_fg[x, y] = score_kde_fg[x, y]/n

                        В итоге у нас есть несколько масок:


                        • score_kde_fg — оценка вероятности быть точкой объекта $p\left(x \mid D_f\right)$
                        • score_kde_bg — оценка вероятности быть точкой объекта $p\left(x \mid D_b\right)$
                        • likelihood_fg — нормализированная вероятность быть точкой объекта $p_f\left(x\right) = \frac{p\left(x \mid D_f\right)}{p\left(x \mid D_f\right) + p\left(x \mid D_b\right)}$
                        • 1 - likelihood_fg нормализированная вероятность быть точкой фона $p_b\left(x\right) = 1 - p_f\left(x\right)$

                        Посмотрим на следующие распределения.


                        Распределение значений score_kde_fg
                        sns.distplot(score_kde_fg.flatten())
                        plt.show()


                        Распределение значений score_kde_bg
                        sns.distplot(score_kde_bg.flatten())
                        plt.show()


                        Распределение значений likelihood_fg:


                        sns.distplot(likelihood_fg.flatten())
                        plt.show()


                        Вселяет надежду то, что на $p_f\left(x\right)$ есть два пика, и количество точек, принадлежащих лицу, явно не меньше, чем фоновых точек. Нарисуем полученные маски.


                        маска score_kde_fg
                        plt.matshow(score_kde_fg, cmap=cm.bwr)
                        plt.show()


                        маска score_kde_bg
                        plt.matshow(score_kde_bg, cmap=cm.bwr)
                        plt.show()


                        plt.matshow(likelihood_fg, cmap=cm.bwr)
                        plt.show()


                        маска 1 - likelihood_fg
                        plt.matshow(1 - likelihood_fg, cmap=cm.bwr)
                        plt.show()


                        К сожалению, часть косяка двери получилась частью лица. Хорошо, что косяк далеко от лица. Этим-то свойством мы и воспользуемся в следущей части.





                        Бинарная маска объекта


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


                        $d\left(a, b\right) = \left| p\left(a\right) - p\left(b\right) \right|$


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


                        def dijkstra(start_points, w):
                            d = np.zeros(w.shape) + np.infty
                            v = np.zeros(w.shape, dtype=np.bool)
                            q = queue.PriorityQueue()
                            for x, y in start_points:
                                d[x, y] = 0
                                q.put((d[x, y], (x, y)))
                        
                            for x, y in it.product(range(w.shape[0]), range(w.shape[1])):
                                if np.isinf(d[x, y]):
                                    q.put((d[x, y], (x, y)))
                        
                            while not q.empty():
                                _, p = q.get()
                                if v[p]:
                                    continue
                        
                                neighbourhood = []
                                if p[0] - 1 >= 0:
                                    neighbourhood.append((p[0] - 1, p[1]))
                                if p[0] + 1 <= w.shape[0] - 1:
                                    neighbourhood.append((p[0] + 1, p[1]))
                                if p[1] - 1 >= 0:
                                    neighbourhood.append((p[0], p[1] - 1))
                                if p[1] + 1 < w.shape[1]:
                                    neighbourhood.append((p[0], p[1] + 1))
                        
                                for x, y in neighbourhood:
                                    # тут вычисляется расстояние
                                    d_tmp = d[p] + np.abs(w[x, y] - w[p])
                                    if d[x, y] > d_tmp:
                                        d[x, y] = d_tmp
                                        q.put((d[x, y], (x, y)))
                        
                                v[p] = True
                        
                            return d
                        
                        # вызываем алгоритм для двух масок
                        d_fg = dijkstra(xy_fg, likelihood_fg)
                        d_bg = dijkstra(xy_bg, 1 - likelihood_fg)

                        новая нечеткая маска объекта
                        plt.matshow(d_fg, cmap=cm.bwr)
                        plt.show()


                        новая нечеткая маска фона
                        plt.matshow(d_bg, cmap=cm.bwr)
                        plt.show()


                        А теперь относим к объекту все те точки, от которых расстояние до объекта меньше чем расстояния до фона (можно добавить некоторый зазор).


                        margin = 0.0
                        mask = (d_fg < (d_bg + margin)).astype(np.uint8)
                        
                        plt.matshow(mask)
                        plt.show()


                        Можно отправить себя в космос.


                        img_fg = img_input/255.0
                        img_bg = (np.array(Image.open('./../data/background.jpg'))/255.0)[:800, :800, :]
                        
                        x = int(img_bg.shape[0] - img_fg.shape[0])
                        y = int(img_bg.shape[1]/2 - img_fg.shape[1]/2)
                        
                        img_bg_fg = img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :]
                        mask_3d = np.dstack([mask, mask, mask])
                        img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :] = mask_3d*img_fg + (1 - mask_3d)*img_bg_fg
                        
                        plt.imshow(img_bg)





                        Сглаживание маски


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



                        Допустим, у нас есть структурный элемент (СЭ) типа "диск" — бинарная маска диска.


                        • эрозия: прикладываем к каждой точке объекта на оригинальном изображении СЭ так, чтобы совпадал центр СЭ и точка на изображении; если СЭ полностью принадлежит в объекту, то такая точка объекта остается; получается, что удаляются детали, которые меньше чем СЭ, и объект "худеет"; в примере из синего квадрата сделали голубой
                        • наращивание (dilation): на каждую точку объекта накладывается СЭ, и недостающие точки дорисовываются; таким образом закрашиваются дырки меньшие чем СЭ, а объект в целом "толстеет"; на примере из синего квадрата сделали голубой, углы получились закругленные
                        • размыкание (opening): сначала эрозия, потом наращивание тем же СЭ
                        • замыкание (closing): сначала наращивание, потом эрозия тем же СЭ

                        Мы воспользуемся размыканием, что сначала удалит "волосатость" по краям, а потом вернет первоначальный размер (объект "похудеет" после эрозии).


                        mask = morphology.opening(mask, morphology.disk(11))
                        plt.imshow(mask)


                        После применения такой маски результат станет поприятнее:


                        код применения маски
                        img_fg = img_input/255.0
                        img_bg = (np.array(Image.open('./../data/background.jpg'))/255.0)[:800, :800, :]
                        
                        x = int(img_bg.shape[0] - img_fg.shape[0])
                        y = int(img_bg.shape[1]/2 - img_fg.shape[1]/2)
                        
                        img_bg_fg = img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :]
                        mask_3d = np.dstack([mask, mask, mask])
                        img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :] = \
                            mask_3d*img_fg + (1 - mask_3d)*img_bg_fg
                        
                        plt.imshow(img_bg)





                        Накладываем маску


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


                        Подопытный экземпляр
                        img_target = np.array(Image.open('./../data/target.jpg'))
                        img_target = (transform.rescale(img_target, scale=0.5, mode='constant')*255).astype(np.uint8)
                        print(img_target.shape)
                        plt.imshow(img_target)


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


                        img_gray = cv2.cvtColor(img_target, cv2.COLOR_BGR2GRAY)
                        rects_target = detector(img_gray, 0)
                        shape_target = predictor(img_gray, rects_target[0])
                        shape_target = face_utils.shape_to_np(shape_target)

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


                        $\left({\begin{array}{cc} x^1_1, x^2_1, 1 \\ x^1_2, x^2_2, 1 \\ \cdots \\ x^1_{62}, x^2_{62}, 1 \end{array} } \right) \times \left({\begin{array}{cc} a_1^1, a_2^1, a_3^1 \\ a_1^2, a_2^2, a_3^2 \\ a_1^3, a_2^3, a_3^3 \\ \end{array} } \right) = \left({\begin{array}{cc} y^1_1, y^2_1, 1 \\ y^1_2, y^2_2, 1 \\ \cdots \\ y^1_{62}, y^2_{62}, 1 \end{array} } \right) $


                        Данное уравнение легко решается с помощью псевдообратной матрицы:


                        $\large X\cdot A = Y \Rightarrow A = \left(X^T X\right)^{-1}X^T Y$


                        Так и сделаем:


                        # добавим справа к матрицам колонку единиц,
                        # иначе будет только масштабирование и поворот, без переноса
                        X = np.hstack((shape, np.ones(shape.shape[0])[:, np.newaxis]))
                        Y = np.hstack((shape_target, np.ones(shape_target.shape[0])[:, np.newaxis]))
                        # учим оператор
                        A = np.dot(np.dot(np.linalg.inv(np.dot(X.T, X)), X.T), Y)
                        
                        # выбираем точки лица по искомой маске
                        X = np.array([(y, x, 1) for (x, y) in 
                            it.product(range(mask.shape[0]), 
                                           range(mask.shape[1])) if mask[x, y] == 1.0])
                        # вычисляем новые координаты маски на целевом изображении
                        Y = np.dot(X, A).astype(np.int)

                        Накладываем маску
                        img_tmp = img_target.copy()
                        for y, x, _ in Y:
                            if x < 0 or x >= img_target.shape[0] or y < 0 or y >= img_target.shape[1]:
                                continue
                            img_tmp[x, y, :] = np.array([0, 0, 0])
                        
                        plt.imshow(img_tmp)


                        Переносим лицо
                        img_trans = img_target.copy().astype(np.uint8)
                        points_face = {}
                        for ix in range(X.shape[0]):
                            y1, x1, _ = X[ix, :]
                            y2, x2, _ = Y[ix, :]
                            if x2 < 0 or x2 >= img_target.shape[0] or 
                               y2 < 0 or y2 >= img_target.shape[1]:
                                continue
                            points_face[(x2, y2)] = img_input[x1, y1, :]
                        
                        for (x, y), c in points_face.items():
                            img_trans[x, y, :] = c
                        plt.imshow(img_trans)





                        Заключение


                        В качестве домашней работы вы можоте самостоятельно сделать следующие улучшения:


                        • добавить прозрачности по краям, чтобы был плавный переход маски в целевое изображение (matting);
                        • пофиксить перенос на изображения большего размера — точек оригинального лица не хватит, чтобы покрыть все точки целевого лица, таким образом, образуются пропуски между точками; исправить это можно увеличением размера исходного лица;
                        • сделать какую-либо маску, например бурундука Дейла, нанести вручную 62 точки и осуществить перенос (вот вам и маскарад);
                        • использовать какой-нибудь модный нейросетевой детектор точек, который умеет искать большее количество точек.

                        Ноутбук с исходниками находится тут. Приятного времяпрепровождения.


                        Как обычно спс bauchgefuehl за редактуру.

                        Original source: habrahabr.ru (comments, light).

                        https://habrahabr.ru/post/336594/


                        Сегментация лица на селфи без нейросетей

                        Понедельник, 11 Сентября 2017 г. 14:08 + в цитатник
                        mephistopheies сегодня в 14:08 Разработка

                        Сегментация лица на селфи без нейросетей

                          Приветствую вас, коллеги. Оказывается, не все компьютерное зрение сегодня делается с использованием нейронных сетей. Хотя многие стартапы и заявляют, что у них дип лернинг везде, спешу вас разочаровать, они просто хотят хайпануть немножечко. Рассмотрим, например, задачу сегментации. В нашем слаке развернулась целая драма. Одна богатая и высокотехнологичная селфи-компания собрала датасет для сегментации селфи с помощью нейросетей (а это непростое и недешевое занятие). А другая, более бедная и не очень развитая решила, что можно подкупить людей, размечающих фотки, и спполучить базу. В общем, страсти в этих ваших Интернетах еще те. Недавно я наткнулся на статью, где без всяких нейросетей на устройстве делают очень даже хорошую сегментацию. Для сегментации от пользователя требуется дать алгоритму несколько подсказок, но с помощью dlib и opencv такие подсказки легко автоматизируются. В качестве бонуса мы так же сгладим вырезанное лицо и перенесем на какого-нибудь рандомного человека, тем самым поймем, как работают маски во всех этих снапчятах и маскарадах. В общем, классика еще жива, и если вы хотите немного окунуться в классическое компьютерное зрение на питоне, то добро пожаловать под кат.


                          Алгоритм


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



                          Далее делаем следующие шаги:


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

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


                          import
                          %matplotlib inline
                          import matplotlib
                          import numpy as np
                          import matplotlib.pyplot as plt
                          import seaborn as sns
                          sns.set_style("dark")
                          plt.rcParams['figure.figsize'] = 16, 12
                          import pandas as pd
                          from PIL import Image
                          from tqdm import tqdm_notebook
                          from skimage import transform
                          import itertools as it
                          from sklearn.neighbors.kde import KernelDensity
                          import matplotlib.cm as cm
                          import queue
                          from skimage import morphology
                          import dlib
                          import cv2
                          from imutils import face_utils
                          from scipy.spatial import Delaunay




                          Автоматизируем штрихи


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



                          img_input = np.array(Image.open('./../data/input2.jpg'))[:500, 400:, :]
                          print(img_input.shape)
                          plt.imshow(img_input)


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


                          # инстанцируем класс для детекции лиц (рамка)
                          detector = dlib.get_frontal_face_detector()
                          # инстанцируем класс для детекции ключевых точек
                          predictor = dlib.shape_predictor('./../data/shape_predictor_68_face_landmarks.dat')
                          
                          # конвертируем изображение в много оттенков серого
                          img_gray = cv2.cvtColor(img_input, cv2.COLOR_BGR2GRAY)
                          # вычисляем список рамок на каждое найденное лицо
                          rects = detector(img_gray, 0)
                          # вычисляем ключевые точки
                          shape = predictor(img_gray, rects[0])
                          shape = face_utils.shape_to_np(shape)

                          отрисуем ключевые точки
                          img_tmp = img_input.copy()
                          for x, y in shape:
                              cv2.circle(img_tmp, (x, y), 1, (0, 0, 255), -1)
                          plt.imshow(img_tmp)


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


                          # оригинальная рамка
                          face_origin = sorted([(t.width()*t.height(), 
                                                 (t.left(), t.top(), t.width(), t.height())) 
                                                for t in rects], 
                                               key=lambda t: t[0], reverse=True)[0][1]
                          
                          # коэффициенты расширения рамки
                          rescale = (1.3, 2.2, 1.3, 1.3)
                          # расширение рамки, так чтобы она не вылезла за края
                          (x, y, w, h) = face_origin
                          cx = x + w/2
                          cy = y + h/2
                          w = min(img_input.shape[1] - x, int(w/2 + rescale[2]*w/2))
                          h = min(img_input.shape[0] - y, int(h/2 + rescale[3]*h/2))
                          fx = max(0, int(x + w/2*(1 - rescale[0])))
                          fy = max(0, int(y + h/2*(1 - rescale[1])))
                          fw = min(img_input.shape[1] - fx, int(w - w/2*(1 - rescale[0])))
                          fh = min(img_input.shape[0] - fy, int(h - h/2*(1 - rescale[1])))
                          
                          face = (fx, fy, fw, fh)

                          отрисовываем рамки
                          img_tmp = cv2.rectangle(img_input.copy(), (face[0], face[1]), 
                                                  (face[0] + face[2], face[1] + face[3]),
                                                  (255, 0, 0), thickness=3, lineType=8, shift=0)
                          img_tmp = cv2.rectangle(img_tmp, (face_origin[0], face_origin[1]), 
                                                  (face_origin[0] + face_origin[2], face_origin[1] + face_origin[3]),
                                                  (0, 255, 0), thickness=3, lineType=8, shift=0)
                          plt.imshow(img_tmp)
                          


                          Теперь у нас имеется область, которая точно не относится к лицу — всё, что вне красной рамки. Выберем оттуда некоторое количество случайных точек и будем считать их штрихами фона. Также у нас имеются 62 точки, которые точно расположены на лице. Для упрощения задачи я выберу 5 из них: по одной на уровне глаз на краю лица, по одной на уровне рта на краю лица и одну внизу посередине подбородка. Все точки внутри этого пятиугольника будут принадлежать только лицу. Опять же для простоты будем считать, что лицо вертикально расположено на изображении и потому мы можем отразить полученный пятиугольник по оси $y$, тем самым получив восьмиугольник. Все, что внутри восьмиугольника будем считать штрихом объекта.


                          # выбираем вышеописанные пять точек
                          points = [shape[0].tolist(), shape[16].tolist()]
                          for ix in [4, 12, 8]:
                              x, y = shape[ix].tolist()
                              points.append((x, y))
                              points.append((x, points[0][1] + points[0][1] - y))
                          
                          # я не особо в прототипе запариваюсь над производительностью
                          # так что вызываю триангуляцию Делоне,
                          # чтобы использовать ее как тест на то, что точка внутри полигона
                          # все это можно делать быстрее, т.к. точный тест не нужен
                          # для прототипа :good-enough: 
                          hull = Delaunay(points)
                          xy_fg = []
                          for x, y in it.product(range(img_input.shape[0]), range(img_input.shape[1])):
                              if hull.find_simplex([y, x]) >= 0:
                                  xy_fg.append((x, y))
                          print('xy_fg%:', len(xy_fg)/np.prod(img_input.shape))
                          
                          # вычисляем количество точек для фона
                          # примерно равно что бы было тому, что на лице
                          r = face[1]*face[3]/np.prod(img_input.shape[:2])
                          print(r)
                          k = 0.1
                          xy_bg_n = int(k*np.prod(img_input.shape[:2]))
                          print(xy_bg_n)
                          
                          # накидываем случайные точки
                          xy_bg = zip(np.random.uniform(0, img_input.shape[0], size=xy_bg_n).astype(np.int),
                                      np.random.uniform(0, img_input.shape[1], size=xy_bg_n).astype(np.int))
                          xy_bg = list(xy_bg)
                          xy_bg = [(x, y) for (x, y) in xy_bg 
                                   if y < face[0] or y > face[0] + face[2] or x < face[1] or x > face[1] + face[3]]
                          print(len(xy_bg)/np.prod(img_input.shape[:2]))

                          отрисовываем штрихи
                          img_tmp = img_input/255
                          for x, y in xy_fg:
                              img_tmp[x, y, :] = img_tmp[x, y, :]*0.5 + np.array([1, 0, 0]) * 0.5
                          
                          for x, y in xy_bg:
                              img_tmp[x, y, :] = img_tmp[x, y, :]*0.5 + np.array([0, 0, 1]) * 0.5
                          
                          plt.imshow(img_tmp)





                          Нечеткое разделение фона и объекта


                          Теперь у нас есть два набора данных: точки объекта $D_f$ и точки фона $D_b$.


                          points_fg = np.array([img_input[x, y, :] for (x, y) in xy_fg])
                          points_bg = np.array([img_input[x, y, :] for (x, y) in xy_bg])

                          Посмотрим на распределение цветов по RGB каналам в каждом из множеств. Первая гистограмма — для объекта, вторая — для фона.


                          отрисовка распределений
                          fig, axes = plt.subplots(nrows=2, ncols=1)
                          sns.distplot(points_fg[:, 0], ax=axes[0], color='r')
                          sns.distplot(points_fg[:, 1], ax=axes[0], color='g')
                          sns.distplot(points_fg[:, 2], ax=axes[0], color='b')
                          sns.distplot(points_bg[:, 0], ax=axes[1], color='r')
                          sns.distplot(points_bg[:, 1], ax=axes[1], color='g')
                          sns.distplot(points_bg[:, 2], ax=axes[1], color='b')


                          Радует, что распределения отличаются. Это значит, что если мы сможем получить функции, оценивающие вероятность принадлежности точки к нужному распределению, то мы получим нечеткие маски. И оказывается такой способ есть — kernel density estimation. Для заданного набора точек, можно построить функцию оценки плотности для новой точки $x$ следующим образом (для простоты пример для одномерного распределения):


                          $F\left(x\right) = \frac{1}{h\cdot\left|D\right|} \sum_{i =1}^{\left|D\right|} K\left(\frac{x - x_i}{h}\right)$


                          где:


                          • $h$ — параметр сглаживания
                          • $K$ — некоторое ядро

                          Мы для простоты будем использовать Гауссово ядро:


                          $K(u)={\frac {1}{\sqrt {2\pi }}}e^{-{\frac {1}{2}}u^{2}}$


                          Хотя для скорости Гауссово ядро не лучший выбор и если взять ядро Епанечникова, то все будет считаться быстрее. Так же я буду использовать KernelDensity из sklearn, что в итоге выльется в 5 минут скоринга. Авторы этой статьи утверждают, что замена KDE на оптимальную реализацию сокращает расчеты на устройстве до одной секунды.


                          # инстанцируем классы KDE для объекта и фона
                          kde_fg = KernelDensity(kernel='gaussian', 
                                                 bandwidth=1, 
                                                 algorithm='kd_tree', 
                                                 leaf_size=100).fit(points_fg)
                          kde_bg = KernelDensity(kernel='gaussian', 
                                                 bandwidth=1, 
                                                 algorithm='kd_tree', 
                                                 leaf_size=100).fit(points_bg)
                          
                          # инициализируем и вычисляем маски
                          score_kde_fg = np.zeros(img_input.shape[:2])
                          score_kde_bg = np.zeros(img_input.shape[:2])
                          likelihood_fg = np.zeros(img_input.shape[:2])
                          coodinates = it.product(range(score_kde_fg.shape[0]), 
                                                  range(score_kde_fg.shape[1]))
                          for x, y in tqdm_notebook(coodinates, 
                                                    total=np.prod(score_kde_fg.shape)):
                              score_kde_fg[x, y] = np.exp(kde_fg.score(img_input[x, y, :].reshape(1, -1)))
                              score_kde_bg[x, y] = np.exp(kde_bg.score(img_input[x, y, :].reshape(1, -1)))
                              n = score_kde_fg[x, y] + score_kde_bg[x, y]
                              if n == 0:
                                  n = 1
                              likelihood_fg[x, y] = score_kde_fg[x, y]/n

                          В итоге у нас есть несколько масок:


                          • score_kde_fg — оценка вероятности быть точкой объекта $p\left(x \mid D_f\right)$
                          • score_kde_bg — оценка вероятности быть точкой объекта $p\left(x \mid D_b\right)$
                          • likelihood_fg — нормализированная вероятность быть точкой объекта $p_f\left(x\right) = \frac{p\left(x \mid D_f\right)}{p\left(x \mid D_f\right) + p\left(x \mid D_b\right)}$
                          • 1 - likelihood_fg нормализированная вероятность быть точкой фона $p_b\left(x\right) = 1 - p_f\left(x\right)$

                          Посмотрим на следующие распределения.


                          Распределение значений score_kde_fg
                          sns.distplot(score_kde_fg.flatten())
                          plt.show()


                          Распределение значений score_kde_bg
                          sns.distplot(score_kde_bg.flatten())
                          plt.show()


                          Распределение значений likelihood_fg:


                          sns.distplot(likelihood_fg.flatten())
                          plt.show()


                          Вселяет надежду то, что на $p_f\left(x\right)$ есть два пика, и количество точек, принадлежащих лицу, явно не меньше, чем фоновых точек. Нарисуем полученные маски.


                          маска score_kde_fg
                          plt.matshow(score_kde_fg, cmap=cm.bwr)
                          plt.show()


                          маска score_kde_bg
                          plt.matshow(score_kde_bg, cmap=cm.bwr)
                          plt.show()


                          plt.matshow(likelihood_fg, cmap=cm.bwr)
                          plt.show()


                          маска 1 - likelihood_fg
                          plt.matshow(1 - likelihood_fg, cmap=cm.bwr)
                          plt.show()


                          К сожалению, часть косяка двери получилась частью лица. Хорошо, что косяк далеко от лица. Этим-то свойством мы и воспользуемся в следущей части.





                          Бинарная маска объекта


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


                          $d\left(a, b\right) = \left| p\left(a\right) - p\left(b\right) \right|$


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


                          def dijkstra(start_points, w):
                              d = np.zeros(w.shape) + np.infty
                              v = np.zeros(w.shape, dtype=np.bool)
                              q = queue.PriorityQueue()
                              for x, y in start_points:
                                  d[x, y] = 0
                                  q.put((d[x, y], (x, y)))
                          
                              for x, y in it.product(range(w.shape[0]), range(w.shape[1])):
                                  if np.isinf(d[x, y]):
                                      q.put((d[x, y], (x, y)))
                          
                              while not q.empty():
                                  _, p = q.get()
                                  if v[p]:
                                      continue
                          
                                  neighbourhood = []
                                  if p[0] - 1 >= 0:
                                      neighbourhood.append((p[0] - 1, p[1]))
                                  if p[0] + 1 <= w.shape[0] - 1:
                                      neighbourhood.append((p[0] + 1, p[1]))
                                  if p[1] - 1 >= 0:
                                      neighbourhood.append((p[0], p[1] - 1))
                                  if p[1] + 1 < w.shape[1]:
                                      neighbourhood.append((p[0], p[1] + 1))
                          
                                  for x, y in neighbourhood:
                                      # тут вычисляется расстояние
                                      d_tmp = d[p] + np.abs(w[x, y] - w[p])
                                      if d[x, y] > d_tmp:
                                          d[x, y] = d_tmp
                                          q.put((d[x, y], (x, y)))
                          
                                  v[p] = True
                          
                              return d
                          
                          # вызываем алгоритм для двух масок
                          d_fg = dijkstra(xy_fg, likelihood_fg)
                          d_bg = dijkstra(xy_bg, 1 - likelihood_fg)

                          новая нечеткая маска объекта
                          plt.matshow(d_fg, cmap=cm.bwr)
                          plt.show()


                          новая нечеткая маска фона
                          plt.matshow(d_bg, cmap=cm.bwr)
                          plt.show()


                          А теперь относим к объекту все те точки, от которых расстояние до объекта меньше чем расстояния до фона (можно добавить некоторый зазор).


                          margin = 0.0
                          mask = (d_fg < (d_bg + margin)).astype(np.uint8)
                          
                          plt.matshow(mask)
                          plt.show()


                          Можно отправить себя в космос.


                          img_fg = img_input/255.0
                          img_bg = (np.array(Image.open('./../data/background.jpg'))/255.0)[:800, :800, :]
                          
                          x = int(img_bg.shape[0] - img_fg.shape[0])
                          y = int(img_bg.shape[1]/2 - img_fg.shape[1]/2)
                          
                          img_bg_fg = img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :]
                          mask_3d = np.dstack([mask, mask, mask])
                          img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :] = mask_3d*img_fg + (1 - mask_3d)*img_bg_fg
                          
                          plt.imshow(img_bg)





                          Сглаживание маски


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



                          Допустим, у нас есть структурный элемент (СЭ) типа "диск" — бинарная маска диска.


                          • эрозия: прикладываем к каждой точке объекта на оригинальном изображении СЭ так, чтобы совпадал центр СЭ и точка на изображении; если СЭ полностью принадлежит в объекту, то такая точка объекта остается; получается, что удаляются детали, которые меньше чем СЭ, и объект "худеет"; в примере из синего квадрата сделали голубой
                          • наращивание (dilation): на каждую точку объекта накладывается СЭ, и недостающие точки дорисовываются; таким образом закрашиваются дырки меньшие чем СЭ, а объект в целом "толстеет"; на примере из синего квадрата сделали голубой, углы получились закругленные
                          • размыкание (opening): сначала эрозия, потом наращивание тем же СЭ
                          • замыкание (closing): сначала наращивание, потом эрозия тем же СЭ

                          Мы воспользуемся размыканием, что сначала удалит "волосатость" по краям, а потом вернет первоначальный размер (объект "похудеет" после эрозии).


                          mask = morphology.opening(mask, morphology.disk(11))
                          plt.imshow(mask)


                          После применения такой маски результат станет поприятнее:


                          код применения маски
                          img_fg = img_input/255.0
                          img_bg = (np.array(Image.open('./../data/background.jpg'))/255.0)[:800, :800, :]
                          
                          x = int(img_bg.shape[0] - img_fg.shape[0])
                          y = int(img_bg.shape[1]/2 - img_fg.shape[1]/2)
                          
                          img_bg_fg = img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :]
                          mask_3d = np.dstack([mask, mask, mask])
                          img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :] = \
                              mask_3d*img_fg + (1 - mask_3d)*img_bg_fg
                          
                          plt.imshow(img_bg)





                          Накладываем маску


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


                          Подопытный экземпляр
                          img_target = np.array(Image.open('./../data/target.jpg'))
                          img_target = (transform.rescale(img_target, scale=0.5, mode='constant')*255).astype(np.uint8)
                          print(img_target.shape)
                          plt.imshow(img_target)


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


                          img_gray = cv2.cvtColor(img_target, cv2.COLOR_BGR2GRAY)
                          rects_target = detector(img_gray, 0)
                          shape_target = predictor(img_gray, rects_target[0])
                          shape_target = face_utils.shape_to_np(shape_target)

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


                          $\left({\begin{array}{cc} x^1_1, x^2_1, 1 \\ x^1_2, x^2_2, 1 \\ \cdots \\ x^1_{62}, x^2_{62}, 1 \end{array} } \right) \times \left({\begin{array}{cc} a_1^1, a_2^1, a_3^1 \\ a_1^2, a_2^2, a_3^2 \\ a_1^3, a_2^3, a_3^3 \\ \end{array} } \right) = \left({\begin{array}{cc} y^1_1, y^2_1, 1 \\ y^1_2, y^2_2, 1 \\ \cdots \\ y^1_{62}, y^2_{62}, 1 \end{array} } \right) $


                          Данное уравнение легко решается с помощью псевдообратной матрицы:


                          $\large X\cdot A = Y \Rightarrow A = \left(X^T X\right)^{-1}X^T Y$


                          Так и сделаем:


                          # добавим справа к матрицам колонку единиц,
                          # иначе будет только масштабирование и поворот, без переноса
                          X = np.hstack((shape, np.ones(shape.shape[0])[:, np.newaxis]))
                          Y = np.hstack((shape_target, np.ones(shape_target.shape[0])[:, np.newaxis]))
                          # учим оператор
                          A = np.dot(np.dot(np.linalg.inv(np.dot(X.T, X)), X.T), Y)
                          
                          # выбираем точки лица по искомой маске
                          X = np.array([(y, x, 1) for (x, y) in 
                              it.product(range(mask.shape[0]), 
                                             range(mask.shape[1])) if mask[x, y] == 1.0])
                          # вычисляем новые координаты маски на целевом изображении
                          Y = np.dot(X, A).astype(np.int)

                          Накладываем маску
                          img_tmp = img_target.copy()
                          for y, x, _ in Y:
                              if x < 0 or x >= img_target.shape[0] or y < 0 or y >= img_target.shape[1]:
                                  continue
                              img_tmp[x, y, :] = np.array([0, 0, 0])
                          
                          plt.imshow(img_tmp)


                          Переносим лицо
                          img_trans = img_target.copy().astype(np.uint8)
                          points_face = {}
                          for ix in range(X.shape[0]):
                              y1, x1, _ = X[ix, :]
                              y2, x2, _ = Y[ix, :]
                              if x2 < 0 or x2 >= img_target.shape[0] or 
                                 y2 < 0 or y2 >= img_target.shape[1]:
                                  continue
                              points_face[(x2, y2)] = img_input[x1, y1, :]
                          
                          for (x, y), c in points_face.items():
                              img_trans[x, y, :] = c
                          plt.imshow(img_trans)





                          Заключение


                          В качестве домашней работы вы можоте самостоятельно сделать следующие улучшения:


                          • добавить прозрачности по краям, чтобы был плавный переход маски в целевое изображение (matting);
                          • пофиксить перенос на изображения большего размера — точек оригинального лица не хватит, чтобы покрыть все точки целевого лица, таким образом, образуются пропуски между точками; исправить это можно увеличением размера исходного лица;
                          • сделать какую-либо маску, например бурундука Дейла, нанести вручную 62 точки и осуществить перенос (вот вам и маскарад);
                          • использовать какой-нибудь модный нейросетевой детектор точек, который умеет искать большее количество точек.

                          Ноутбук с исходниками находится тут. Приятного времяпрепровождения.


                          Как обычно спс bauchgefuehl за редактуру.

                          Original source: habrahabr.ru (comments, light).

                          https://habrahabr.ru/post/336594/


                          Сегментация лица на селфи без нейросетей

                          Понедельник, 11 Сентября 2017 г. 14:08 + в цитатник
                          mephistopheies сегодня в 14:08 Разработка

                          Сегментация лица на селфи без нейросетей

                            Приветствую вас, коллеги. Оказывается, не все компьютерное зрение сегодня делается с использованием нейронных сетей. Хотя многие стартапы и заявляют, что у них дип лернинг везде, спешу вас разочаровать, они просто хотят хайпануть немножечко. Рассмотрим, например, задачу сегментации. В нашем слаке развернулась целая драма. Одна богатая и высокотехнологичная селфи-компания собрала датасет для сегментации селфи с помощью нейросетей (а это непростое и недешевое занятие). А другая, более бедная и не очень развитая решила, что можно подкупить людей, размечающих фотки, и спполучить базу. В общем, страсти в этих ваших Интернетах еще те. Недавно я наткнулся на статью, где без всяких нейросетей на устройстве делают очень даже хорошую сегментацию. Для сегментации от пользователя требуется дать алгоритму несколько подсказок, но с помощью dlib и opencv такие подсказки легко автоматизируются. В качестве бонуса мы так же сгладим вырезанное лицо и перенесем на какого-нибудь рандомного человека, тем самым поймем, как работают маски во всех этих снапчятах и маскарадах. В общем, классика еще жива, и если вы хотите немного окунуться в классическое компьютерное зрение на питоне, то добро пожаловать под кат.


                            Алгоритм


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



                            Далее делаем следующие шаги:


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

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


                            import
                            %matplotlib inline
                            import matplotlib
                            import numpy as np
                            import matplotlib.pyplot as plt
                            import seaborn as sns
                            sns.set_style("dark")
                            plt.rcParams['figure.figsize'] = 16, 12
                            import pandas as pd
                            from PIL import Image
                            from tqdm import tqdm_notebook
                            from skimage import transform
                            import itertools as it
                            from sklearn.neighbors.kde import KernelDensity
                            import matplotlib.cm as cm
                            import queue
                            from skimage import morphology
                            import dlib
                            import cv2
                            from imutils import face_utils
                            from scipy.spatial import Delaunay




                            Автоматизируем штрихи


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



                            img_input = np.array(Image.open('./../data/input2.jpg'))[:500, 400:, :]
                            print(img_input.shape)
                            plt.imshow(img_input)


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


                            # инстанцируем класс для детекции лиц (рамка)
                            detector = dlib.get_frontal_face_detector()
                            # инстанцируем класс для детекции ключевых точек
                            predictor = dlib.shape_predictor('./../data/shape_predictor_68_face_landmarks.dat')
                            
                            # конвертируем изображение в много оттенков серого
                            img_gray = cv2.cvtColor(img_input, cv2.COLOR_BGR2GRAY)
                            # вычисляем список рамок на каждое найденное лицо
                            rects = detector(img_gray, 0)
                            # вычисляем ключевые точки
                            shape = predictor(img_gray, rects[0])
                            shape = face_utils.shape_to_np(shape)

                            отрисуем ключевые точки
                            img_tmp = img_input.copy()
                            for x, y in shape:
                                cv2.circle(img_tmp, (x, y), 1, (0, 0, 255), -1)
                            plt.imshow(img_tmp)


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


                            # оригинальная рамка
                            face_origin = sorted([(t.width()*t.height(), 
                                                   (t.left(), t.top(), t.width(), t.height())) 
                                                  for t in rects], 
                                                 key=lambda t: t[0], reverse=True)[0][1]
                            
                            # коэффициенты расширения рамки
                            rescale = (1.3, 2.2, 1.3, 1.3)
                            # расширение рамки, так чтобы она не вылезла за края
                            (x, y, w, h) = face_origin
                            cx = x + w/2
                            cy = y + h/2
                            w = min(img_input.shape[1] - x, int(w/2 + rescale[2]*w/2))
                            h = min(img_input.shape[0] - y, int(h/2 + rescale[3]*h/2))
                            fx = max(0, int(x + w/2*(1 - rescale[0])))
                            fy = max(0, int(y + h/2*(1 - rescale[1])))
                            fw = min(img_input.shape[1] - fx, int(w - w/2*(1 - rescale[0])))
                            fh = min(img_input.shape[0] - fy, int(h - h/2*(1 - rescale[1])))
                            
                            face = (fx, fy, fw, fh)

                            отрисовываем рамки
                            img_tmp = cv2.rectangle(img_input.copy(), (face[0], face[1]), 
                                                    (face[0] + face[2], face[1] + face[3]),
                                                    (255, 0, 0), thickness=3, lineType=8, shift=0)
                            img_tmp = cv2.rectangle(img_tmp, (face_origin[0], face_origin[1]), 
                                                    (face_origin[0] + face_origin[2], face_origin[1] + face_origin[3]),
                                                    (0, 255, 0), thickness=3, lineType=8, shift=0)
                            plt.imshow(img_tmp)
                            


                            Теперь у нас имеется область, которая точно не относится к лицу — всё, что вне красной рамки. Выберем оттуда некоторое количество случайных точек и будем считать их штрихами фона. Также у нас имеются 62 точки, которые точно расположены на лице. Для упрощения задачи я выберу 5 из них: по одной на уровне глаз на краю лица, по одной на уровне рта на краю лица и одну внизу посередине подбородка. Все точки внутри этого пятиугольника будут принадлежать только лицу. Опять же для простоты будем считать, что лицо вертикально расположено на изображении и потому мы можем отразить полученный пятиугольник по оси $y$, тем самым получив восьмиугольник. Все, что внутри восьмиугольника будем считать штрихом объекта.


                            # выбираем вышеописанные пять точек
                            points = [shape[0].tolist(), shape[16].tolist()]
                            for ix in [4, 12, 8]:
                                x, y = shape[ix].tolist()
                                points.append((x, y))
                                points.append((x, points[0][1] + points[0][1] - y))
                            
                            # я не особо в прототипе запариваюсь над производительностью
                            # так что вызываю триангуляцию Делоне,
                            # чтобы использовать ее как тест на то, что точка внутри полигона
                            # все это можно делать быстрее, т.к. точный тест не нужен
                            # для прототипа :good-enough: 
                            hull = Delaunay(points)
                            xy_fg = []
                            for x, y in it.product(range(img_input.shape[0]), range(img_input.shape[1])):
                                if hull.find_simplex([y, x]) >= 0:
                                    xy_fg.append((x, y))
                            print('xy_fg%:', len(xy_fg)/np.prod(img_input.shape))
                            
                            # вычисляем количество точек для фона
                            # примерно равно что бы было тому, что на лице
                            r = face[1]*face[3]/np.prod(img_input.shape[:2])
                            print(r)
                            k = 0.1
                            xy_bg_n = int(k*np.prod(img_input.shape[:2]))
                            print(xy_bg_n)
                            
                            # накидываем случайные точки
                            xy_bg = zip(np.random.uniform(0, img_input.shape[0], size=xy_bg_n).astype(np.int),
                                        np.random.uniform(0, img_input.shape[1], size=xy_bg_n).astype(np.int))
                            xy_bg = list(xy_bg)
                            xy_bg = [(x, y) for (x, y) in xy_bg 
                                     if y < face[0] or y > face[0] + face[2] or x < face[1] or x > face[1] + face[3]]
                            print(len(xy_bg)/np.prod(img_input.shape[:2]))

                            отрисовываем штрихи
                            img_tmp = img_input/255
                            for x, y in xy_fg:
                                img_tmp[x, y, :] = img_tmp[x, y, :]*0.5 + np.array([1, 0, 0]) * 0.5
                            
                            for x, y in xy_bg:
                                img_tmp[x, y, :] = img_tmp[x, y, :]*0.5 + np.array([0, 0, 1]) * 0.5
                            
                            plt.imshow(img_tmp)





                            Нечеткое разделение фона и объекта


                            Теперь у нас есть два набора данных: точки объекта $D_f$ и точки фона $D_b$.


                            points_fg = np.array([img_input[x, y, :] for (x, y) in xy_fg])
                            points_bg = np.array([img_input[x, y, :] for (x, y) in xy_bg])

                            Посмотрим на распределение цветов по RGB каналам в каждом из множеств. Первая гистограмма — для объекта, вторая — для фона.


                            отрисовка распределений
                            fig, axes = plt.subplots(nrows=2, ncols=1)
                            sns.distplot(points_fg[:, 0], ax=axes[0], color='r')
                            sns.distplot(points_fg[:, 1], ax=axes[0], color='g')
                            sns.distplot(points_fg[:, 2], ax=axes[0], color='b')
                            sns.distplot(points_bg[:, 0], ax=axes[1], color='r')
                            sns.distplot(points_bg[:, 1], ax=axes[1], color='g')
                            sns.distplot(points_bg[:, 2], ax=axes[1], color='b')


                            Радует, что распределения отличаются. Это значит, что если мы сможем получить функции, оценивающие вероятность принадлежности точки к нужному распределению, то мы получим нечеткие маски. И оказывается такой способ есть — kernel density estimation. Для заданного набора точек, можно построить функцию оценки плотности для новой точки $x$ следующим образом (для простоты пример для одномерного распределения):


                            $F\left(x\right) = \frac{1}{h\cdot\left|D\right|} \sum_{i =1}^{\left|D\right|} K\left(\frac{x - x_i}{h}\right)$


                            где:


                            • $h$ — параметр сглаживания
                            • $K$ — некоторое ядро

                            Мы для простоты будем использовать Гауссово ядро:


                            $K(u)={\frac {1}{\sqrt {2\pi }}}e^{-{\frac {1}{2}}u^{2}}$


                            Хотя для скорости Гауссово ядро не лучший выбор и если взять ядро Епанечникова, то все будет считаться быстрее. Так же я буду использовать KernelDensity из sklearn, что в итоге выльется в 5 минут скоринга. Авторы этой статьи утверждают, что замена KDE на оптимальную реализацию сокращает расчеты на устройстве до одной секунды.


                            # инстанцируем классы KDE для объекта и фона
                            kde_fg = KernelDensity(kernel='gaussian', 
                                                   bandwidth=1, 
                                                   algorithm='kd_tree', 
                                                   leaf_size=100).fit(points_fg)
                            kde_bg = KernelDensity(kernel='gaussian', 
                                                   bandwidth=1, 
                                                   algorithm='kd_tree', 
                                                   leaf_size=100).fit(points_bg)
                            
                            # инициализируем и вычисляем маски
                            score_kde_fg = np.zeros(img_input.shape[:2])
                            score_kde_bg = np.zeros(img_input.shape[:2])
                            likelihood_fg = np.zeros(img_input.shape[:2])
                            coodinates = it.product(range(score_kde_fg.shape[0]), 
                                                    range(score_kde_fg.shape[1]))
                            for x, y in tqdm_notebook(coodinates, 
                                                      total=np.prod(score_kde_fg.shape)):
                                score_kde_fg[x, y] = np.exp(kde_fg.score(img_input[x, y, :].reshape(1, -1)))
                                score_kde_bg[x, y] = np.exp(kde_bg.score(img_input[x, y, :].reshape(1, -1)))
                                n = score_kde_fg[x, y] + score_kde_bg[x, y]
                                if n == 0:
                                    n = 1
                                likelihood_fg[x, y] = score_kde_fg[x, y]/n

                            В итоге у нас есть несколько масок:


                            • score_kde_fg — оценка вероятности быть точкой объекта $p\left(x \mid D_f\right)$
                            • score_kde_bg — оценка вероятности быть точкой объекта $p\left(x \mid D_b\right)$
                            • likelihood_fg — нормализированная вероятность быть точкой объекта $p_f\left(x\right) = \frac{p\left(x \mid D_f\right)}{p\left(x \mid D_f\right) + p\left(x \mid D_b\right)}$
                            • 1 - likelihood_fg нормализированная вероятность быть точкой фона $p_b\left(x\right) = 1 - p_f\left(x\right)$

                            Посмотрим на следующие распределения.


                            Распределение значений score_kde_fg
                            sns.distplot(score_kde_fg.flatten())
                            plt.show()


                            Распределение значений score_kde_bg
                            sns.distplot(score_kde_bg.flatten())
                            plt.show()


                            Распределение значений likelihood_fg:


                            sns.distplot(likelihood_fg.flatten())
                            plt.show()


                            Вселяет надежду то, что на $p_f\left(x\right)$ есть два пика, и количество точек, принадлежащих лицу, явно не меньше, чем фоновых точек. Нарисуем полученные маски.


                            маска score_kde_fg
                            plt.matshow(score_kde_fg, cmap=cm.bwr)
                            plt.show()


                            маска score_kde_bg
                            plt.matshow(score_kde_bg, cmap=cm.bwr)
                            plt.show()


                            plt.matshow(likelihood_fg, cmap=cm.bwr)
                            plt.show()


                            маска 1 - likelihood_fg
                            plt.matshow(1 - likelihood_fg, cmap=cm.bwr)
                            plt.show()


                            К сожалению, часть косяка двери получилась частью лица. Хорошо, что косяк далеко от лица. Этим-то свойством мы и воспользуемся в следущей части.





                            Бинарная маска объекта


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


                            $d\left(a, b\right) = \left| p\left(a\right) - p\left(b\right) \right|$


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


                            def dijkstra(start_points, w):
                                d = np.zeros(w.shape) + np.infty
                                v = np.zeros(w.shape, dtype=np.bool)
                                q = queue.PriorityQueue()
                                for x, y in start_points:
                                    d[x, y] = 0
                                    q.put((d[x, y], (x, y)))
                            
                                for x, y in it.product(range(w.shape[0]), range(w.shape[1])):
                                    if np.isinf(d[x, y]):
                                        q.put((d[x, y], (x, y)))
                            
                                while not q.empty():
                                    _, p = q.get()
                                    if v[p]:
                                        continue
                            
                                    neighbourhood = []
                                    if p[0] - 1 >= 0:
                                        neighbourhood.append((p[0] - 1, p[1]))
                                    if p[0] + 1 <= w.shape[0] - 1:
                                        neighbourhood.append((p[0] + 1, p[1]))
                                    if p[1] - 1 >= 0:
                                        neighbourhood.append((p[0], p[1] - 1))
                                    if p[1] + 1 < w.shape[1]:
                                        neighbourhood.append((p[0], p[1] + 1))
                            
                                    for x, y in neighbourhood:
                                        # тут вычисляется расстояние
                                        d_tmp = d[p] + np.abs(w[x, y] - w[p])
                                        if d[x, y] > d_tmp:
                                            d[x, y] = d_tmp
                                            q.put((d[x, y], (x, y)))
                            
                                    v[p] = True
                            
                                return d
                            
                            # вызываем алгоритм для двух масок
                            d_fg = dijkstra(xy_fg, likelihood_fg)
                            d_bg = dijkstra(xy_bg, 1 - likelihood_fg)

                            новая нечеткая маска объекта
                            plt.matshow(d_fg, cmap=cm.bwr)
                            plt.show()


                            новая нечеткая маска фона
                            plt.matshow(d_bg, cmap=cm.bwr)
                            plt.show()


                            А теперь относим к объекту все те точки, от которых расстояние до объекта меньше чем расстояния до фона (можно добавить некоторый зазор).


                            margin = 0.0
                            mask = (d_fg < (d_bg + margin)).astype(np.uint8)
                            
                            plt.matshow(mask)
                            plt.show()


                            Можно отправить себя в космос.


                            img_fg = img_input/255.0
                            img_bg = (np.array(Image.open('./../data/background.jpg'))/255.0)[:800, :800, :]
                            
                            x = int(img_bg.shape[0] - img_fg.shape[0])
                            y = int(img_bg.shape[1]/2 - img_fg.shape[1]/2)
                            
                            img_bg_fg = img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :]
                            mask_3d = np.dstack([mask, mask, mask])
                            img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :] = mask_3d*img_fg + (1 - mask_3d)*img_bg_fg
                            
                            plt.imshow(img_bg)





                            Сглаживание маски


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



                            Допустим, у нас есть структурный элемент (СЭ) типа "диск" — бинарная маска диска.


                            • эрозия: прикладываем к каждой точке объекта на оригинальном изображении СЭ так, чтобы совпадал центр СЭ и точка на изображении; если СЭ полностью принадлежит в объекту, то такая точка объекта остается; получается, что удаляются детали, которые меньше чем СЭ, и объект "худеет"; в примере из синего квадрата сделали голубой
                            • наращивание (dilation): на каждую точку объекта накладывается СЭ, и недостающие точки дорисовываются; таким образом закрашиваются дырки меньшие чем СЭ, а объект в целом "толстеет"; на примере из синего квадрата сделали голубой, углы получились закругленные
                            • размыкание (opening): сначала эрозия, потом наращивание тем же СЭ
                            • замыкание (closing): сначала наращивание, потом эрозия тем же СЭ

                            Мы воспользуемся размыканием, что сначала удалит "волосатость" по краям, а потом вернет первоначальный размер (объект "похудеет" после эрозии).


                            mask = morphology.opening(mask, morphology.disk(11))
                            plt.imshow(mask)


                            После применения такой маски результат станет поприятнее:


                            код применения маски
                            img_fg = img_input/255.0
                            img_bg = (np.array(Image.open('./../data/background.jpg'))/255.0)[:800, :800, :]
                            
                            x = int(img_bg.shape[0] - img_fg.shape[0])
                            y = int(img_bg.shape[1]/2 - img_fg.shape[1]/2)
                            
                            img_bg_fg = img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :]
                            mask_3d = np.dstack([mask, mask, mask])
                            img_bg[x:(x + img_fg.shape[0]), y:(y + img_fg.shape[1]), :] = \
                                mask_3d*img_fg + (1 - mask_3d)*img_bg_fg
                            
                            plt.imshow(img_bg)





                            Накладываем маску


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


                            Подопытный экземпляр
                            img_target = np.array(Image.open('./../data/target.jpg'))
                            img_target = (transform.rescale(img_target, scale=0.5, mode='constant')*255).astype(np.uint8)
                            print(img_target.shape)
                            plt.imshow(img_target)


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


                            img_gray = cv2.cvtColor(img_target, cv2.COLOR_BGR2GRAY)
                            rects_target = detector(img_gray, 0)
                            shape_target = predictor(img_gray, rects_target[0])
                            shape_target = face_utils.shape_to_np(shape_target)

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


                            $\left({\begin{array}{cc} x^1_1, x^2_1, 1 \\ x^1_2, x^2_2, 1 \\ \cdots \\ x^1_{62}, x^2_{62}, 1 \end{array} } \right) \times \left({\begin{array}{cc} a_1^1, a_2^1, a_3^1 \\ a_1^2, a_2^2, a_3^2 \\ a_1^3, a_2^3, a_3^3 \\ \end{array} } \right) = \left({\begin{array}{cc} y^1_1, y^2_1, 1 \\ y^1_2, y^2_2, 1 \\ \cdots \\ y^1_{62}, y^2_{62}, 1 \end{array} } \right) $


                            Данное уравнение легко решается с помощью псевдообратной матрицы:


                            $\large X\cdot A = Y \Rightarrow A = \left(X^T X\right)^{-1}X^T Y$


                            Так и сделаем:


                            # добавим справа к матрицам колонку единиц,
                            # иначе будет только масштабирование и поворот, без переноса
                            X = np.hstack((shape, np.ones(shape.shape[0])[:, np.newaxis]))
                            Y = np.hstack((shape_target, np.ones(shape_target.shape[0])[:, np.newaxis]))
                            # учим оператор
                            A = np.dot(np.dot(np.linalg.inv(np.dot(X.T, X)), X.T), Y)
                            
                            # выбираем точки лица по искомой маске
                            X = np.array([(y, x, 1) for (x, y) in 
                                it.product(range(mask.shape[0]), 
                                               range(mask.shape[1])) if mask[x, y] == 1.0])
                            # вычисляем новые координаты маски на целевом изображении
                            Y = np.dot(X, A).astype(np.int)

                            Накладываем маску
                            img_tmp = img_target.copy()
                            for y, x, _ in Y:
                                if x < 0 or x >= img_target.shape[0] or y < 0 or y >= img_target.shape[1]:
                                    continue
                                img_tmp[x, y, :] = np.array([0, 0, 0])
                            
                            plt.imshow(img_tmp)


                            Переносим лицо
                            img_trans = img_target.copy().astype(np.uint8)
                            points_face = {}
                            for ix in range(X.shape[0]):
                                y1, x1, _ = X[ix, :]
                                y2, x2, _ = Y[ix, :]
                                if x2 < 0 or x2 >= img_target.shape[0] or 
                                   y2 < 0 or y2 >= img_target.shape[1]:
                                    continue
                                points_face[(x2, y2)] = img_input[x1, y1, :]
                            
                            for (x, y), c in points_face.items():
                                img_trans[x, y, :] = c
                            plt.imshow(img_trans)





                            Заключение


                            В качестве домашней работы вы можоте самостоятельно сделать следующие улучшения:


                            • добавить прозрачности по краям, чтобы был плавный переход маски в целевое изображение (matting);
                            • пофиксить перенос на изображения большего размера — точек оригинального лица не хватит, чтобы покрыть все точки целевого лица, таким образом, образуются пропуски между точками; исправить это можно увеличением размера исходного лица;
                            • сделать какую-либо маску, например бурундука Дейла, нанести вручную 62 точки и осуществить перенос (вот вам и маскарад);
                            • использовать какой-нибудь модный нейросетевой детектор точек, который умеет искать большее количество точек.

                            Ноутбук с исходниками находится тут. Приятного времяпрепровождения.


                            Как обычно спс bauchgefuehl за редактуру.

                            Original source: habrahabr.ru (comments, light).

                            https://habrahabr.ru/post/336594/



                            Поиск сообщений в rss_rss_hh_full
                            Страницы: 1824 ... 1527 1526 [1525] 1524 1523 ..
                            .. 1 Календарь