Vitali Haradkou

Senior SDET. Certified Node.js Application Developer (JSNAD). TG: @haradkou_sdet

Еще совсем недавно я выпустил библиотеку которая должна была искоренить ошибки связанные с null и undefined . Честно признаюсь, что решил подсмотреть у Rust Option и Result апи, поскольку увидел в этом потенциал и удобство использования!

Если кратко пройтись по истории создания сего чуда, то при изучения Rust увидел потенциал данных оберток. И после ресерча на наличие подобных решений такого вдохновения решил, что напишу такое чудо сам для javascript и буду использовать в своих проектах (об этом позже). Довольно быстро появилась версия 1, а за ней еще куча фиксов, затем появилась версия 2, где появился cmp пакет и dbg. И только недавно (10 апреля 2024) появилась на свет версия 3 для всех пакетов std, cmp, dbg.

Введение в rslike

Rslike - это библиотека которая позволяет избежать ошибок с использованием null, undefined, и ошибками за счет 2 основных классов - Option и Result.

Option<T> - предназначен для кода, который может быть null и(ли) undefined. Функции Some и None позволяют обернуть значение и позже, где это требуется сделать, можно вернуть значение с помощью unwrap и expect функций, либо проверить наличие значение переменной с помощью isSome, isNone функции.

Для удобства покажу, что будет вызвано не как Option а как Some, либо None

Some() // None<undefined>
Some(3) // Some<3>
Some<number>(3) // Some<number>
Some(undefined) //! None<undefined>
Some<number>(undefined) //! None<number>

None() // None<undefined>
None(null) // None<null>
None(3) // None<number>

Result<T,E> - предназначен для работы с кодом, который может "упасть". Чтобы не словить неожиданное падение - желательно предусмотреть все варианты выполнения функции, либо обернуть в Bind чтобы сделать функцию "безопасной" за счет того, что возвращается Result<Option<T>, E>. Где T и E - generics которые можете передать для функции.

Ok(3) // Result<3, unknown>
Ok<number>(3) // Result<number, unknown>
Ok(undefined) // Result<undefined, unknown>

Err(undefined) // Result<unkown, undefined>
Err<number>('hello world') // Result<number, string>
Err(new Error('hello world')) // Result<unkown, Error>

Также благодаря полезным функциям Bind и Async можно фукнции и асинхронный код делать безопасным, т.к. в результате будет использовано двойное оборачивание в Result<Option<T>, E>

import { Bind, Async } from '@rslike/std'
function external(arg1: any, arg2: any): any {
  // some implementation, can throws
}
external(1,2) // ok. e.g. returns 5
external(1,NaN) // throws Error

const binded = Bind(external)
binded(1,2) // Ok(Some(5))
binded(1,NaN) // Err(Error)

const promiseOk = Promise.resolve(5)
const safePromise1 = await Async(promiseOk) // Ok(Some(5))

safePromise1.isErr() // false
safePromise1.isOk() // true

const promiseErr = Promise.reject('I fails unexpected')
const safePromise2 = await Async(promiseErr) // Err('I fails unexpected')

safePromise2.isErr() // true
safePromise2.isOk() // false

А теперь к тому что было изменено.

Std. Well-known Symbols

для удобство использования пришлось реализовать многие Well-known Symbols.

Сюда входят такие символы как:

  • Symbol.iterator

  • Symbol.asyncIterator

  • Symbol.search

  • Symbol.split

  • Symbol.toPrimitive

  • Symbol.toStringTag

  • Symbol.inspect(да, знаю что он не well-known и применяется исключительно для inspect функции в node.js. Почему бы и нет)

Cамый простой пример использования - использование цикла for...of

До 3 версии нужно было делать unwrap

import { Some } from '@rslike/std'

const a = Some([1,2,3])

for(let el of a.unwrap()){
  // el: 1,2,3
}

С версии 3 теперь стал доступен синтаксический сахар без использования unwrap. Мелочь, а приятно ?.

import { Some } from '@rslike/std'
const a = Some([1,2,3])

for(let el of a){
  // el: 1,2,3
}

Бонус - TS Type inferring. если внутри Option или Result не итерируемый тип - будет never. А также в runtime будет выброшено UndefinedBehaviorError поскольку у числа нет реализации Symbol.iterator (вызывается именно данный метод для обернутого значения).

import { Some } from '@rslike/std'

const a = Some(3)
for(let el of a) {
  // el: never
}

STD. instanceof для Some, None, Err, Ok

Для того, чтобы не импортировать лишний класс лишь для одной проверки instanceof был реализован Symbol.hasInstance для функций Some, None, OK и Err.

Взглянем на пример до версии 3

// v2
import { Ok, Result } from '@rslike/std'
const a = Ok(3)
a instanceof Result // true
a instanceof Ok // false

А теперь на пример после (не делается испорт Result)

// v2
import { Ok, Err } from '@rslike/std'
const a = Ok(3)
a instanceof Ok // true
a instanceof Err // false

Да, это синтаксический сахар, для того чтобы не делать лишнего импорта.

STD. TS types

Отдельная личная гордость - typescript и его вычисляемые типы. Теперь в Some, None , Ok, Err приходят не просто generic а константный generic. Это позволило сделать небольшие хитрости. А в случае когда мы не можем определить тип(или он более общий) то будет вызвана прежняя реализация

import { Some } from '@rslike/std'

let a: number = 5
const a = Some(a) // Some<number>

a.isSome() // boolean

let b: number = 5
const c = Some(b) // Some<number>

a.isSome() // boolean

Правда не обошлось без проблем. Никак не могу побороть проблему с мутацией занчения внутри класса. Например метод replace - должен мутировать значение(да, он действительно мутирует). Но как сделать так, чтобы еще и Typescript поволял мутировать типы для класса - загадка ?. (если знаете - напишите коментарий или в личку, контакты в конце статьи).

STD. match и double unwrap

В пакете std помимо Option и Result также находится некоторые полезности, такие как Bind, Async и match. Если Bind и Async не поменялись, то функция match наоборот, приобрела полезную функцию, - вызывать unwrap для Result<Option> дважды. Это позволило в проекте сократить код с использованием match раза в два.

Давайте сравним как было до (67 строк)

import { Bind, match, Err, Ok } from '@rslike/std'

function divide(a: number, b: number) {
  if (b === 0) Err("Divide to zero");
  if (a === 0) Ok(0);
  if (Number.isNaN(a) || Number.isNaN(b)) return Err(undefined);
  return a / b;
}

const binded = Bind(divide);
const fn1 = binded(1, 1); // Result<Option<number | undefined>, string>
const fn2 = binded(NaN, 1); // Result<Option<undefined>, string>

const res1 = match(
  fn1, // or fn2
  (res) => {
    return match(
      res,
      (value) => {
        console.log("value is:", value);
      },
      () => {
        console.log("value is None");
      }
    );
    },
    (err) => {
      console.error(err);
    }
);

console.log(res1); // value is: 1
console.log(res2); // value is None

Можно заметить что тут вызывается дважды функция match. Короче было бы просто сделать проверку на isOk и isSome и код был бы короче.

Начиная с версии 3 (27 строк)

import { Bind, match, Err, Ok } from '@rslike/std'

function divide(a: number, b: number) {
  if (b === 0) Err("Divide to zero");
  if (a === 0) Ok(0);
  if (Number.isNaN(a) || Number.isNaN(b)) return Err(undefined);
  return a / b;
}

const binded = Bind(divide);
const fn1 = binded(1, 1); // Result<Option<number | undefined>, string>
const fn2 = binded(NaN, 1); // Result<Option<undefined>, string>

const res1 = match(
  fn1, // or fn2
  (value) => {
    console.log("value is:", value);
  },
  (err) => {
    if (err) console.error(err);
    else console.log("value is None");
  }
);

res1 // value is: 1
// or res2 - value is None

Cmp

Для пакета который предназначен для сравнения(cmp или comparison package) было выброшено из интерфейсов для Eq, PartialEq, Ord методы equals, partialEquals, compare. Вместо этого интерфейсы требуют реализации Symbol.equals, Symbol.partialEquals и Symbol.compare соотвественно.

import { type Eq, equals } from '@rslike/cmp'

class Author implements Eq {
  constructor(readonly name: string){}
  
  [Symbol.equals](another: unknown){
    return another instanceof Author && this.name === another.name
  }
}

const pushkin = new Author('Пушкин')
const tolkien = new Author('Толкиен')

pushin[Symbol.equals](tolkien) // false
pushin[Symbol.equals](new Author('Пушкин')) // true

// либо можно вызвать утилитарную функцию
equals(pushkin, tolkien) // false
equals(pushkin, new Author('Пушкин')) // true

Как бонус, данные well-known symbols определены для следующих глобальных объектов:

  • Number

  • String

  • Boolean

  • Date

Использование в проекте

3 версия появилась благодаря тому, что свою же библиотеку начал использовать на текущем месте работы. Если кратко - то нужно было реализовать CLI на node.js для того, чтобы ходить на сервер, забирать данные и ложить в фаил. Данный код лежит вместе с проектом и сам является атомарным и лишен импортирования чего-либо из проекта (кроме npm библиотек). К тому же мне дали для этого задания полную творческую свободу в реализации, подходов, библиотек и стиля кода. Также желательно сделать так, чтобы весь код умещался в 1 фаил, и не раздувать CLI на множество моделей, аргументов и т.д. Сказано - сделано.

Сам фаил умещается в 500 строк отформатированного кода. Хелперы(типа истанса axios, путей, и описания кодов ошибок) занимают ~250 строк отформатированного кода. Итого логики ~250 строк. Кол-во команд - 5:

  • login

  • logout

  • ls - вывести все по окружению на сервере

  • get - получить инфо из сервера по параметрам и записать в фаил

  • ctl - тоже самое что и get только вместо записи в фаил - пробросить в команду, которая передана как аргумент. Например program ctl 'npm run tests' . В tests будет как env будет передана информация полученая из сервера.

Я попробовал переписать данный фаил без использования rslike и получил ~20% больше кода, поскольку данный код это в основном проверки на null, undefined , и try catch finally . например передали ли мы аргумент или нет. Поэтому считаю свою библиотеку довольно удачной.

В заключение

В заключение хочется добавить что сам релиз получился довольно большим на кол-во изменений.

git info
git info

В связи с этим не мог не увеличиться выходной бандл. Если посмотреть на bundlephobia то можно заметить "прожорливость". В отличие от версии 2, основу конечно же составляет JSDoc которой прибавилось из-за примеров, и того, какие исключения будут выброшены, а также более сложная TS типизация для того, чтобы конечным пользователям было проще и удобнее!

8.3kB v2 & 12.6kB v3
8.3kB v2 & 12.6kB v3

Контакты

Как и обещал - мои контакты для связи.

Telegram - @vitalicset

А также мой канал куда делаю посты о свяком, автоматизации, IT новостях и о том что захочу. Это не призыв! Но было бы приятно - @haradkou_sdet

Комментарии (0)