我正在玩TypeScript类型系统,我似乎碰到了一堵看不见的墙。
对于初学者,我有Func
的助手类型(因为Function
不是 TypeScript 中的泛型类型):
type Func <A, B> = (_: A) => B;
然后我有一个基本的抽象类。似乎它不能是一个接口,因为这样 TypeScript 就不允许我专门化方法的签名,请参阅下面的派生类,这是可疑的,但它适用于抽象类和abstract override
:
abstract class Wrappable <A> {
abstract andThen <B>(func: Func<A, B>): Wrappable<B>;
abstract andThenWrap <B>(func: Func<A, Wrappable<B>>): Wrappable<B>;
}
通过上述定义,我可以实现类似Maybe
的东西(另请注意用于专门化基类的andThen
和andThenWrap
方法的abstract override
):
abstract class Maybe <A> extends Wrappable <A> {
abstract override andThen <B>(func: Func<A, B>): Maybe<B>;
abstract override andThenWrap <B>(func: Func<A, Maybe<B>>): Maybe<B>;
static option <A>(value: A | null | undefined): Maybe<A> {
return (!value) ? Maybe.none<A>() : Maybe.some<A>(value);
}
static some <A>(value: A): Some<A> {
return new Some<A>(value);
}
static none <A>(): None<A> {
return new None<A>();
}
}
class Some <A> extends Maybe <A> {
private value: A;
constructor(value: A) {
super();
this.value = value;
}
override andThen <B>(func: Func<A, B>): Maybe<B> {
return new Some(func(this.value));
}
override andThenWrap <B>(func: Func<A, Maybe<B>>): Maybe<B> {
return func(this.value);
}
}
class None <A> extends Maybe <A> {
constructor() {
super();
}
override andThen <B>(_: Func<A, B>): Maybe<B> {
return new None<B>();
}
override andThenWrap <B>(_: Func<A, Maybe<B>>): Maybe<B> {
return new None<B>();
}
}
当我尝试实现更棘手的东西时,问题就开始了,比如这个ExceptionW
,它应该包装另一个包装类:
class ExceptionW <R, W extends Wrappable<R>> extends Wrappable <W> {
private value: W;
constructor(value: W) {
super();
this.value = value;
}
override andThen <T, U extends Wrappable<T>>(func: Func<W, U>): ExceptionW<T, U> {
return new ExceptionW<T, U>(func(this.value));
}
override andThenWrap <T, U extends Wrappable<T>>(func: Func<W, ExceptionW<T, U>>): ExceptionW<T, U> {
return func(this.value);
}
}
这给了我一个超级模糊的错误:
Property 'andThen' in type 'ExceptionW<R, W>' is not assignable to the same property in base type 'Wrappable<W>'.
Types of parameters 'func' and 'func' are incompatible.
Type 'B' is not assignable to type 'Wrappable<unknown>'.
和
Property 'andThenWrap' in type 'ExceptionW<R, W>' is not assignable to the same property in base type 'Wrappable<W>'.
Types of parameters 'func' and 'func' are incompatible.
Property 'value' is missing in type 'Wrappable<B>' but required in type 'ExceptionW<unknown, Wrappable<unknown>>'.
据我所知,它抱怨类型参数W
我试图约束它Wrappable<?>
。
第二个错误误导我认为它与value
属性有关。所以我试图用另一个抽象层来缓解它:
abstract class WrapperTransformer <A, W extends Wrappable<A>> extends Wrappable <W> {
abstract override andThen <B, U extends Wrappable<B>>(func: Func<W, U>): WrapperTransformer<B, U>;
abstract override andThenWrap <B, U extends Wrappable<B>>(func: Func<W, WrapperTransformer<B, U>>): WrapperTransformer<B, U>;
}
我再次看到类似的错误:
Property 'andThen' in type 'WrapperTransformer<A, W>' is not assignable to the same property in base type 'Wrappable<W>'.
Types of parameters 'func' and 'func' are incompatible.
Type 'B' is not assignable to type 'Wrappable<unknown>'.
和
Property 'andThenWrap' in type 'WrapperTransformer<A, W>' is not assignable to the same property in base type 'Wrappable<W>'.
Types of parameters 'func' and 'func' are incompatible.
Call signature return types 'Wrappable<B>' and 'WrapperTransformer<unknown, Wrappable<unknown>>' are incompatible.
The types of 'andThen' are incompatible between these types.
Type '<B>(func: Func<B, B>) => Wrappable<B>' is not assignable to type '<B, U extends Wrappable<B>>(func: Func<Wrappable<unknown>, U>) => WrapperTransformer<B, U>'.
Types of parameters 'func' and 'func' are incompatible.
Types of parameters '_' and '_' are incompatible.
Type 'B' is not assignable to type 'Wrappable<unknown>'.
在这个阶段,我对为什么会发生这种情况有点迷茫,我倾向于认为这可能是类型系统的问题,TypeScript 是 JavaScript 的转译器的怪癖,或者只是我对 TypeScript 类型系统的一些误解。
有人可以指出我正确的方向吗?
这就是我喜欢函数式编程的原因:你花几个小时(或者,就像我的情况一样,几天)思考问题,然后用几行整齐的代码来解决它。
在此配置中,TypeScript 无法扣除模板类型的约束。
但上述代码片段更大的问题实际上不仅仅是 TypeScript 本身,而是代码背后的思想。
仅供上下文参考:我试图实现几个monad(即:Maybe
和Either
)和一个monad转换器(在问题中 -ExceptionT
)。
使用上面的代码,仅编译的实现可能如下所示:
class ExceptionW <A> implements Wrappable <A> {
constructor(private readonly value: Wrappable<A>) {}
andThen<B>(func: Func<A, B>): ExceptionW<B> {
return new ExceptionW<B>(this.value.andThen(func));
}
andThenWrap<B>(func: Func<A, ExceptionW<B>>): ExceptionW<B> {
return new ExceptionW<B>(this.value.andThenWrap(func));
}
}
因此,我没有约束ExceptionW
的模板参数,而是将整个类包装在它周围。粗略地说,利用组合而不是继承。
完成,案件驳回。
然而,深入兔子洞,ExceptionT
背后的想法是,我们可以将其用作monad本身(ExceptionT
),并使用它map
和flatMap
(在上面的代码中 - 相应地andThen
和andThenWrap
)对它包装的值进行操作。
像这样的事情就可以了:
class ExceptionW <A> implements Wrappable <A> {
constructor(private readonly value: Func0<Wrappable<A>>) {}
andThen<B>(func: Func<A, B>): ExceptionW<B> {
return new ExceptionW<B>(() => this.value().andThen(func));
}
andThenWrap<B>(func: Func<A, ExceptionW<B>>): ExceptionW<B> {
return new ExceptionW<B>(() => this.value().andThenWrap(func));
}
unsafeRun(): Wrappable<A> {
return this.value();
}
}
此实现本身就是一个 monad,它将类型包装A
。
问题中的实现,即ExceptionW
包装Wrappable<A>
的位置是行不通的。这样做的原因是 monad 接口(在这种情况下Wrappable
)对andThen
和andThenWrap
强制执行非常具体的规则 - 它们必须在 monad 包装的类型上运行。
因此,如果类签名看起来像
class ExceptionW <A, W extends Wrappable<A>> implements Wrappable <W> {}
这与
class ExceptionW <A> implements Wrappable <Wrappable<A>> {}
那么从Wrappable
接口继承的方法应如下所示:
andThen(func: Func<Wrappable<A>, Wrappable<B>>): ExceptionW<Wrappable<B>> {}
andThenWrap(func: Func<Wrappable<A>, ExceptionW<Wrappable<B>>>): ExceptionW<Wrappable<B>> {}
而这个单子的价值将是...可疑。
考虑如何使用monad会导致更合理的实现。
比如说,有一个函数将XML作为字符串,解析它并返回解析的XMLDocument
对象:
const getResponseXML = (response: string): XMLDocument =>
new DOMParser().parseFromString(response, "text/xml");
但是,如果传递的字符串不是有效的XML,则此逻辑可能会失败,因此我们通常会将其包装为try..catch
:
const getResponseXML = (response: string): XMLDocument => {
try {
const doc = new DOMParser().parseFromString(response, "text/xml");
// see https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString#error_handling
if (doc.querySelector('parsererror'))
throw new Error('Parser error');
return doc;
} catch (e) {
// ???
}
};
在功能方面,可以使用Maybe<XMLDocument>
monad 进行最简单的错误处理,也可以使用Either<Error, XMLDocument>
对错误情况进行更精细的控制(和反馈)。
然后整个程序将围绕这两个monads之一构建:
const getResponseXML = (response: string): Either<Error, XMLDocument> => {
const doc = new DOMParser().parseFromString(response, "text/xml");
if (doc.querySelector('parsererror'))
return Either<Error, XMLDocument>.left(new Error('Parser error'));
return Either<Error, XMLDocument>.right(doc);
};
const program = getResponseXML('')
.andThen(doc => processDocument(doc));
但是getResponseXML
函数仍然是一个表达式,而不是一个值,这意味着当你运行函数时,它实际上会做一些工作,而不是描述做这项工作的意图(并且程序不会是完成工作后对工作所做的所有计算的链)。
这在函数式编程世界中是呃-呃-不好的。
听起来像是名为ExceptionW
的东西的完美用例。
class ExceptionW <A> implements Wrappable <A> {
constructor(private readonly task: Func0<Wrappable<A>>, private readonly exceptionHandler: Func<unknown, Wrappable<A>>) {}
andThen<B>(func: Func<A, B>): ExceptionW<B> {
return new ExceptionW<B>(
() => this.task().andThen(func),
(e) => this.exceptionHandler(e).andThen(func)
);
}
andThenWrap<B>(func: Func<A, ExceptionW<B>>): ExceptionW<B> {
return new ExceptionW<B>(
() => this.task().andThenWrap(func),
(e) => this.exceptionHandler(e).andThenWrap(func)
);
}
runExceptionW(): Wrappable<A> {
try {
return this.task();
} catch (e) {
return this.exceptionHandler(e);
}
}
}
这样,代码就变得更加函数式编程友好:
const getResponseXML = (response: string): ExceptionW<XMLDocument> =>
new ExceptionW(
() => {
const doc = new DOMParser().parseFromString(response, "text/xml");
if (doc.querySelector('parsererror'))
throw new Error('Parser error');
Either<Error, XMLDocument>.right(doc)
},
(e) => Either<Error, XMLDocument>.left(e)
);
因此,当您运行getResponseXML
函数时,什么都不会发生 - 结果您将简单地获取ExceptionW<XMLDocument>
对象。不会创建解析器,也不会返回任何错误。然后,您可以按照"当我们实际运行此代码并获得一些结果时会发生什么"的方式编写程序:
const program = (getResponseXML('invalid XML')
.andThen(doc => processXMLDocument(doc))
.runExceptionW() as Either<Error, XMLDocument>>)
.bimap(
(result) => console.log('success', result),
(error) => console.error('error', error)
);
只是为了让它更漂亮一点:
class ExceptionW {
runExceptionW<W extends Wrappable<A>>(): W {
try {
return this.task() as W;
} catch (e) {
return this.exceptionHandler(e) as W;
}
}
}
const program = getResponseXML('invalid XML')
.andThen(doc => processXMLDocument(doc))
.runExceptionW<Either<Error, XMLDocument>>>()
.bimap(
(result) => console.log('success', result),
(error) => console.error('error', error)
);