是的,表面上一个非常简单的问题。但是,如果您花时间考虑到底,那么您将深入到无法估量的类型理论中。类型理论也注视着您。
首先,当然,您已经正确地发现F#没有类型类,这就是原因。但是你建议一个接口Mappable
。好吧,让我们调查一下。
假设我们可以声明这样的接口。您能想象它的签名会是什么样子吗?
type Mappable =
abstract member map : ('a -> 'b) -> 'f<'a> -> 'f<'b>
f
实现接口的类型在哪里。等一下!F#也没有!这f
是类型较高的类型变量,而F#根本没有类型较高的变量。没有办法声明一个函数f : 'm<'a> -> 'm<'b>
或类似的东西。
但是好吧,我们也克服了这一障碍。现在我们有一个接口Mappable
,可以通过实现List
,Array
,Seq
,以及厨房水槽。可是等等!现在我们有了方法而不是函数,而且方法的组合不好!让我们看一下将42加到嵌套列表的每个元素中:
// Good ol' functions:
add42 nestedList = nestedList |> List.map (List.map ((+) 42))
// Using an interface:
add42 nestedList = nestedList.map (fun l -> l.map ((+) 42))
看:现在我们必须使用lambda表达式!没有办法通过.map
实现作为值给另一个函数。实际上是“函数作为值”的结尾(是的,我知道,在这个示例中使用lambda看起来并不糟糕,但是请相信我,它变得非常丑陋)
但是,等等,我们还没有完成。现在是方法调用,类型推断不起作用!由于.NET方法的类型签名取决于对象的类型,因此编译器无法推断两者。实际上,这是新手与.NET库进行互操作时遇到的一个非常普遍的问题。唯一的解决方法是提供类型签名:
add42 (nestedList : #Mappable) = nestedList.map (fun l -> l.map ((+) 42))
哦,但这还不够!即使我已经为其提供了签名nestedList
,也没有为lambda的parameter提供签名l
。这种签名应该是什么?你会说应该fun (l: #Mappable) -> ...
吗?哦,现在我们终于可以对N个类型进行排名了,如您所见,它#Mappable
是“任何类型'a
诸如此类的'a :> Mappable
-即本身是通用的lambda表达式。
或者,我们可以返回更高种类的对象,并nestedList
更精确地声明类型:
add42 (nestedList : 'f<'a<'b>> where 'f :> Mappable, 'a :> Mappable) = ...
但是好吧,让我们暂时搁置类型推断,然后回到lambda表达式以及我们现在如何不能将其map
作为值传递给另一个函数。假设我们对语法进行了一些扩展,以允许Elm对记录字段执行以下操作:
add42 nestedList = nestedList.map (.map ((+) 42))
会是什么类型.map
?就像在Haskell中一样,它必须是受约束的类型!
.map : Mappable 'f => ('a -> 'b) -> 'f<'a> -> 'f<'b>
哇好 撇开.NET甚至不允许此类类型存在的事实,有效地,我们只需返回类型类即可!
但是有一个原因,就是F#首先没有类型类。上面已说明了该原因的许多方面,但更简单的表达方式是:简单性。
如您所见,这是一个毛线球。一旦有了类型类,就必须具有约束,更高种类,N级(或至少2级),并且在不知道它之前,您就要求强制性类型,类型函数,GADT和所有其余的。
但是Haskell确实为所有好东西付出了代价。事实证明,没有很好的方法来推断所有这些东西。种类较多的类型可以工作,但约束已经不起作用。等级N-甚至不要梦想。即使工作正常,您也会遇到必须要拥有博士学位才能理解的类型错误。这就是为什么在Haskell中会鼓励您在所有内容上加上类型签名的原因。好吧,不是所有的东西 -而是几乎所有的东西。以及您不放置类型签名的地方(例如,在内部let
和where
)-令人惊讶的是,那些地方实际上是单色的,因此您实质上回到了简单的F#领域。
另一方面,在F#中,类型签名很少见,主要用于文档或.NET互操作。除了这两种情况,您可以用F#编写整个大型复杂程序,而不必一次使用类型签名。类型推断可以很好地工作,因为没有什么太复杂或模棱两可的处理方式了。
这是F#与Haskell相比的最大优势。是的,Haskell可让您以非常精确的方式表达超级复杂的内容,这很好。但是F#就像Python或Ruby一样让您变得一厢情愿,如果遇到问题,仍然让编译器抓住您。