我正在学习 F#。我用 Haskell 开始了 FP,对此我很好奇。
由于 F# 是 .NET 语言,我认为Mappable像 haskellFunctor类型类一样声明接口似乎更合理。
但是就像上图一样,F# 函数是分开的,并且是自己实现的。这种设计的设计目的是什么?对我来说,Mappable.map为每种数据类型引入和实现它会更方便。
Fyo*_*kin 25
是的,表面上是一个非常简单的问题。但是如果你花时间把它思考到底,你就会进入无法估量的类型理论的深处。类型理论也盯着你。
首先,当然,您已经正确地发现 F# 没有类型类,这就是原因。但是你提出了一个接口Mappable。好的,让我们研究一下。
假设我们可以声明这样一个接口。你能想象它的签名会是什么样子吗?
type Mappable =
abstract member map : ('a -> 'b) -> 'f<'a> -> 'f<'b>
Run Code Online (Sandbox Code Playgroud)
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))
Run Code Online (Sandbox Code Playgroud)
看:现在我们必须使用 lambda 表达式!无法将此.map实现作为值传递给另一个函数。有效地结束“函数作为值”(是的,我知道,在这个例子中使用 lambda 看起来并不是很糟糕,但相信我,它变得非常丑陋)
但是等等,我们还没有完成。现在它是一个方法调用,类型推断不起作用!由于 .NET 方法的类型签名取决于对象的类型,因此编译器无法推断两者。这实际上是新手在与 .NET 库互操作时遇到的一个非常常见的问题。唯一的解决方法是提供类型签名:
add42 (nestedList : #Mappable) = nestedList.map (fun l -> l.map ((+) 42))
Run Code Online (Sandbox Code Playgroud)
哦,但这还不够!即使我为nestedList自己提供了签名,我也没有为 lambda 的参数提供签名l。这样的签名应该是什么?你说它应该是fun (l: #Mappable) -> ...吗?呵呵,现在我们终于得到了排名-N类型,你看到的,#Mappable是“任何类型的快捷方式'a,使得'a :> Mappable” -即lambda表达式本身是通用的。
或者,我们可以回到更高级的类型并nestedList更精确地声明类型:
add42 (nestedList : 'f<'a<'b>> where 'f :> Mappable, 'a :> Mappable) = ...
Run Code Online (Sandbox Code Playgroud)
但是好吧,让我们暂时搁置类型推断,回到 lambda 表达式以及我们现在如何不能将map值作为值传递给另一个函数。假设我们稍微扩展了语法以允许像 Elm 对记录字段所做的那样:
add42 nestedList = nestedList.map (.map ((+) 42))
Run Code Online (Sandbox Code Playgroud)
会是什么类型.map?它必须是受约束的类型,就像在 Haskell 中一样!
.map : Mappable 'f => ('a -> 'b) -> 'f<'a> -> 'f<'b>
Run Code Online (Sandbox Code Playgroud)
哇,好的。抛开 .NET 甚至不允许存在这样的类型这一事实,实际上我们只是找回了类型类!
但首先 F# 没有类型类是有原因的。上面描述了该原因的许多方面,但更简洁的说法是:简单性。
你看,这是一个毛线球。一旦你有了类型类,你就必须有约束、更高等级、N 级(或至少 2 级),并且在你知道它之前,你要求使用不可预测的类型、类型函数、GADTs,以及所有的剩下的。
但是 Haskell 确实为所有的好东西付出了代价。事实证明,没有什么好方法可以推断出所有这些东西。高级类型有点工作,但约束已经有点不。Rank-N - 做梦都别想。即使它有效,你也会遇到类型错误,你必须有博士学位才能理解。这就是为什么在 Haskell 中你被温和地鼓励在所有东西上都加上类型签名。嗯,不是一切——一切,但实际上几乎一切。在没有放置类型签名的地方(例如,在letand里面where)——出人意料的是,这些地方实际上是单态的,所以你本质上又回到了简单的 F# 领域。
另一方面,在 F# 中,类型签名很少见,主要用于文档或 .NET 互操作。除了这两种情况之外,您可以在 F# 中编写一个完整的大型复杂程序,并且一次不使用类型签名。类型推断工作正常,因为它没有什么太复杂或模棱两可的事情要处理。
这就是 F# 相对于 Haskell 的一大优势。是的,Haskell 可以让你以非常精确的方式表达超级复杂的东西,这很好。但是 F# 会让你变得非常虚幻,就像 Python 或 Ruby 一样,如果你绊倒了,编译器仍然会抓住你。