为什么要选择 F#?

23 分钟阅读

如果几个月前有人告诉我,在中断 15 年多后,我又要玩 .NET 了,我可能会嘲笑他。1 在我职业生涯的早期,我玩过 .NET 和 Java,即使 .NET 在某些方面比 Java 做得更好(因为它有机会从一些早期的 Java 错误中学习),但我很快就选择了 Java,因为它是一个真正可移植的环境。

我想每个阅读我的博客的人都知道,在过去的几年里,我一直在断断续续地玩 OCaml,我认为可以肯定地说,它已经成为我最喜欢的编程语言之一,与 Ruby 和 Clojure 等语言并列。我最近对 OCaml 的研究引起了我对 F# 的关注,F# 是一种以 .NET 为目标的 ML 语言,由 Microsoft 开发。它是(主要)面向对象的 C# 的函数式对应物。最新的 ML 语言...

什么是 F#?Permalink

可惜的是,没有人能告诉你什么是 Matrix。你必须亲眼看到它。 – Morpheus, The Matrix

在我们开始讨论 F# 之前,我想我们应该首先回答“什么是 F#?”这个问题。我将从官方页面借用一些内容来回答它。

F# 是一种通用的编程语言,用于编写简洁、健壮和高性能的代码。

F# 允许你编写简洁、自文档化的代码,让你专注于你的问题域,而不是编程的细节。

它在不牺牲速度和兼容性的前提下实现了这一点——它是开源的、跨平台的并且可互操作的。

open System // 访问 System 命名空间中的功能。
// 定义一个名称列表
let names = [ "Peter"; "Julia"; "Xi" ]
// 定义一个接受名称并生成问候语的函数。
let getGreeting name = $"Hello, {name}"
// 为每个名称打印问候语!
names
|> List.map getGreeting
|> List.iter (fun greeting -> printfn $"{greeting}! Enjoy your F#")

花絮: F# 是使管道操作符 (|>) 流行的语言。

F# 具有许多特性,包括:

完整的特性集记录在 F# 语言指南中。

听起来很有希望,对吧?

F# 1.0 于 2005 年 5 月由 Microsoft Research 正式发布。它最初由 Don Syme 在 Cambridge 的 Microsoft Research 开发,并从一个名为“Caml.NET”的早期研究项目演变而来,该项目旨在将 OCaml 引入 .NET 平台。2 2010 年,F# 正式从 Microsoft Research 转移到 Microsoft(作为其开发者工具部门的一部分)(与 F# 2.0 的发布时间一致)。

自早期以来,F# 一直在稳步发展,最新的版本 F# 9.0 于 2024 年 11 月发布。F# 在其 20 周年纪念日引起我的注意似乎是很合适的!

我想尝试 F# 有几个原因:

下面你会发现我对几个领域的初步印象。

语言Permalink

作为 ML 语言家族的成员,其语法不会让任何熟悉 OCaml 的人感到惊讶。但是,由于熟悉 OCaml 的人不多,因此我会提到 Haskell 程序员也会觉得语法很熟悉。还有 Lispers。

对于其他人来说,掌握基础知识应该相当容易。

// 函数应用
printfn "Hello, World!"
// 函数定义
let greet name =
  printfn "Hello, %s!" name
greet "World"
// 空格很重要,就像在 Python 中一样
let foo =
  let i, j, k = (1, 2, 3)
  // Body expression:
  i + 2 * j + 3 * k
// 条件表达式
let test x y =
 if x = y then "equals"
 elif x < y then "is less than"
 else "is greater than"
printfn "%d %s %d." 10 (test 10 20) 20
// 循环遍历列表。
let list1 = [ 1; 5; 100; 450; 788 ]
for i in list1 do
  printfn "%d" i
// 循环遍历元组序列
let seq1 = seq { for i in 1 .. 10 -> (i, i*i) }
for (a, asqr) in seq1 do
 printfn "%d squared is %d" a asqr
// 一个简单的 for...to 循环。
let function1 () =
 for i = 1 to 10 do
  printf "%d " i
 printfn ""
// 一个反向计数的 for...to 循环。
let function2 () =
 for i = 10 downto 1 do
  printf "%d " i
 printfn ""
// 记录
// 在同一行定义时,标签用分号分隔。
type Point = { X: float; Y: float; Z: float }
// 您可以在自己的行上定义带有或不带有分号的标签。
type Customer =
  { First: string
   Last: string
   SSN: uint32
   AccountNumber: uint32 }
let mypoint = { X = 1.0; Y = 1.0; Z = -1.0 }
// 可区分联合
type Shape =
  | Circle of radius: float
  | Rectangle of width: float * height: float
// 使用模式匹配的函数
let area shape =
  match shape with
  | Circle radius -> System.Math.PI * radius * radius
  | Rectangle (width, height) -> width * height
let circle = Circle 5.0
let rectangle = Rectangle(4.0, 3.0)
printfn "Circle area: %f" (area circle)
printfn "Rectangle area: %f" (area rectangle)

没有什么令人震惊的,对吧?

这是另一个稍微复杂一些的例子:

open System
// 示例数据 - 简单的销售记录
type SalesRecord = { Date: DateTime; Product: string; Amount: decimal; Region: string }
// 示例数据集
let sales = [
  { Date = DateTime(2023, 1, 15); Product = "Laptop"; Amount = 1200m; Region = "North" }
  { Date = DateTime(2023, 2, 3); Product = "Phone"; Amount = 800m; Region = "South" }
  { Date = DateTime(2023, 1, 20); Product = "Tablet"; Amount = 400m; Region = "North" }
  { Date = DateTime(2023, 2, 18); Product = "Laptop"; Amount = 1250m; Region = "East" }
  { Date = DateTime(2023, 1, 5); Product = "Phone"; Amount = 750m; Region = "West" }
  { Date = DateTime(2023, 2, 12); Product = "Tablet"; Amount = 450m; Region = "North" }
  { Date = DateTime(2023, 1, 28); Product = "Laptop"; Amount = 1150m; Region = "South" }
]
// 快速分析管道
let salesSummary =
  sales
  |> List.groupBy (fun s -> s.Product)             // 按产品分组
  |> List.map (fun (product, items) ->             // 转换每个组
    let totalSales = items |> List.sumBy (fun s -> s.Amount)
    let avgSale = totalSales / decimal (List.length items)
    let topRegion =
      items
      |> List.groupBy (fun s -> s.Region)          // 嵌套分组
      |> List.maxBy (fun (_, regionItems) ->
        regionItems |> List.sumBy (fun s -> s.Amount))
      |> fst
    (product, totalSales, avgSale, topRegion))
  |> List.sortByDescending (fun (_, total, _, _) -> total)   // 按总销售额排序
// 显示结果
salesSummary
|> List.iter (fun (product, total, avg, region) ->
  printfn "%s: $%M total, $%M avg, top region: %s"
    product total avg region)

为什么不尝试将上面的代码段保存在一个名为 Sales.fsx 的文件中,然后像这样运行它:

dotnet fsi Sales.fsx

现在你知道 F# 是即席脚本的绝佳选择!此外,单独运行 dotnet fsi 将会弹出一个 F# REPL,你可以在其中随意探索该语言。

我不会在这里详细介绍,因为我之前在这里写的关于 OCaml 的大部分内容也适用于 F#。我还建议快速浏览 F#,以便更好地了解它的语法。

提示: 如果你想查看快速语法参考,请查看 F# cheatsheet

给我留下深刻印象的一件事是语言设计者专注于让 F# 更容易被新手接受,他们为新手提供了许多小的生活质量改进。以下是一些示例,可能对你来说意义不大,但对熟悉 OCaml 的人来说意义重大:

// 行注释
(* 经典的 ML 注释也存在 *)
// 可变值
let mutable x = 5
x <- 6
// 范围和切片
let l = [1..2..10]
name[5..]
// C# 方法调用看起来很自然
let name = "FOO".ToLower()
// 运算符可以为不同的类型重载
let string1 = "Hello, " + "world"
let num1 = 1 + 2
let num2 = 1.0 + 2.5
// 通用打印
printfn "%A" [1..2..100]

我想其中一些可能会引起争议,具体取决于你是否是 ML 语言的纯粹主义者,但在我看来,任何使 ML 更受欢迎的事情都是一件好事。

我是否还提到可以很容易地使用 unicode 字符串和正则表达式?

人们经常说 F# 主要是未来 C# 特性的预演,也许这是真的。我还没有足够长时间地观察这两种语言,无法对这个问题发表自己的看法,但我印象深刻地了解到 async/await(C# 和后来的 JavaScript 的成名之作)起源于……F# 2.0。

2012 年,当 C#5 发布时,情况发生了变化,其中引入了现在流行的 async/await 关键字配对。此功能允许你编写具有手写异步代码的所有优点的代码,例如在启动长时间运行的进程时不会阻塞 UI,但读取起来就像普通的同步代码一样。这种 async/await 模式现在已经出现在许多现代编程语言中,例如 Python、JS、Swift、Rust,甚至 C++。 F# 的异步编程方法与 async/await 略有不同,但实现了相同的目标(事实上,async/await 是 F# 方法的简化版本,该方法在几年前在 F#2 中引入)。 – Isaac Abraham, F# in Action

时间会证明会发生什么,但我认为 C# 永远不可能完全取代 F#。

我还发现了这条来自 2022 年的令人鼓舞的评论,表明 Microsoft 可能愿意在 F# 上投入更多:

给你的好消息。经过 10 年由内部 2.5 个人和一些随机社区努力开发的 F#,Microsoft 最终决定适当地投资 F#,并在今年夏天在布拉格创建了一个成熟的团队。我是这个团队中的一个开发人员,就像你一样,我也是多年的 F# 粉丝,所以我很高兴事情终于在这里开始发展了。

看看 F# 8.0 和 F 9.0 中的变化,似乎新的成熟团队已经做了很多伟大的工作!

生态系统Permalink

在这么短的时间内很难评估 F# 周围的生态系统,但总的来说,在我看来,相对较少的“原生”F# 库和框架,大多数人严重依赖核心 .NET API 和许多面向 C# 的第三方库和框架。这是一种相当常见的设置,当涉及到托管语言时,通常不会有太多令人惊讶的地方。

如果你曾经使用过另一种托管语言(例如 Scala、Clojure、Groovy),那么你可能知道会发生什么。

Awesome F# 跟踪流行的 F# 库、工具和框架。我将在这里重点介绍 Web 开发和数据科学库:

Web 开发

数据科学

到目前为止,我还没有对它们进行过多的研究,因此我将保留任何反馈和建议,留待将来某个时候再进行。

文档Permalink

官方文档非常好,尽管我发现其中一些文档托管在 Microsoft 的网站上,而其余文档托管在 https://fsharp.org/(F# Software Foundation 的网站)上,这有点奇怪。

我非常喜欢文档的以下部分:

https://fsharpforfunandprofit.com/ 是另一个很好的学习资源。(即使它看起来有点过时)

开发工具Permalink

F# 的开发工具故事有点坎坷,因为从历史上看,对 F# 的支持仅在 Visual Studio 中很棒,而在其他地方则有点差。幸运的是,在过去的十年中,工具链的故事有了很大的改进:

2014 年,Tomas Petricek、Ryan Riley 和 Dave Thomas 以及许多后来的贡献者创建了 FSharp.Compiler.Service (FCS) 包,并在技术上取得了突破。它包含 F# 编译器、编辑器工具和脚本引擎的核心实现,形式为单个库,可用于在各种情况下制作 F# 工具。这使得 F# 可以交付到更多的编辑器、脚本和文档工具中,并允许开发 F# 的替代后端。关键的基于社区的编辑器工具包括 Krzysztof Cieślak 及其贡献者的 Ionide,用于在跨平台 VSCode 编辑器中提供丰富的编辑支持,截至撰写本文时下载量超过 100 万次。 – Don Syme,《F# 的早期历史》

我玩过几个编辑器的 F# 插件:

总的来说,Rider 和 VS Code 提供了最多(和最完善)的功能,但其他选项也非常有用。这很大程度上归功于 F# LSP 服务器 fsautocomplete(命名很难!)非常强大,并且任何具有良好 LSP 支持的编辑器都可以免费获得许多功能。

不过,我还是要提到我发现这些工具在某些方面有所不足:

我真的在努力使用 VS Code 的键绑定(太多的修改键和功能键不合我的口味)和编辑模型,因此我可能会继续使用 Emacs。或者我终于会花更多的时间和 neovim 在一起!

似乎每个人都在使用相同的代码格式化程序 (Fantomas),包括 F# 团队,这太棒了!F# 中的 linter 故事并不那么好(似乎唯一流行的 linter FSharpLint 这些天已经过时了),但是当你的编译器如此出色时,你真的不需要太多 linter。

哦,好吧……似乎 Microsoft 并没有真正特别投入支持 F# 的工具,因为这个领域几乎所有的主要项目都是社区驱动的。

使用 AI 编码代理(例如 Copilot)与 F# 配合得很好,但我没有在这方面花费太多时间。

归根结底,任何编辑器都可能会奏效,只要你使用的是 LSP。

顺便说一句,我在用 F#(以及 OCaml)编程时有一个有趣的观察 - 当你使用具有真正好的类型系统的语言时,你真的不需要编辑器提供太多功能。大多数时候,我只需要一些内联类型信息(例如类似 CodeLenses 的东西)、自动完成以及轻松地将代码发送到 fsi 的能力就非常满意了。简洁仍然是最终的复杂性……

你应该关注的其他工具包括:

用例Permalink

鉴于 .NET 的深度和广度 - 我想天空是你唯一的限制!

在我看来,F# 特别适合用于数据分析和操作,因为它具有 类型提供程序 等特性。

可能适合后端服务,甚至全栈应用程序,尽管我还没有真正使用过这方面的 F# first 解决方案。

Fable 和 Elmish 使 F# 成为客户端编程的可行选择,并且可能提供另一种轻松地将 F# 偷偷引入你日常工作中的方法。

注意: 从历史上看,Fable 一直用于以 JavaScript 为目标,但自从 Fable 4 以来,你也可以以其他语言为目标,例如 TypeScript、Rust、Python 等。

以下是将 F# 代码库转译成其他内容是多么容易:

# 如果你想转译成 JavaScript
dotnet fable
# 如果你想转译成 TypeScript
dotnet fable --lang typescript
# 如果你想转译成 Python
dotnet fable --lang python

很酷的东西!

社区Permalink

我对社区的初步印象是它相当小,甚至可能比 OCaml 的社区还要小。F# Reddit 和 Discord(Reddit 上列出的那个)似乎是 F# 对话最活跃的地方。应该也有一些 F# Slack,但我无法获得邀请。(似乎发出这些邀请的自动化过程已经损坏了一段时间)

我仍然不确定 Microsoft 在社区中扮演什么角色,因为我总体上没有看到他们提供太多。

对我来说,小社区并不是真正的问题,只要社区充满活力和积极性。而且 - 我注意到我总是觉得与较小的社区联系更紧密。从 Java 转移到 Ruby 在社区参与和归属感方面感觉像是天壤之别。

我没有找到许多专门针对 F# 的书籍和社区网站/博客,但我一开始并没有真正期望找到。

我发现的最值得注意的社区倡议是:

在我看来,可以做更多的工作来推广该语言并让新的程序员和企业参与其中,尽管在某些项目存在 20 年后,这从来都不是一件容易的事。我仍然有点困惑为什么 Microsoft 不更多地营销 F#,因为我认为它可能是他们的一个伟大的营销工具。

总而言之 - 我觉得我没有资格在这一点上对 F# 社区发表太多评论。

人气竞赛Permalink

根据你的类型,你可能关心也可能不关心编程语言的“人气”。人们经常问我为什么我花很多时间学习不太可能为我带来工作机会的语言,例如:

专业机会当然很重要,但以下也很重要:

也就是说,按照大多数传统指标,F# 并不是一种流行的语言。它在 TIOBE、StackOverflow 或大多数招聘网站上的排名都不高。但它并不比大多数“主流”函数式编程语言更不受欢迎。可悲的现实是,函数式编程仍然不是主流,也许它永远不会成为主流。

关于这个主题的更多资源:

F# 与 OCamlPermalink

F# 的早期概念很简单:将 OCaml 的优势带到 .NET,并将 .NET 带到 OCaml:强类型函数式编程和 .NET 之间的结合。这里的“OCaml”既指语言本身的核心,也指它所代表的强类型函数式编程的务实方法。最初的任务相对明确:我将重新实现 OCaml 语言的核心及其基础库的一部分,以针对 .NET Common Language Runtime。该实现将是全新的,即不使用任何 OCaml 代码库,以确保法律清晰。 – Don Syme,《F# 的创建者,《F# 的早期历史》

F# 派生自 OCaml,因此这两种语言共享许多 DNA。早期,F# 努力支持尽可能多的 OCaml 语法,它甚至允许对 F# 代码使用 .ml.mli 文件扩展名。随着时间的推移,这两种语言开始出现一些分歧。3

当然,创建一种独立于 OCaml 的语言从一开始就是预期的。这也反映在选择名称 F# 的决定中,即使该语言的早期版本被称为“Caml.NET”:

尽管 F# 的第一个版本最初被呈现为“.NET 的 Caml”,但实际上它始终是一种新语言,从第一天起就为 .NET 设计。F# 从未与任何版本的 OCaml 完全兼容,尽管它共享一个兼容的子集,并且它将 Caml-Light 和 OCaml 作为其设计指导和灵感的主要来源。 – Don Syme,《F# 的早期历史》

如果你问大多数人关于 F# 相对于 OCaml 的优缺点,你可能会得到以下答案。

F# 优点

F# 缺点

F# 和 OCaml 也可以通过 F# 端的 Fable 和 OCaml 端的 Js_of_ocaml 和 Melange 定位 JavaScript 运行时。粗略一看,Fable 似乎是一个更成熟的解决方案,但我还没有充分使用这三者中的任何一个,无法提供有根据的意见。

归根结底,两者仍然是两个相当相似的健壮但小众的语言,它们不太可能在未来变得非常流行。我猜对大多数人来说,使用 F# 进行专业工作更有可能发生,因为 .NET 非常受欢迎,我可以想象在已建立的 C# 代码库中偷偷摸摸地使用一些 F# 很容易。

我注意到 F# 项目中一件奇怪的事情是,它们仍然使用 XML 项目清单,你必须在其中手动列出源文件,并按应编译的顺序排列(以解决它们之间的依赖关系)。我有点震惊的是,编译器无法自动处理依赖关系,但我猜那是因为在 F# 中,源文件和模块之间没有直接的映射。无论如何 - 我更喜欢 OCaml 编译过程(和 Dune)。

由于我对 ML 的兴趣主要是教育方面的,因此我个人倾向于 OCaml,但是如果我必须使用 ML 语言构建 Web 服务,我可能会选择 F#。我也非常尊重每种都有自己运行时的语言,因为这意味着运行时不太可能对该语言做出一些妥协。

结束语Permalink

问题:C# 可以做些什么而 F# 不能?回答:NullReferenceException! – F# 社区玩笑

总而言之,我比预期更喜欢 F#!在某种程度上,它让我想起了我过去使用 Clojure 的经历,因为 Clojure 是当时最实用的 Lisp,这主要是因为它与 Java 具有出色的互操作性。

我有一种感觉,如果 .NET 从一开始就是可移植的(并且是开源的),那么 ClojureCLR 可能会像 Clojure 一样受欢迎,并且 F# 现在可能会发展出更大的社区和更广泛的用途。我很确定,如果不是因为 .NET Core,我永远不会再次涉足 .NET,我怀疑我不是唯一的一个。直到 2010 年 F# 才开源的事实也没有帮助早期的采用。

似乎我并不是唯一一个这样认为的人:

错误很难承认,最好在历史背景下看待它们。从早期历史来看,与 F# 相关的最大错误是 .NET 和该语言都不是开源的,也没有使用开放工程。核心贡献者当时很好地理解了这个错误,并且 Microsoft 的许多人都主张转向开源。简而言之,一种创新的语言在一家尚未拥抱开源的公司研究实验室中发展起来:那些参与其中的人通过源代码投放尽了最大努力,并且该问题最终通过从 2011 年到 2014 年转向开源工程和设计来解决。纠正这个错误可能是该语言历史上最重要的发展。此外,F# 能够在 2002 年至 2011 年期间使用封闭式工程进行导航,这很大程度上归功于 Microsoft 决策者对其品质的认可。 – Don Syme,《F# 的早期历史》

学习 OCaml 绝对不难,但我认为有兴趣学习一些 ML 方言的人可能会更容易学习 F#。而且,如前所述,你可能会更容易地将其“投入生产”。

我认为每个有 .NET 经验的人都将受益于学习 F#。也许更重要的是 - 每个希望使用 ML 系列语言做更多事情的人都应该考虑 F#,因为它本身就是一种伟大的语言,可以让你访问现有的最强大的编程平台之一。

让我们不要忘记 Fable,它使你可以在 JavaScript、Dart、Rust 和 Python 运行时中利用 F#!

那么,为什么要选择 F#?在 F# 社区中,有这样一句谚语,F# 中的“F”代表“乐趣”。在我与 F# 的短暂经历中,我发现这是非常正确的!我将更进一步,并声称 F# 既非常有趣又非常实用!

此外,如果你的代码可以编译,那么它可能会按照你期望的方式工作。我听说这通常被认为是编程世界中令人向往的事情!

这就是我今天给你的全部内容。请在评论中分享你喜欢 F# 的什么!

我们相信健全的类型系统!

讨论Permalink

  1. 我在大学里上了一些 C# 课程,并且用 C# 编写了我的学士论文。它是 Arch Linux 的 pacman 的重写,在 Mono 上运行。那是 2007 年。
  2. 参见 https://fsharp.org/history/hopl-final/hopl-fsharp.pdf
  3. https://github.com/fsharp/fslang-suggestions/issues/985

标签: .NET, F#, ML, OCaml

更新时间: 2025 年 3 月 30 日

分享到

Twitter Facebook LinkedIn 上一篇 下一篇

你可能也喜欢

更新我的工具箱:Ghostty 和 Fish

5 分钟阅读

通常,当我设置新计算机时,我会花一些时间评估我的编程工具箱,并对其进行一些(通常很小的)调整。一月份,我...

OCaml 的标准库 (Stdlib)

8 分钟阅读

每种编程语言都包含一些“电池”,主要以其标准库的形式存在。这通常是所有功能的...

neocaml:用于 OCaml 编程的新 Emacs 包

4 分钟阅读

我不是 Emacs 中 TreeSitter 的早期采用者,因为通常如此大的转变并不顺利,并且 Emacs 中对 TreeSitter 的初始支持还有很多不足之处...

学习 OCaml:没有参数的函数

1 分