Распространенные шаблоны 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 г.