Активно разрабатываю на JavaScript/TypeScript (browser/node/electron, svelte/vue/hapi/etc...) и на Rust (mio/tokio/hyper/neon/etc...).
Менторю начинающих разработчиков.
Занимаюсь FrontOps (разрабатываю loader'ы и plugin'ы для webpack/rollup, разрабатываю вспомогательный тулкит, настраиваю окружение).
Имею научный интерес к компиляторам, анализу AST деревьев, транспиляции и кодогенерации.
Проповедую функциональное программирование.
В свободное время пишу свой учебник по современному JavaScript.
Контакты
Местоположение
Россия, Санкт-Петербург и область, Санкт-Петербург

Достижения

Все достижения (45)

Наибольший вклад в теги

Все теги (240)

Лучшие ответы пользователя

Все ответы (1279)
  • Как асинхронная программа(event loop) понимает, что пришел ответ от сервера?

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

    Итак, мы пишем на нашем высокоуровневом любимом языке, неважно JS/Rust/C#/Scala/Python или любой другой. В современном мире у нас скорее всего есть какая либо абстракция для работы с асинхронными апи, предоставляемая или стандартной библиотекой языка или сторонними библиотеками. Она может быть примитивной и основанной на колбэках или более продвинутой, вроде Future/Promise/Task или чем-то подобным. Иногда наш язык предоставляет синтаксис наподобие async/await для более простой работы с этими абстракциями, а иногда асинхронная работа может вообще быть скрыта от нас в рантайме языка, например как с горутинами в Go. Но в любом случае где-то под капотом у нас будет event-loop, а иногда и не один, так как никто не запрещает нам писать многопоточку в то же время используя асинхронные вызовы.

    Сам event-loop - это не более чем обычный while(true) или любой другой бесконечный цикл. И внутри этого цикла наша программа имеет доступ на извлечение к некоторой очереди (если не знаете, что это за структура данных, то погуглите), которая содержит в себе результаты уже обработанных задач. Программа берет очередной результат, находит ожидающий ее колбэк/Promise/Future/Task и запускает выполнение ожидающего кода. Очередей опять же может быть несколько и обрабатываться они могут по разному, но это не важно. Важно то, что наш основной поток (или потоки) ничего не знают, о том как выполняются асинхронные задачи. Он лишь смотрит, есть ли в очереди результат, и если есть - обрабатывает его, а если нет, то принимает решение или выйти из цикла (и завершить поток, а иногда и весь процесс) или уснуть пока новых результатов не появится.

    Но откуда же в очереди берутся результаты? Надо понимать, что асинхронная программа почти всегда многопоточная и результат операций попадает в очередь из фоновых потоков, которые просто блокируются в ожидании нужного ресурса (или сразу многих ресурсов, если используют системные апи вроде epoll или kqueue). Как правило такие фоновые потоки большую часть времени находятся в состоянии ожидания, а значит не потребляют ресурсы CPU и не попадают в планировщик ОС. Такая простая модель действительно позволяет сильно экономить ресурсы по сравнению с моделью, где множество потоков выполняют по 1 задаче и самостоятельно ожидают свои запросы.

    Важно отметить, что в современном мире даже на среднеуровневых языках, вроде C или C++, не говоря уже о высокоуровневых, не реализуют асинхронность сами. Во-первых, на разных ОС для этого используются разные апи. Во-вторых, эти апи на разных ОС умеют обрабатывать разные типы ресурсов (с сетью вроде как умеют работать все основные ОС, но помимо сети асинхронно можно работать с пользовательским вводом, диском и периферийными устройствами, вроде сканеров, вебкамер и прочего цепляемого в usb). Наибольшую популярность (ИМХО) имеет кроссплатформенная библиотека libuv, хотя в Rust принято использовать mio (или даже абстракции над ней, вроде tokio), в C# подобные механизмы есть в .NET Core, а в Go оно уже зашито
    в те самые 1.5МБ рантайма, что Go засовывает в каждый бинарь
    (там правда еще и GC, но один фик это много и достойно вынесения в динамическую либу)


    Ок. С прикладным кодом вроде разобрались. А что же происходит в ядре ОС? Ведь, как писалось выше, у нас даже есть апи, чтоб ждать запросы пачкой. Все просто. Ядра ОС стали асинхронными еще до того, как это стало мейнстримом, если мы конечно имеем дело не с ОС реального времени (но у нас же винда/линь/мак/фряха, а не ОС для бортового компа боинга, где это критично). Смотрите, когда что-то происходит на внешней периферии (ну например диск запрошенные данные прочитал или по сети данные пришли, или юзер мышкой дернул), то формируется прерывание. CPU реально прерывает свою текущую работу и бежит смотреть что случилось, точнее вызывает обработчик предоставленный ОС. Но у ОС то есть основная работа, поэтому она скорее старается освободить обработчик и просто скидывает все данные в оперативку, а разбираться будет потом, когда очередь дойдет. Ничего не напоминает? Очень похоже, на то что происходило в event-loop, только вместо фоновых потоков "результаты" попадают в очередь из прерываний. А уже когда-то потом ОС отдаст данные в драйвер устройства, ну и т.д., пока они не дойдут до нашего прикладного приложения. Вот и все, никакой магии.
    Ответ написан
  • Что такое обратный вызов в программировании?

    bingo347
    @bingo347
    Бородатый программер
    Что такое обратные вызовы?
    Я знаю только что это функция которая передается как аргумент в другую функцию.
    В принципе, можно и так сказать. Если быть более точным - это вызов функции переданной в качестве аргумента.
    Почему они так называются?
    Это игра слов. На английском callback - это не только обратный вызов, но и обратный звонок (по телефону). Данная абстракция позволяет вызываемому коду вызвать вызывающий код, подобно тому как собеседник может перезвонить Вам позднее, если Вы сообщите ему куда.
    В чем их смысл и зачем нужны?
    В принципе я уже ответил, они нужны для возможности вызываемому коду вызвать вызывающий код. Это позволяет строить высокоуровневые абстракции, вроде обобщенных функций или асинхронных функций.
    Обобщенные функции позволяют не писать однотипный код, снижая тем самым вероятность ошибок, а с помощью обратных вызовов они могут принимать в себя фрагменты кода, которые могут меняться от использования к использованию. Для примера, абстрагируем цикл от 0 до n на C:
    // абстракция цикла
    void each(int n, void (*callback)(int, void*), void* closure_data) {
      if(n <= 0) { return; }
      for(int i = 0; i < n; i++) {
        (*callback)(n, closure_data);
      }
    }
    
    // колбэк - тело цикла, вариант 1
    void cb_body1(int i, void* _) {
      printf("%d", i);
    }
    
    // колбэк - тело цикла, вариант 2
    void cb_body2(int i, void* acc) {
      int* normalized_acc = (int*)acc;
      *normalized_acc += i;
    }
    
    int main() {
      each(10, cb_body1, null); // напечатает строки 0, 1, ...9
    
      int result = 0;
      each(10, cb_body2, &result); // посчитает в result сумму чисел от 0 до 9
      printf("%d", result);
      return 0;
    }

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

    Так же стоит заметить, что во многих высокоуровневых языках наряду с обратными вызовами используется механизм замыканий, который позволяет объявлять функции внутри других функций и захватывать окружающие переменные. Но нужно понимать, что это лишь компиляторный сахар, и на самом деле в функцию просто передаются указатели на захваченные переменные в качестве аргументов, подобно тому, как я сделал это руками в примере выше, с помощью аргумента closure_data в функции each. Обычно компилятор создает для этого анонимные структуры (C++, Rust) или анонимные классы (C#), которые хранят указатель на функцию и указатели на окружение. А в некоторых языках, например в js, замыкания возведены в абсолют, и каждая функция является замыканием.
    Ответ написан
  • Простым языком о замыканиях?

    bingo347
    @bingo347 Куратор тега JavaScript
    Бородатый программер
    1. Для чего замыкание существуют?
    Для инкапсуляции данных.
    В ООП есть модификаторы доступа (private, protected), которые закрывают доступ к данным извне класса, но позволяют обращаться к ним из методов.
    В ФП для этой задачи используют замыкания, закрывая данные внутри функции. Из вне данные недоступны, но вложенные функции имеют к ним доступ.

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

    Хоть и просили без примеров, но на примере показать проще:
    // makeCounter - внешняя функция
    function makeCounter(initialValue) {
      var value = +initialValue || 0;
      // counter - внутренняя функция
      // она использует переменную value из внешней функции
      // что-бы это было возможным, для counter создается замыкание,
      // в котором хранится переменная value
      // переменная initialValue функции counter не нужна, поэтому ее можно "забыть"
      return function counter() {
        return value++;
      };
    }
    
    // у нас 3 экземпляра функции counter
    var counter1 = makeCounter();
    var counter2 = makeCounter();
    var counter3 = makeCounter(100);
    // и для каждой есть своя переменная value
    console.log(counter1()); // 0
    console.log(counter1()); // 1
    console.log(counter2()); // 0
    console.log(counter1()); // 2
    console.log(counter3()); // 100
    
    // а вот получить как-то напрямую переменную value мы не можем
    // инкапсуляция нам не дает поломать данные
    Ответ написан
  • Как часто используются дескрипторы, декораторы и bind, call, apply?

    bingo347
    @bingo347 Куратор тега JavaScript
    Бородатый программер
    Обо всем по порядку

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

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

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

    call и apply позволяют вызвать функцию с нужным контекстом, разница в том что apply принимает 2 аргумента - контекст и массиво-подобный объект содержащий аргументы, а call принимает произвольное число аргументов: 1й - контекст, последующие передаются как аргументы функции. Используется постоянно.

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

    Ну и напоследок, конструкция var self = this; позволяющая сохранить контекст в замыкании уже потихоньку уходит в прошлое, благодаря стрелочным функциям из es2015
    Ответ написан
  • Почему умножение работает быстрее деления?

    bingo347
    @bingo347
    Бородатый программер
    По сути процессор умеет работать только с битами
    С целыми числами все выглядит давольно просто:
    1й бит числа определяет знак числа - 0 для положительных и 1 для отрицательных
    У нас есть операции для управления битами:
    • ~ инверсия
    • & конъюнкция
    • | дизъюнкция
    • >> сдвиг вправо
    • << сдвиг влево

    введем арифметические операции по порядку:
    1. Сложение, сводится по сути к сложению битов в столбик
    2. Отрицание или смена знака, -a выражается как ~a + 1
    3. Вычитание a - b выражается через a + -b
    4. Умножение, тут несколько вариантов, зависит от компилятора и его оптимизатора:
      1. Умножение на степени 2 можно представить сдвигом влево: a * 8 приводится к a << 3 т.к. 8 - это 3я степень 2
      2. Простые случаи вроде a * 3 можно заменить на a + a + a
      3. Случаи по сложнее a * 11 складывать a 11 раз само с собой не оптимально
        раскладываем 11 на степени 2: 11 = 8 + 2 + 1
        вычисляем (a << 3) + (a << 1) + a



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

    Вот такая вот высшая математика над двоичными числами происходит "за кадром" наших с виду простых программ :)
    Ответ написан

Лучшие вопросы пользователя

Все вопросы (21)