考虑Functor
Haskell 中的类型类,其中f
是更高种类的类型变量:
class Functor f where
fmap :: (a -> b) -> f a -> f b
这个类型签名的意思是 fmap 改变了f
from a
to的类型参数b
,但保持f
原样。因此,如果您fmap
在列表上使用,您将得到一个列表,如果您在解析器上使用它,您将得到一个解析器,依此类推。这些是静态的,编译时保证。
我不知道 F#,但是让我们考虑一下如果我们尝试用Functor
Java 或 C# 之类的语言表达抽象,使用继承和泛型,但没有更高种类的泛型,会发生什么。第一次尝试:
interface Functor<A> {
Functor<B> map(Function<A, B> f);
}
第一次尝试的问题是允许接口的实现返回任何实现的类Functor
。有人可以编写一个FunnyList<A> implements Functor<A>
其map
方法返回不同类型的集合,或者甚至是其他一些根本不是集合但仍然是Functor
. 此外,当您使用该map
方法时,您不能对结果调用任何特定于子类型的方法,除非您将其向下转换为您实际期望的类型。所以我们有两个问题:
- 类型系统不允许我们表达
map
方法总是返回与Functor
接收者相同的子类的不变量。
- 因此,没有静态类型安全的方式来
Functor
调用map
.
您可以尝试其他更复杂的方法,但它们都不是真正有效的。例如,您可以通过定义Functor
限制结果类型的子类型来尝试增加第一次尝试:
interface Collection<A> extends Functor<A> {
Collection<B> map(Function<A, B> f);
}
interface List<A> extends Collection<A> {
List<B> map(Function<A, B> f);
}
interface Set<A> extends Collection<A> {
Set<B> map(Function<A, B> f);
}
interface Parser<A> extends Functor<A> {
Parser<B> map(Function<A, B> f);
}
// …
这有助于禁止那些较窄接口的实现者从方法中返回错误的类型Functor
,map
但由于Functor
您可以拥有的实现数量没有限制,因此您需要的较窄接口的数量也没有限制。
(编辑:请注意,这仅适用Functor<B>
于作为结果类型出现,因此子接口可以缩小它。所以AFAIK我们不能缩小Monad<B>
以下接口中的两种用途:
interface Monad<A> {
<B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}
在 Haskell 中,具有更高级别的类型变量,这是(>>=) :: Monad m => m a -> (a -> m b) -> m b
.)
另一种尝试是使用递归泛型来尝试让接口将子类型的结果类型限制为子类型本身。玩具示例:
/**
* A semigroup is a type with a binary associative operation. Law:
*
* > x.append(y).append(z) = x.append(y.append(z))
*/
interface Semigroup<T extends Semigroup<T>> {
T append(T arg);
}
class Foo implements Semigroup<Foo> {
// Since this implements Semigroup<Foo>, now this method must accept
// a Foo argument and return a Foo result.
Foo append(Foo arg);
}
class Bar implements Semigroup<Bar> {
// Any of these is a compilation error:
Semigroup<Bar> append(Semigroup<Bar> arg);
Semigroup<Foo> append(Bar arg);
Semigroup append(Bar arg);
Foo append(Bar arg);
}
但是这种技术(对于普通的 OOP 开发人员来说相当神秘,对于普通的功能开发人员来说也是如此)仍然无法表达所需的Functor
约束:
interface Functor<FA extends Functor<FA, A>, A> {
<FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}
这里的问题是 this 不限FB
于与 - 相同F
,FA
因此当您声明 type 时List<A> implements Functor<List<A>, A>
,该map
方法仍然可以返回 a NotAList<B> implements Functor<NotAList<B>, B>
。
最后尝试,在 Java 中,使用原始类型(未参数化的容器):
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
这里F
将被实例化为未参数化的类型,例如 justList
或Map
。这保证 aFunctorStrategy<List>
只能返回 a List
——但是您已经放弃使用类型变量来跟踪列表的元素类型。
这里问题的核心是 Java 和 C# 等语言不允许类型参数有参数。在Java中,ifT
是一个类型变量,你可以写T
and List<T>
,但不能写T<String>
。更高种类的类型消除了这个限制,所以你可以有这样的东西(没有完全考虑过):
interface Functor<F, A> {
<B> F<B> map(Function<A, B> f);
}
class List<A> implements Functor<List, A> {
// Since F := List, F<B> := List<B>
<B> List<B> map(Function<A, B> f) {
// ...
}
}
并特别解决这一点:
(我认为)我明白了,而不是myList |> List.map f
或myList |> Seq.map f |> Seq.toList
更高种类的类型允许您简单地编写myList |> map f
它,它会返回一个List
. 这很棒(假设它是正确的),但似乎有点小?(难道不能简单地通过允许函数重载来完成吗?)我通常转换为Seq
无论如何,然后我可以转换为我想要的任何东西。
有许多语言以map
这种方式概括函数的概念,通过对其进行建模,就好像在本质上,映射是关于序列的。您的这句话就是本着这种精神:如果您有一个支持Seq
与Seq.map
.
然而,在 Haskell 中,类比这Functor
更普遍。它与序列的概念无关。您可以fmap
为没有良好映射到序列的类型实现,例如IO
操作、解析器组合器、函数等:
instance Functor IO where
fmap f action =
do x <- action
return (f x)
-- This declaration is just to make things easier to read for non-Haskellers
newtype Function a b = Function (a -> b)
instance Functor (Function a) where
fmap f (Function g) = Function (f . g) -- `.` is function composition
“映射”的概念实际上与序列无关。最好理解函子定律:
(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs
非常不正式:
- 第一定律说,使用身份/noop 函数进行映射与什么都不做是一样的。
- 第二定律说,任何你可以通过两次映射产生的结果,你也可以通过一次映射产生。
这就是你想要fmap
保留类型的原因——因为一旦你得到map
产生不同结果类型的操作,做出这样的保证就变得非常非常困难。