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
. например передали ли мы аргумент или нет. Поэтому считаю свою библиотеку довольно удачной.
В заключение
В заключение хочется добавить что сам релиз получился довольно большим на кол-во изменений.
В связи с этим не мог не увеличиться выходной бандл. Если посмотреть на bundlephobia то можно заметить "прожорливость". В отличие от версии 2, основу конечно же составляет JSDoc которой прибавилось из-за примеров, и того, какие исключения будут выброшены, а также более сложная TS типизация для того, чтобы конечным пользователям было проще и удобнее!
Контакты
Как и обещал - мои контакты для связи.
Telegram - @vitalicset
А также мой канал куда делаю посты о свяком, автоматизации, IT новостях и о том что захочу. Это не призыв! Но было бы приятно - @haradkou_sdet