Как стать автором
Обновить

Комментарии 60

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

"современные языки программирования" - улыбнуло. Внезапно в 70-ых lisp был в состоянии решить такие проблемы.

Внезапно в 70-ых перед lisp такой проблемы даже не стояло: формат JSON (JavaScript Object Notation) появился только спустя 30 с лишним лет, - когда lisp уже успел обрести просветление и превратиться в миф.

Не будьте столь категоричны. Во первых - есть реализации json-a под lisp. Во вторых - проблема не в джейсоне, как в таковом. Просто, в случае lispa - математику не хипстеры писали.

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

За хипстеров не скажу, но недавно в обсуждении провели проверку вычисления современными средствами (языками, калькуляторами и т.д.) тангенса гугола (tg(10^100)), порядка десятка инструментов бодро выдали ответ(!), результаты получились от -5 до +6. И расстраивает то, что ни один не намекнул, что с такими числами обращаться не умеет.

А тангенсы в градусах или в радианах брали и возвращали значения?

А что на это говорила документация (языков, калькуляторов и т.д.)?

В JS говорит что нельзя смешивать BigInt с другими типами. Или говорит что нельзя конвертировать нецелые числа в BigInt.(чтобы конвертировать градусы в радианы).

Мне кажется что в мире программирования есть реализация чего угодно под что угодно)

Не несите чуши. Математика в JS как и в большинстве современных языков программирования реализована согласно стандарту IEEE 754.

Есть еще Fortran)

Я в свое время обжёгся с этим bigint. Клиент и сервер на python где поддержка больших чисел уже много лет из коробки. Сериализация и десериализация JSON на тестах работает четко. Но в продакшн между ними оказался API Gateway от известного вендора, со скриптигном на JS, вытворявший все те непотребства, что описаны в статье. В итоге все переделали на строки.

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

Как PoC библиотека просто класс и думаю ей можно найти массу применений. Но гонять bigint в JSON я бы не рекомендовал.

Отличный пример, спасибо, что поделились! Еще интересный момент, что Dev Tools в Google Chrome также показывает значения в отформатироанном JSON уже некорректно, что иногда сильно сбивает с толку :)

Но гонять bigint в JSON я бы не рекомендовал.

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

Так там и bigint не пригодится. Всё влезет в обычный number.

ок, ОЧЕНЬ милых котиков :)

Верхний предел int64 со знаком примерно 10^19. Число людей в обозримом будущем врятли превысит 10^11. условимся, что на каждого человека приходится в среднем не более 100 аккаунтов. Тогда нам потребуется хранить число максимум 10^13, что даёт нам запас ещё на 6 порядков.
Кстати, в некоторых случаях вполне прагматичным костылём будет именно хранение числа в строке, и преобразавоние его в число только там, где с ним непосредственно нужно производить числовые операции. Нужно всегда смотреть по ситуации.

из рфц: This specification allows implementations to set limits on the range and precision of numbers accepted

Справедливо, но дело в том, что другие ЯП дают возможность разработчику самому решить как парсить JSON, в этом случае "implementation" (из вашей отсылки) это код разработчика. А авторы EcmaScript решили это за нас и не оставили нам никакого выбора (строго наложив ограничения IEEE 754). Собственно по этому мы и работает над расширением стандарта :)

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

Да, доступ к результатам работы токенайзера даёт ещё больше свободы. Например, я могу поставить себе парсер для которого правила интерпретации задаются какой-нибудь JSON Schema. Но это довольно специфическая задача, и мне кажется будет лучше иметь это в виде отдельного API, а не костылей к JSON.parse(). За одно там можно было бы предложить решение для парсинга потоковых форматов типа json-lines.

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

А в чем сложность с json-lines? Сканируешь себе поток и скармливаешь строки парсеру постепенно. Не уверен что эту задачу в библиотеку стоит выделять, хотя, конечно, можно это сделать.

Сканируешь себе поток и скармливаешь строки парсеру постепенно.

А можно было бы проходить json-lines поток в один проход - без загрузки и буферизации каждой отдельной строки в памяти (они могут быть довольно длинными, что сказывается как на потреблении памяти так и на latency). Вот ещё кейс, когда по сути нужен лишь токенайзер, без парсинга: https://habr.com/ru/company/quadcode/blog/660229/ .

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

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

А что Вы имеете ввиду под "все равно делает парсинг, даже когда это фактически не требуется" и "создаёт массу новых временных объектов"? Не совсем понятно.

JSON.parse(

'{ "foo": 123456789123456789123 }',

(key, value, context) => (

(key === 'foo' ? BigInt(context.source) : value)

)

);

Здесь для "foo" value не используется, однако парсер его все равно парсит, создаёт и передаёт в колбек, как велит интерфейс функции JSON.parse(). А если мы пишем, например, санитайзер - как делали ребята по ссылке в комменте выше, когда интересуют только ключи - то парсить value не требуется вообще - нужны лишь данные от токенайзера (в вашем примере key и context.source). Получается парсер делает кучу ненужной работы.

Чтобы передать в callback значение context, в вашей редакции каждый раз создаётся новый объект. По одному разу на каждую ноду.

Причина у проблем общая - добавление ортогональной по сути фичи в интерфейс JSON.parse().

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

Интересно, в каких ещё ситуациях может потребоваться доступ к сырым строкам? В JSON разных типов данных не много. Тема с number / bigint, как видно, отпадает - надёжнее передавать сразу строкой. string и так строки. Из примитивных остаются только "true", "false" и null.

А можно ли передать в JSON зацикленные структуры или когда некоторые структуры являются ссылочными, а JSON стрингует их как отдельные? Есть какие то методы?

Нельзя (по стандарту). Но есть библиотеки для сериализации (только это уже не JSON).

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

Уже пришлось поколхозить. Сколько лет JSON существует, а без колхоза никуда.

Он просто не для циклических структур, берите что то подходящее и не колхозьте

Пример либы (в описании как раз излагается принцип): https://github.com/WebReflection/flatted

Спасибо, глянул. Но только потом подумал, что приёмниками такого «нового» формата могут быть только клиенты JS. C# NewtonSoft JSON не поддерживает ссылки внутри Json. Шьорт побьери. Эта их жажда угодить клиенту, сделав ключ .Parent, чего в обычном JS точно нет, сломала возможность делать множественные ссылки.

Ну, портировать либу на другие языки нетрудно.

Но если у вас проблема только с ключом .Parent – то проще всего сериализовать без него (на js – в JSON.serialize передать скипающую его функцию), а после десериализации обходить дерево, восстанавливая его.

Но JSON – это всё-таки для "чистых данных", а не формат сериализации общего назначения.

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

Я думаю, человек имел ввиду не вложенности внутри объекта класса, а рекурсивная циклическая зависимость, такое Newtonsoft из коробки не потянет, нужно специальную опцию сериализации включать ReferenceLoopHandling.Ignore, чтобы не падал с эксэпшэном.

Наверное это не совсем соотносится с тематикой статьи, но, тем не менее, рекомендую посмотреть как этот вопрос решается в стандарте JSON Schema (https://json-schema.org/understanding-json-schema/structuring.html) возможно будет полезно.

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

Если бы у меня стояла ОСТРАЯ нужда, то можно сделать проход по дереву объектов в поисках ссылок на элементы структуры, после этого создать отдельное поле в JSON с иерархией и путем до оригиналов. После десериализации просто восстанавливать полное представление. У Articy Draft в экспорте json есть модели объектов и иерархия, можно попробовать такой подход тоже.
Как вариант, можно добавить префикс для строк, после которого вставлять путь до оригинала. В любом случае так лучше не делать)

Уже было в симпсонах. Авторы YAML решили предусмотреть и зацикленность ссылки и еще тысячу других няшных фич. В итоге вырастили кактус который теперь приходится всем жрать https://noyaml.com/

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


This specification allows implementations to set limits on the range
and precision of numbers accepted. Since software that implements
IEEE 754-2008 binary64 (double precision) numbers [IEEE754] is
generally available and widely used, good interoperability can be
achieved by implementations that expect no more precision or range
than these provide, in the sense that implementations will
approximate JSON numbers within the expected precision. A JSON
number such as 1E400 or 3.141592653589793238462643383279 may indicate
potential interoperability problems, since it suggests that the
software that created it expects receiving software to have greater
capabilities for numeric magnitude and precision than is widely
available.

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




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

Наверное, все так и делают. Мы тоже бигнамбы строками передаем

Забыли упомянуть, что в JavaScript под integer не 64 бита, а 53.

64 - это в Java.

JSON является одним из очень простых, но в то же время эффективных языков для хранения и передачи данных.

JSON - это текстовый формат, а не язык.

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

Формальный язык, Википедия

И в добавок - эффективным будет бинарный формат, а json явно на порядок медленнее и обьемнее

Именно по этому я тоже написал свою библиотеку. Но я пошел дальше - JS/JSON объекты кодируются/декодируются в бинарный формат с различными оптимизациями. Есть поддержка всех базовых JS типов включая новые BigInt/Symbol. Все работает как в браузере так и на nodeJS, а так же покрыто 100% тестами. Если нужно добавить уникальные кастомные типы - это тоже есть.
https://github.com/superman2211/xobj/tree/master/packages/core

А во что сериализуется symbol?

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

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


А вот с функциями вопрос интереснее...

хотя я не представляю зачем вообще оно нужно

Оно имело бы некоторый смысл, если бы в сериализации объектов поддерживались ключи-символы. Но автор случайно забыл про Object.getOwnPropertySymbols...

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

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


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

Но невозможность восстановить уникальный символ — это ключевое и общеизвестное свойство символов.

Да идея со словарем отличная!

Да вы правы - символ уникален в пределах сообщения. Symbol как значение передать в принципе нельзя.

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

Аналогично можно и символами сделать ) Создавать некую таблицу.

И они должны быть строго анонимные

Причем тут анонимность? Проблема в другом. Допустим, функция создана так:

const func = ((x) => () => x)(123);

func() всегда возвращает 123

func.toString() дает строку "() => x". Очевидно, что десериализованная из такой строки функция совсем необязательно будет возвращать 123.

Честно говоря удивлен, но оказалось что-то подобное уже есть https://www.npmjs.com/package/serialize-closures. Причем сделано снаружи, а не изнутри движка (так-то DevTools показывают внутренности объектов, поэтому вопрос сериализации замыканий это лишь вопрос доступа к соответствующим API).

Не, движок и его отладочные интерфейсы тут вообще ни при чём:


Note: only functions whose code has first been processed by ts-closure-transform are eligible for serialization.

Начать следовало бы с того - как расшифровывается JSON: JavaScript Object Notation

Уже с єтого момента, должен был бы сработать маячек на тему, а не связаны ли особенности, которые я обсуждаю в рамках своей заметки чем-то фундаментальным, что связано с самим языком?

Открыв официальную спецификаци, языка JavaScript, а именно главу касающуюся парсинга переданной строки в метод обьекта JSON https://tc39.es/ecma262/multipage/structured-data.html#sec-json.parse, мы должны были бы обратить внимание на строку алгоритма парсинга:

Let completion be Completion(Evaluation of script).

которая снимает ровно все вопросы, которые бы мог бы себе задать программист языка JS, а именно что будет если я подсуну некорректные с точки зрения синтаксиса языка данные и получить испчерпывающий ответ на єтот вопрос - произойдет ровно тоже самое, что должно произойти былобы если бы Вы тоже самое сделали используя обычный (не json) код JS.

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

То есть: JSON.parse работает строго в рамках того как он и доажен работать и решение "заявлрнной" проблемы лежит не в плоскости изменения метода parse, который не является ничем иным как официальным (єталонным) алгоритмом парсинга JS кода), но только в введение отдельного апи в рамках host системы которая сделает работу за Вас.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории