Monads(二)

上一篇文章中,我以OOP程序员而非函数式程序员的视角去审视了Monads,"Monad Pattern"是一种针对类型的设计模式,而"monad"则是应用这种模式的一个类型,让我们先来讲一些你肯定已经非常熟悉的monad类型,而非直接研究"Monad Pattern"本身

我在第一时间想到了以下类型,可能不是很完善,如果你觉得C#中还有其他常用的monad类型,请在评论区指出

  • Nullable<T> —— 提供一个可能为null的T类型的值
  • Func<T> —— 提供一个可以"随机应变"的计算出来的T类型的值1
  • Lazy<T> —— 提供一个可以被指定的代码计算出来的T类型的值并将其缓存
  • Task<T> —— 提供一个可以被异步计算并在未来获取的T类型的值
  • IEnumerable<T> 提供一个有序,只读的T类型列表,列表长度可以是0或者任意值

那么,这些类型有哪些共同之处呢?最明显的一处想必是他们都有且仅有一个泛型参数,并且处于一个非常尴尬的地位——除了Nullable<T>以外,其余所有无论T是什么类型都可以正常运作,这些类型完全不知道其泛型参数具体的语义和实现,即便是Nullable<T>,也仅仅"知道"T必须是一个不可空的值类型2

注: 这个现象(Nullable<T>类型的限制)其实是历史原因所致,如果C#刚出生就伴随着泛型的话,那么Nullable<T>就可以用任何类型去实现(不仅仅是不可空的值类型,而且C# 8的确做到了这一点)

另外一点就是这些类型都是"放大器(amplifiers)"(我非常感谢我的前同事在这篇文章(dc注: 已经没了)中对于monads的解释,它在很大程度上帮助我理解了这个概念)——某种可以增强其泛型参数的"表现力(原文representational power,此处翻译可能不准确)"的类型

好比一个byte可以表示为其256个值中的任意一个,简单而又有用。我们也可以用泛型来很轻松的表述出诸如"一个可以被异步计算的可空字节序列(an asynchronously-computed sequence of nullable bytes)": Task<IEnumerable<Nullable<byte>>>,这样就在不改变byte本身性质的前提下大大了扩展他的能力

所以,难道monad指的就是能够给自己的泛型参数"扩展能力"的这些泛型类吗?也不尽然,为了实现"Monad Pattern",我们还需要了解更多内容,在下一节中,我们会操作这五种类型以试图找出他们其他的共性


  1. dc注: 原文"represents a T that can be computed on demand",根据C#语言的实际特性,这里的意思应该是指Func<T>T的具体值在你调用这个委托的那一刻才会被确定,比如Func<int> rnd = () => new Random().NextInt(),在这个例子中rnd就是"随机应变"的,只有在你调用它的那一刻rnd里面包装的值才会被确定 ↩︎
  2. dc注: 此处指Nullable<T>的泛型约束(generic constraint),Nullable<T>在C#中的定义如下:public struct Nullable<T> where T : struct。其中将泛型T约束为struct意味其必须是不可空的值类型(non-nullable value types),注意此处的"non-nullable"修饰"value types",也就是T必须是值类型,而且不能是另一个Nullable<T>(值类型本身不可空) ↩︎

乱我心者,今日之日多烦忧