考虑Functor
Haskell中的类型类,其中f
是类型较高的类型变量:
class Functor f where
fmap :: (a -> b) -> f a -> f b
这种类型签名的意思是fmap将a的类型参数f
从更改a
为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);
}
这里的问题是,这并不限制FB
具有相同F
的FA
-所以,当你声明一个类型List<A> implements Functor<List<A>, A>
,该map
方法能仍返回NotAList<B> implements Functor<NotAList<B>, B>
。
在Java中使用原始类型(未参数化的容器)的最终尝试:
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
在这里F
将实例化为未参数化的类型,例如just List
或Map
。这样可以保证a FunctorStrategy<List>
只能返回a,List
但是您已经放弃使用类型变量来跟踪列表的元素类型。
问题的核心在于,Java和C#等语言不允许类型参数具有参数。在Java中,如果T
是类型变量,则可以编写T
和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
产生不同结果类型的操作,就很难像这样保证。