Распространенные шаблоны JSON в Haskell, Rust и TypeScript
Многие веб-разработки так или иначе трансформируют JSON. В TypeScript / JavaScript это просто, поскольку JSON встроен в язык. Но можем ли мы также добиться хорошей эргономики в Haskell и Rust?
Дорогой читатель, я рад, что ты спросил! 🙌
Сравнения, которые мы увидим, не предназначены для того, чтобы показать, лучше ли один подход, чем другой. Вместо этого он предназначен для ознакомления с общими шаблонами на нескольких языках. В этом посте мы будем использовать несколько инструментов и библиотек.
Ядро работы с JSON в Haskell и Rust покрывается:
- Aeson: библиотека сериализации / десериализации Haskell JSON¹.
- Serde: библиотека сериализации / десериализации Rust JSON.
Затем эргономика в Haskell улучшается за счет использования одной из следующих опций²:
- Lens: тяжеловесная библиотека для трансформации и работы с записями (и многого другого!) ³.
- Record Dot Syntax: новое расширение языка Haskell, которое недавно было принято руководящим комитетом GHC⁴.
Мы рассмотрим типичные варианты использования, наблюдаемые в кодовых базах TypeScript / JavaScript, и посмотрим, как добиться того же в Haskell и Rust.
Содержание:
1. Подготовка: Настройка наших данных
2. Сравнение
- Получить поле
- Получить вложенное поле
- Получить необязательное поле
- Установить поле < br /> - Установить вложенное поле
- Установить каждый элемент в списке
- Кодировать / Сериализовать
- Декодировать / Десериализовать
3. Журнал изменений
Подготовка: Настройка наших данных
Сначала мы настроим наши структуры данных и несколько примеров, которые мы будем использовать в этом посте. Haskell и Rust требуют немного больше церемоний, потому что мы будем использовать пакеты / ящики. Для TypeScript мы используем ts-node
для запуска TypeScript в REPL.
TypeScript
Давайте сначала настроим наш эталонный объект в TypeScript. Сохраните следующее в house.ts
(или посмотрите typescript-json):
Haskell
Включенный фрагмент кода служит для того, чтобы дать вам представление о структурах данных, типах и именах, с которыми мы будем работать.
Вы можете найти настройку для каждого конкретного решения в:
- Haskell-lens: содержит приложение к объективу.
- Haskell-record-dot: содержит значение синтаксиса точки записи.
Просмотрите src/House.hs
для структур данных и src/Main.hs
для всех примеров в этом посте.
Чтобы разрешить перекрытие полей записи, мы используем DuplicateRecordFields вместе с OverloadedLabels (только в версии Lens) и кучей других расширений для получения вещей через дженерики.
Мы контролируем детали сериализации / десериализации JSON с помощью пакета derive-aeson + языкового расширения DerivingVia
.
Ржавчина
Полную настройку можно найти в ржавчине-серде. Просмотрите src/house.rs
для структур данных и src/main.rs
для всех примеров в этом посте.
Сравнение
Если вы хотите продолжить, вы можете запустить REPL для каждого подхода. Для версий TypeScript и Rust, где мы используем изменяемость, мы будем каждый раз клонировать объекты, чтобы они были согласованы во всех примерах и в нашем REPL.
💡 В TypeScript это чаще всего делается с помощью оператора распространения
...
или чего-то вроде_.cloneDeep(value)
.
TypeScript
$ cd typescript-json
$ npm i
$ npm run repl
> import data from './house'
> let newData
Haskell
$ cd haskell-lens
$ stack build
$ stack ghci
*Main Data>
К сожалению, плагины GHC плохо работают с ghci
. Вместо этого мы создадим проект, чтобы поэкспериментировать с примерами из src/Main.hs
.
$ cd haskell-record-dot
$ stack build
$ # Open src/Main.hs in your editor
$ stack run
Ржавчина
Поскольку в Rust нет REPL, вместо этого мы создадим проект, поэтому поэкспериментируем с примерами из src/main.rs
.
$ cd rust-serde
$ cargo build
$ # Open src/main.rs in your editor
$ cargo run
Получите поле
Первый прост: мы получим значение от нашего объекта.
Во-первых, наша версия TypeScript:
> data.house.owner
{ id: 1, firstname: 'Ariel', lastname: 'Swanson' }
Давайте посмотрим, как этого добиться в Haskell с линзами:
*Main Data> house ^. #owner
Person {id = 1, firstname = "Ariel", lastname = "Swanson"}
Здесь, вероятно, уже есть два незнакомых синтаксиса.
Первая, ^.
, исходит от Lens и является view
функцией, которую мы используем как средство доступа к объекту / записи. Второй, префикс #
для #owner
, происходит от расширения OverloadedLabels
и позволяет нам иметь несколько полей записи с одинаковым именем в области видимости.
Давайте посмотрим, как этого добиться в Haskell с синтаксисом точки записи:
house.owner
--> Person {id = 1, firstname = "Ariel", lastname = "Swanson"}
Наконец, давайте посмотрим на Rust:
house.owner
--> Person { id: 1, firstname: "Ariel", lastname: "Swanson" }
Получить вложенное поле
Мы постепенно увеличиваем сложность, обращаясь к вложенному полю.
TypeScript:
> data.house.owner.firstname
'Ariel'
Haskell с линзами:
*Main Data> house ^. #owner . #firstname
"Ariel"
Haskell с синтаксисом Record Dot:
house.owner.firstname
--> "Ariel"
Ржавчина:
house.owner.firstname
--> "Ariel"
Получите необязательное поле
Как мы обрабатываем необязательные поля?
TypeScript:
Необязательное связывание (?
) - важный шаг на пути к написанию более безопасного и чистого кода на JS / TS.
Haskell с линзами:
#_Just
от Lens дает нам удобный доступ к полям, заключенным в Maybe
s, с запасным значением.
Haskell с синтаксисом Record Dot:
В конечном итоге мы пишем более обычный код для погружения в значение Maybe
, используя maybe
для продолжения или возврата к значению по умолчанию.
Ржавчина:
Мы используем and_then
немного похоже на maybe
, передавая функцию для работы с нашим значением, если оно Some
, а затем создаем случай по умолчанию с unwrap_or
.
Установить поле
Начнем с обновления невложенного поля.
TypeScript:
Haskell с линзами:
Мы добавляем здесь два новых синтаксиса. &
- это обратный оператор приложения, но для всех намерений и целей он рассматривается как ^.
для установщиков. Наконец, .~
- это то, что позволяет нам установить наше значение.
Haskell с синтаксисом Record Dot:
Довольно аккуратно. Обратите внимание, что отсутствие интервала в house{
намеренно.
Ржавчина:
В качестве альтернативы мы могли бы использовать Синтаксис обновления структуры ..
в Rust, который работает так же, как синтаксис распространения (...
) в JavaScript. Это будет выглядеть примерно как Household { owner: new_ariel, ..house }
.
Установить вложенное поле
Теперь все становится немного сложнее.
TypeScript:
Haskell с линзами:
Обратите внимание, что мы смешиваем &
и .
, чтобы глубже проникнуть в объект / запись, что очень похоже на доступ к вложенному полю.
Haskell с синтаксисом Record Dot:
Обратите внимание, что отсутствие интервала в house{
действительно важно, по крайней мере, в текущем состоянии RecordDotSyntax.
Ржавчина:
Установить каждый элемент в списке
Давайте немного поработаем над списком людей в нашем доме. Мы сделаем эти имена более свежими.
TypeScript:
Haskell с линзами:
mapped
позволяет нам отображать функцию по всем значениям в #people
.
Haskell с синтаксисом Record Dot:
Использование map
кажется очень естественным и очень близко к обычному коду, который вы бы написали на Haskell.
Ржавчина:
Кодировать / сериализовать
Кодировать JSON из наших данных довольно просто. В TypeScript / JavaScript он встроен, а в Haskell и Rust мы просто обращаемся к Aeson и Serde. Каждая из библиотек дает нам возможность управлять деталями различными способами, например, опуская значения Nothing
/ None
.
TypeScript:
Haskell с линзами + Haskell с синтаксисом Record Dot:
Ржавчина:
Декодировать / десериализовать
К счастью, декодирование JSON в наш тип данных также несложно, хотя нам нужно будет сообщить Haskell и Rust немного больше информации, чем при кодировании (как и следовало ожидать).
TypeScript:
Haskell с линзами + Haskell с синтаксисом Record Dot:
Поскольку мы находимся в REPL, мы вручную включаем TypeApplications
языковое расширение. Затем мы используем это при декодировании в @Household
, чтобы сообщить Haskell, в какой тип данных мы пытаемся преобразовать эту случайную строку.
В качестве альтернативы мы могли бы написать (decode houseJson) :: Maybe Household
. Maybe
- это то, во что декодер оборачивает значение, если мы передали ему искаженную строку JSON.
Ржавчина:
Как и в случае с Haskell, мы сообщаем Rust, в какой тип данных мы пытаемся преобразовать нашу случайную строку. Мы делаем это, аннотируя тип deserialize
с помощью deserialize: Household
. unwrap
здесь для удобства, но в реальном коде вы, вероятно, с большей вероятностью сделаете вместо этого serde_json::from_str(&house_json)?
.
Есть ли другие общие закономерности, которые вы хотели бы увидеть? Считаете, что некоторые подходы можно улучшить? Оставьте комментарий, и я постараюсь расширить этот список, чтобы он был более полным!
Журнал изменений
Благодаря всем отзывам сообществ / r / rust и / r / haskell, были внесены следующие изменения:
13 апреля 2020 г.
- Добавлены примеры сериализации и десериализации
- Включен производный код Aeson во фрагмент Haskell, так как два примера типов данных Haskell оказались почти идентичными.
6 апреля 2020 г.
house & #people . mapped %~ (\p -> p & #firstname .~ "Fly " ++ p ^. #firstname)
стал намного более лаконичным сhouse & #people . mapped . #firstname %~ ("Fly " <>)
.- Добавлен допустимый интервал между
house{
и остальными подходами RecordDotSyntax (например,house{ owner.firstname = "New Ariel"}
). - Изменено с
map
наforEach
в TypeScript, поскольку возвращаемое значение было отброшено. - Изменены подходы Rust к использованию мутаций вместо унидиоматического неизменяемого стиля, в котором он был написан.
[1]: Наряду с aeson мы будем использовать новую библиотеку deriving-aeson для создания наших экземпляров.
[2]: Конечно, есть и другие варианты, например Оптика (пример использования), но я не буду их здесь рассматривать.
[3]: Мы используем generic-lens для производных от Lens вместо TemplateHaskell.
[4]: Пройдет немного времени, прежде чем он будет объединен и доступен в GHC, поэтому мы воспользуемся плагином record-dot-preprocessor, чтобы получить краткую информацию.
[5]: maybe
от Data. Может иметь сигнатуру типа maybe :: b -> (a -> b) -> Maybe a -> b
, принимая в качестве аргумента (1) значение по умолчанию (2) функцию, которая будет запускаться, если значение равно Just
и (3) значение Maybe
, с которым мы хотим работать.
Первоначально опубликовано на https://codetalk.io 5 апреля 2020 г.