All streams
Search
Write a publication
Pull to refresh
8
0
Send message

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

Во-первых, потому что у монад kind не , а → *.

Ну как вы верно подметили, в JS "настоящие" типы существуют только в уме. Я не думаю, что у тайпчекеров даже возникнет необходимость поддерживать HKT в полном объеме, потому что речь выше не идет о полностью абстрактной do-нотации, а о do-нотации с указанием конкретного экземпляра монады (пусть даже это будет Free) и о правилах реинтерпретации Free.


Не важно чем является императивный код. Важно что он может неаккуратной строчкой поломать все монады.

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


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

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


Такое препятствие есть: обратная совместимость.

Приведите пример, как введение нового типа MonadPromise, способного биндиться от Thenable (или расширение существующего встроенного Promise методами из интерфейса монад), нарушит обратную совместимость существующего кода.

Во-первых, монада — не тип. Соответственно, писать в одной и той же конструкции do (List) { и do (Monad) { очень странно.

Почему же не тип? Это тайпкласс — т.е. интерфейс, в переводе на "общечеловеческий". В конкретно этой идее, аргументом do передается какой-то объект, который содержит реализации bind, return и fail, которые для уменьшения терминологического разрыва можно назвать bind, pure и catch. bind вообще можно было бы назвать then, если бы не существующий then в Promise.


Т.е. запись do вполне могла бы выглядеть как что-то эдакое:


do ({
    bind(xs, f) { return xs.map(f).flat(); },
    pure(x) { return [x]; },
    [Symbol.hasInstance](x) { return Array.isArray(x) }
}) {
    ...
}

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


Если же вас смущает слово Monad — представьте, что это Free. Мне показалось, что если я назову это Free, то из-за смысловой перегруженности термина "free" и ассоциациями с управлением памятью это вызовет путаницу. Тогда как Monad<T> это вроде как "экземпляр какой-то монады", т.е. "что-то, что реализует типокласс монады", т.е. "что-то, что имеет методы bind и pure". Подставьте вместо Monad любую free-монаду, которую может реинтерпретировать вызывающая сторона в своем контексте, и это будет то, что я хотел сказать.


Во-вторых, не слишком ли много хитрых ключевых слов?

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


  1. эти функции должны вызываться исключительно из блока do. Синтаксической ошибкой сделать вызов функции не в том месте нельзя, плюс функции оборачиваются, переименовываются, переопределяются и т.д. Это плохо.
  2. эти функции должны получать доступ к контексту do. Это подразумевает введение магических аттрибутов в scope, и это плохо.
  3. эти функции должны иметь возможность изменять control flow вызывающей стороны, что делает их очень сильно магическими — т.е. как в примере с List, они должны возвращать управление несколько раз подряд.
  4. если использовать yield или await как ключевое слово вместо run, это не даст возможности заключать do-блоки в генераторы и асинхронные функции.

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


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


В-третьих, точно ли имитация императивного кода на монадах является хорошей идеей? В императивном коде можно сделать много странных вещей, которые в ФП не предусмотрены.

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


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


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


В-четвертых, у нас всё-таки язык со слабой динамической типизацией. Что будет, если пропустить pure? Вот мы пишем — return synchronouslyCombineStuff(foo, bar);, как это должно отработать?

Так как bind() любой монады ожидает, что его коллбэк вернет значение, уже обернутое в монаду того же типа, то если мы напишем что-то типа do (MONAD) { return VALUE; }, рантайм должен бы выполнить аналог VALUE instanceof MONAD, который, в свою очередь, вызовет, возможно, MONAD[Symbol.hasInstance](VALUE), который сможет, например, отличить List от ZipList. Если проверка будет провалена, было бы логичным выбросить TypeError, как это происходит в любых других ситуациях, где ожидаемый тип отличается от фактического.


Тайпчекеры типа TypeScript смогут статически вывести несовместимость типов и дать диагностику еще на стадии компиляции.


В-пятых, Promise — не совсем монада.

Так и есть, именно поэтому я написал про "попутно исправив ошибку дизайна и сделав Promise полноценной монадой". Нет никаких препятствий для того, чтобы сделать Promise монадой с минимальными изменениями. Если вам интересно, я могу написать реализацию условного MonadPromise, который бы сохранял совместимость с экземплярами ES6 Promise и при этом удовлетврял монадическим законам.

Был бы благодарен за аргументацию минусов.

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


  • отмена выполнения
  • синхронизационные примитивы, барьеры
  • автоматическая параллелизация и построение графа зависимостей
  • ограниченная параллелизация, троттлинг, дебаунс
  • software transactional memory
  • автоматический повтор неудачных команд в контексте cqrs command handler
  • аудит трейс выполнения
  • реактивное программирование
  • всякие эффекты из библиотек типа effectful.js
  • работа со значениями Observable напрямую
  • всякие "обычные" монады типа Maybe, Either A, Reader, Writer, State, IO, и т.д.

Увы, так как генераторы, это, по сути, лишь умный хак, а не полноценная do-нотация, все монады и часть перечисленных выше кейсов на чистых генераторах сделать нельзя. Например, недетерминированные вычисления (монада List) на генераторах могли бы выглядеть как-то так (пример намеренно усложненный, чтобы показать варианты использования):


// чистая функция
const str = (x: number) => x.toString();

// монадическая функция над абстрактной монадой
const addMult = (xm: Monad<number>,
                 ym: Monad<number>,
                 zm: Monad<number>): Monad<number | null> => Monad.do(function*() {
    const x: number = yield xm;
    if (x === 1) {
        return pure(null);
    }

    const y: number = yield ym;
    const z: number = yield zm;

    const temp1: number = x + y;
    const temp2: Monad<number> = pure(temp1 * z);
    return temp2;
});

// монадическое выражение над конкретной монадой
const result: List<string> = List.do(function*() {
    const value: number | null = yield addMult(
        [1, 2, 3],
        [4, 5, 6],
        pure(10),
    );

    if (value === null) {
        return [];
    }

    return pure(str(value));
});

expect(result).to.equal(['60', '70', '70', '80', '80', '90']);

Увы, чтобы такой генератор сконвертировался в монаду, удовлетворяющую всем монадическим законам, рантайм должен дать возможность сериализовать состояние генераторов в произвольной точке, клонировать их, перезапускать с произвольной точки и т.д. Без этого не выйдет сделать так, чтобы x = yield [1, 2, 3] вернул управление в функцию три раза, а y = yield [4, 5, 6], соответственно, шесть раз (т.к. в трех случаях до него выполнение не дойдет). С этим есть практические сложности.


Полноценная do-нотация избавила бы рантайм от ограничений, накладываемых на генераторы спецификацией, и позволила бы control flow, невозможный в генераторах, без нарушения обратной совместимости. Возможно, это также позволило бы оптимизации, специфичные для монадического кода. Асинхронные функции и генераторы можно было бы также выразить через фреймворк монад, попутно исправив ошибку дизайна и сделав Promise полноценной монадой.


Как бы это на мой взгляд выглядело? Наверное, если рассматривать нечто идиоматически близкое к JavaScript, а не к Haskell, и учитывая в языке отсутствие тайпклассов, то, я думаю, как-то так:


// чистая функция
const str = (x: number) => x.toString();

// монадическая функция над абстрактной монадой
const addMult = (xm: Monad<number>,
                 ym: Monad<number>,
                 zm: Monad<number>): Monad<number | null> => do (Monad) {
    const x: number = run xm;
    if (x === 1) {
        return pure null;
    }

    const y: number = run ym;
    const z: number = run zm;

    const temp1: number = x + y;
    const temp2: Monad<number> = pure (temp1 * z);
    return temp2;
});

// монадическое выражение над конкретной монадой
const result: List<string> = do (List) {
    const value: number | null = run addMult(
        [1, 2, 3],
        [4, 5, 6],
        pure 10,
    );

    if (value === null) {
        return [];
    }

    return pure str(value);
});

expect(result).to.equal(['60', '70', '70', '80', '80', '90']);

Новые ключевые слова — do, run и pure. Кейворд do синтаксически выражается аналогично while или with, а run и pure синтаксически похожи на yield или await. Пример выше рассахаривался бы в следующий код:


// чистая функция
const str = (x: number) => x.toString();

// монадическая функция над абстрактной монадой
const addMult = (xm: Monad<number>,
                 ym: Monad<number>,
                 zm: Monad<number>): Monad<number | null> => {
    return Monad.bind(xm, (x: number) => {
        if (x === 1) {
            return Monad.pure(null);
        }

        return Monad.bind(ym, (y: number) => {
            return Monad.bind(zm, (z: number) => {
                const temp1: number = x + y;
                const temp2: Monad<number> = Monad.pure(temp1 * z);

                return temp2;
            });
        });
    });
};

// монадическое выражение над конкретной монадой
const result: List<string> = List.bind(addMult(
    [1, 2, 3],
    [4, 5, 6],
    List.pure(10),
), (value: number | null) => {
    if (value === null) {
        return [];
    }

    return List.pure(str(value));
});

expect(result).to.equal(['60', '70', '70', '80', '80', '90']);

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


В do (List) {}, идентификатор List — это обычное JS-выражение, которое бы эвалюировалось в значение, имеющее методы bind, pure и опционально catch (для перехвата throw).


Асинхронные do-выражения, следовательно, могли бы выглядеть как


const whatever: Promise<Data> = do (Promise) {
    let foo: Foo = run fetch("/some/foo");
    let bar: Bar = run fetch("/some/bar");
    return pure synchronouslyCombineStuff(foo, bar);
}

и были бы аналогичны


const whatever: Promise<Data> = (async () => {
    let foo: Foo = await fetch("/some/foo");
    let bar: Bar = await fetch("/some/bar");
    return synchronouslyCombineStuff(foo, bar);
})();

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


Что еще обычно из примеров показывают, Maybe? В таком случае, для сниппета


const result = do (Maybe) {
    const x = run foo();
    const y = run bar(x);
    return pure y;
}

Если foo вернет Nothing, то до bar управление даже не дойдет — т.е. рассахаренный код просто не вызовет то, что будет передано в Maybe.bind(foo(), x => { /* вот этот код не будет вызван */ }). Если же foo вернет Just<что-нибудь>, то это что-нибудь будет присвоено x.


В общем-то, наверное, кроме, на мой взгляд, богатых возможностей по организации кода, такая do-нотация была бы еще и тайпчекер-френдли, т.к. можно было бы выводить типы используемых в run-выражениях монад по контексту do и ругаться, если кто-то пытается runить List в контексте Maybe или наоборот; или пытается вернуть из do-выражения не-монадическое значение — например, случайный голый return x вместо return pure x, если x — не монада.


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

Когда увидел "do expressions" в душе проснулась наивная надежда увидеть first class monads. Ха. Ага. Уже, ща.


Увы.

Привет из 2020 года.


https://twitter.com/sharifshameem/status/1284421499915403264


https://twitter.com/sharifshameem/status/1284095222939451393


https://twitter.com/jsngr/status/1284511080715362304


Всего 7 лет понадобилось, чтобы рассказ из фантастики стал печальной реальностью.

Лучше используйте для этих целей AES256, если вам нужно надежно, и RC4, если вам нужно быстро. Используйте HMAC-SHA3, если вам нужна симметричная подпись, или ECDSA, если нужна асимметричная. Используйте ECDH, если вам нужно согласовать симметричный ключ.


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


def get_pks_sha256(user1_md5, user2_md5, plaintext, salt):
    t = time_ns()

    sec = t // 1000000000
    usec = ((t % 1000000000) % 0x100000) % 0xF4240
    uniqid = sprintf('%08x%05x', sec, usec)

    attach_md5 = md5_hex(
        sha512_hex(utf8_encode(plaintext + uniqid + salt))
        + sha512_hex(utf8_encode(salt))
    )

    return sha512_hex(user1_md5 + attach_md5 + user2_md5)

def kdf(pks_sha512, key):
    ki = 0
    pos = 0

    for i in [0..]:
        ki = (ki + hex2dec(pks_sha512[i % 32])) % len(key)

        d = (i % 2) ? -1 : 1
        pos = (abs(300 * ord(key[ki]) + i * d)) % 2000

        if pos <= 65:
            pos = 65 * max(pos, 1) + 1

        if pos >= 2000:
            pos = 200

        yield pos

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


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


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

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

Ютуб все-таки не телевидение, look&feel другой. Можно было бы на основе ютуба сделать, это да.

По описанию задумки я представлял себе устройство аля interdimensional cable из R&M — при помощи кнопок "вперед" и "назад" выполняется навигация по некоему списку тегов, и устройство динамически формирует плейлисты на основе выбранного тега, отслеживает списки, что было просмотрено, и уточняет плейлист на основе рекомендаций.


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


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

Ошибки tsc не препятствуют выдаче javascript-кода. Т.е. даже неверный код вида const x: number = "foobar"; все равно скомпилируется, но не протайпчекается. У вас наверняка включена опция noEmitOnError — отключите ее.

Подозреваю, что "скрипты на S", это просто .S-файлы, т.е. ассемблерные листинги.

Было бы интересно сравнить clang и icc. По идее, icc как раз должен использовать все, что можно, чтобы ускорить код, т.к. по идее построен с учетом знаний о внутренней архитектуре интеловских процессоров.

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

Прочитайте мой комментарий еще раз. Я процитирую важную часть, которую вы, возможно, пропустили.


f(readline) чиста, если только readline чиста.
f(g) чиста, если только g чиста.

Влияет ли переименование аргумента на чистоту описываемых функций?

Что забавно, что чистыми в полном понимании функциями в C++ являются функции, отмеченные как const, а не как pure, т.к. pure разрешает доступ к глобалам, тогда как const требует, чтобы функция использовала только другие const-функции и const-значения.

f(readline) чиста, если только readline чиста.
f(g) чиста, если только g чиста.


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


Более того, если с g() еще черт его знает, что она делает, то readline(), очевидно, возвращает каждый раз разные результаты, и чистой не может быть по определению.

нет. Эта функция нечистая — одним из требований чистоты является ссылочная прозрачность (referential transparency), т.е. должна быть возможность в любом месте заменить функцию на результат ее вычисления, и наоборот — результат вычисления на вызов этой функции.


Заменить эту функцию на "John" нельзя, т.к. потеряется печать строк на экран и чтение ввода.


Что нужно, чтобы эта функция была чистой? Сделать так, чтобы она возвращала список инструкций, которые нужно выполнить.


Т.е. вот так:


function f(readline, printline)
    actions = []
    actions.push("call", printline, "Enter your name:")
    actions.push("call_and_assign", "name", readline)
    actions.push("call", printline, "Hello, {} :)", ref("name"))
    actions.push("return", ref("name"))
    return actions
end

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


function f(readline, printline)
    return printline("Enter your name:").then(=> {
        return readline().then(name => {
            return printline("Hello, $name :)").then(=> {
                return wrap(name)
            })
        })
    })
end

Эта функция все еще чиста, и возвращает другие чистые функции, замыкающиеся на чистые функции. Подразумевается, что printline и readline тоже возвращают монаду IO или ее аналог.


Причем если императивный язык поддерживает вещи типа yield или await, то можно записать это так:


function f(readline, printline)
    yield printline("Enter your name:")
    name = yield readline()
    yield printline("Hello, $name :)")
    yield wrap(name)
end

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

Information

Rating
Does not participate
Registered
Activity

Specialization

Software Architect