У меня есть код, который играет с некоторыми функциональными концепциями javascript, такими как Eithers, представленный Брайаном Лонсдорфом в эта серия.
Я пытался ввести выражение Either. Вот моя попытка:
type RightApi<X> = {
map: <C: *>(f: X => C) => RightApi<C>,
fold: <C: *>(f: Function, g: X => C) => C
};
const Right = <X: *>(x: X): RightApi<X> => ({
map: f => Right(f(x)),
fold: (f, g) => g(x),
})
type LeftApi<X> = {
map: (f: Function) => LeftApi<X>,
fold: <C: *>(f: X => C, g: Function) => C,
};
const Left = <X: *>(x: X): LeftApi<X> => ({
map: f => Left(x),
fold: (f, g) => f(x),
});
const fromMayBeNullOrUndefined = <X: *>(x: X) =>
((x === null || x === undefined) ? Left(x) : Right(x));
const test = fromMayBeNullOrUndefined(3).fold(x => null, x => x);
// null | number
К сожалению, это не совсем так, как хотелось бы. В последней строке я пытаюсь проверить набор текста. Логически мне кажется очевидным, что test
будет number
. Поскольку значение, предоставленное для fromMayBeNullOrUndefined
, равно 3
, то есть не равно null или undefined, поэтому при вызове fold
должна быть вызвана правая функция.
Я здесь ошибся? Или поток не может более точно определить результат?
Хороший вопрос. Насколько я понимаю, это так. Такой синтаксис <X: *>(x: X) => x
будет чувствителен к аргументам, с которыми вызывается функция. Но, глядя на этот простой пример, я думаю, что то, что он делает, скорее рассматривает все возможности с учетом предоставленных аргументов.
Or is it not possible for flow to infer the result more precisely?
Вывод типа останавливается на границах функции. Было бы слишком хрупко, если бы тело одной функции могло повлиять на проверку типов в другой. Единственный раз, когда рассматривается фактическое тело функции, - это проверка типа этой функции на соответствие ее собственному типу.
Итак, хотя, да, вы можете заглянуть внутрь fromMayBeNullOrUndefined
и знать, что результат Left
или Right
зависит от того, является ли x
null
/ undefined
или нет, неразумно ожидать, что Flow сделает то же самое. Это также нарушило бы абстракцию функции, если бы вам пришлось заглянуть в исходный код, чтобы узнать, как выполнить проверку типа.
Похоже, то, что вы ищете, - это одна из двух вещей, которые теперь может предоставить Flow.
%checks
a.k.a. Предикатные функции
$NonMaybeType<T>
, то есть все поставляемые типы, кроме null
и undefined
.
Кроме того, типы Function
в Flow - это рассматривается как "небезопасный", поэтому более конкретное определение типа поможет.
В случае map()
и fold()
вы можете захотеть изучить типа $Call<T>
, который позволяет вам вызывать функцию для типа статически во время автор.
Надеюсь это поможет.
Упомянутая вами проблема может быть решена путем определения различных сигнатур типа для функции fromMaybeOrUndefined
следующим образом:
declare function fromMayBeNullOrUndefined(x: null | void): LeftApi<null | void>;
declare function fromMayBeNullOrUndefined<X>(x: X): RightApi<X>;
function fromMayBeNullOrUndefined<X>(x: X) {
if (x === null || x === undefined) {
return Left(x);
}
return Right(x);
}
Теперь проверьте правильность проверки типов:
const test: null = fromMayBeNullOrUndefined(null).fold(x => null, x => x);
const test: number = fromMayBeNullOrUndefined(3).fold(x => null, x => x);
Но, хотя это забавное упражнение, в нем полностью отсутствует смысл типа Either
:
Суть Either
в том, что Left
и Right
имеют один и тот же API, так что во время выполнения базовое значение (или часто называемые данными) может быть или из них. Независимо от того, является ли значение Left
или Right
, прозрачно. Это важно, потому что вы должны использовать тип Either
(или на самом деле любую монаду), когда есть какое-то внешнее влияние, которое может иметь два результата. Например, вы хотите подключиться к базе данных - соединение может быть создано или или оно не работает. Или в вашем случае пользователь какого-либо API может предоставить или число или нулевое значение.
/* @flow */
type Either<L, R> = {
map: <T>(f: R => T) => Either<L, T>,
fold: <T>(f: L => T, g: R => T) => T
};
const Right = <R>(x: R): Either<any, R> => ({
map: <T>(f: R => T): Either<any, T> => Right(f(x)),
fold: <T>(f: any => T, g: R => T): T => g(x),
})
const Left = <L>(x: L): Either<L, any> => ({
map: <T>(f: any => T): Either<any, T> => Left(x),
fold: <T>(f: L => T, g: any => T): T => f(x),
});
const fromMayBeNullOrUndefined = <X>(x: X): Either<null | void, X> =>
((x === null || x === undefined) ? Left(x) : Right(x));
const test = fromMayBeNullOrUndefined(3).fold(x => null, x => x);
(test: null | number)
С Either
вы можете продолжать кодирование в «счастливом пути» (справа) до тех пор, пока не захотите снова сложить пути выполнения. Пример:
function getAccountBalance(name: string): string {
const account: Either<Error, Account> = getAccountByCustomerName(name);
return account
.map(account => account.balance)
.fold(
() => "Seems like you don't have an account at our service",
balance => `Your balance is ${balance}`,
);
}
Этот пример станет более интересным, когда будет выполняться несколько операций, как описано в следующей видеолекции.
Действительно ли Flow пытается выполнить динамический анализ в подобном случае?