面向 JS 开发人员的 F#
我最近在公司内部做了一次演讲,探讨了 F# 与 JS 的相似之处。这次演讲反响不错,我想把这次演讲整理成一篇博文,供感兴趣的朋友参考。
这绝不是 F# 中功能的详尽列表,但这篇文章的重点是展示熟悉的 JS 代码以及如何在 F# 中等效地编写它,因为我相信展示这样的示例是学习新语言的最佳方式,并且更有可能被采用。
简介
F# 是一种在 .NET 运行时上运行的函数式语言。随着 .NET Core 的引入,它现已跨平台,因此可以在任何机器上编写和运行。默认情况下,它是不可变的,但可以与 C# 或 VB 完全互操作。它受到 Haskell、Scala、Erlang、C# 和 Python 的启发。
F# 可用于创建服务器、脚本、桌面应用和移动应用。(甚至可以使用fable等工具直接编译为 JS,从而创建 Web 应用)
功能
函数是 F# 的核心。函数主要分为两种类型:命名函数和匿名函数。其语法与 JS 类似,但略短一些。在 F# 中,所有函数都自动柯里化,这意味着所有函数都可以部分应用,无需任何额外操作。
JS
const add = (x, y) => x + y
const mul = x => y => x * y // curried
add(4, 4) // 8
mul(4)(4) // 16
F#
let add x y = x + y
let mul x y = x * y
add 4 4 // 8
mul 4 4 // 16
// anonymous
let sub = fun x y -> x - y
sub 8 4 // 4
作品
函数组合是将一个函数的输出作为另一个函数的输入的过程。在 JS 中,需要嵌套函数,或者使用管道或 compose 函数作为辅助函数来实现。在 F# 中,有管道运算符|>
、正向组合运算符>>
和反向组合运算符<<
。
管道操作员
管道运算符只是允许函数参数位于函数前面而不是后面。
JS
const add3 = x => x + 3
const mul5 = x => x * 5
const div2 = x => x / 2
div2(mul5(add3(97))) // 250
F#
let add3 x = x + 3
let mul5 x = x * 5
let div2 x = x / 2
97 |> add3 |> mul5 |> div2 // 250
组合运算符
组合运算符允许将多个函数组合成一个。它与管道的区别在于,组合运算符只能将函数组合在一起,而管道可以接受任何值并将其传递给下一个函数。
JS
const compose = require('..')
const add3 = x => x + 3
const mul5 = x => x * 5
const div2 = x => x / 2
const doMath = compose(div2, mul5, add3)
doMath(97) // 250
F#
let add3 x = x + 3
let mul5 x = x * 5
let div2 x = x / 2
let doMath = add3 >> mul5 >> div2
// or backwards
let doMath = div2 << mul5 << add3
doMath 97 // 250
列表
F# 的列表与 JS 的数组非常相似。虽然 F# 有三种类似数组的集合:列表、数组和序列。但我将只关注列表,因为它的功能最丰富。
列表映射
列表映射在 F# 中看起来与在 JS 中几乎相同,只是您必须使用List.map
函数而不是像在 JS 中那样使用数组原型进行点链。
JS
const data = [1, 2, 3]
data.map(x => x * 2)
// [2, 4, 6]
F#
let data = [1; 2; 3]
List.map (fun x -> x * 2) data
// [2, 4, 6]
列表转换
JS 因其丰富的数组原型函数而备受赞誉,例如 map、filter、find、reduce。F# 不仅拥有这些函数,还提供了 60 多个其他函数!例如 List.sum、List.average、List.distinct、List.isEmpty、List.chunkBySize等等。
JS
[1, 2, 3]
.map(x => x * 2)
.filter(x => x > 3)
.reduce((acc, x) => acc + x, 0)
F#
[1; 2; 3]
|> List.map (fun x -> x * 2)
|> List.filter (fun x -> x > 3)
|> List.sum
条件语句
JS 有经典的 if-else 语法和三元运算符。F# 没有三元运算符,但有 if-else。F# 中实际上不需要三元运算符,因为所有内容都会被隐式返回。F# 的优点在于,由于模式匹配(下文会解释),你很少需要 if-else 语法。无论如何,这里有一个例子。
JS
const bigify = x => x > 4 ? 'big' : 'small'
bigify(2) // 'small'
bigify(5) // 'big'
F#
let bigify x = if x > 4 then "big" else "small"
bigify 2 // "small"
bigify 5 // "big"
对象/记录
JS 对象的对应对象是 F# 记录。显著的区别在于,记录始终需要与某个类型关联,默认情况下它们是引用类型,并且是不可变的。因此,您无法更新现有记录,而需要创建一个新记录并复制其值。
JS
const data = {
name: 'jason',
cool: true,
age: 3.14
}
// immutably update an object by creating a new object
const data2 = {
...data,
age: 2.16
}
F# *需要类型
let data =
{ name = "jason"
cool = true
age = 3.14 }
// immutably update a record by creating a new record
let data2 =
{ data with age = 2.16 }
记录类型
如果不先指定类型,上述示例在 F# 中是不完全可能的。
记录类型定义了记录的结构。由于 F# 具有强大的类型推断功能,因此无需将类型分配给保存数据的变量。编译器将根据定义的属性推断数据类型。因此,在下面的示例中,编译器知道这data
是一个 Person 类型,因为它定义了所有完全相同的字段。
F#
type Person =
{ name: string
cool: bool
age: float }
let data =
{ name = "jason"
cool = true
age = 3.14 }
枚举类型
JS 中没有枚举的直接比较,除非你使用带有整数的对象,但这并不完全相同。
F#
// enum
type CoolLevel =
| Good
| Medium
| Bad
type Person =
{ name: string
age: float
cool: CoolLevel } // requires a value from the enum
let data =
{ name = "lyagushka"
age = 3.14
cool = Good } // assign Good because it is in the enum
可区分联合类型
为了在 JS 中获得与联合类型等效的东西,您必须使用一些第三方模块来获得一致的类型声明,例如DaggyJS。
尽管 Daggy 在 JS 中表现出色,但它的模式匹配功能也仅限于 JS 所能达到的水平。而这正是 F# 大放异彩的地方。
如果您需要有关联合类型的解释,请参阅本文,它的解释比我更好。
下面是等效的 JS daggy 类型与本机 F# 联合类型的示例,底部是模式匹配的峰值。
JS
const { taggedSum } = require('daggy')
const ProductPage = taggedSum('ProductPage', {
Loading: [],
Error: ['msg'],
Success: ['product']
})
const product = {
name: 'Red Shoe',
price: 3.14
}
const state = ProductPage.Success(product)
// pattern match
state.cata({
Loading: () => `<div>Loading...</div>`,
Error: msg => `<div>${msg}</div>`,
Success: p => `<div>${p.name}</div>`
})
F#
type Product =
{ name: string
price: float }
type ProductPage =
| Loading
| Error of string
| Success of Product
let product =
{ name = "Red Shoe"
price = 3.14 }
let state = Success product
// pattern match
match state with
| Loading -> "<div>Loading...</div>"
| Error msg -> "<div>" + msg + "</div>"
| Success p -> "<div>" + p.name + "</div>"
模式匹配
模式匹配在机器学习类语言中非常流行,因为它功能强大。可以把它想象成强化版的 switch-case 语句。在 F# 中,使用 的语法,match [anything] with
您可以成功找出任何对象的类型或值。完全避免使用 if-else 或 switch-case 语句。
布尔值
布尔值很简单,因为它们只能是两个值中的 1 个:真或假。
let age = 6
match age > 12 with
| true -> printf "Teen"
| false -> printf "Not teen"
数字
数字并不像布尔值那样直接,因为可能存在无限数量的匹配可能性,因此当尝试匹配数字时,如果没有匹配的模式,您将被迫使用下划线提供默认模式。
let age = 5
match age with
| 13 -> "teen"
| 1 -> "One Year Old"
| 4 | 5 -> "little" // 4 or 5 will match here
| x when x < 0 -> "not alive" // conditional logic
| _ -> "any other age" // default incase age is not matched with anything
列表
与列表匹配更酷,因为您可以使用下划线作为列表内任何值的通配符。
let myList = [1; 2]
match myList with
| [] -> "empty list"
| [ _ ] -> "list has 1 item"
| [ _; 5 ] -> "list has 2 items, 2nd item is 5"
| [ _; _; _ ] -> "list has 3 items"
| _ -> "list does not match any of the above patterns"
单子
Monad 是一个很大的话题,我甚至写了一整篇关于 JS 中的 monad 的文章。
在 F# 中,一些 monad 是内置的,例如 Option 类型,除了输入 Some 或 None 之外,不需要进一步的工作即可使用。
JS
const { taggedSum } = require('daggy')
const Maybe = taggedSum('Maybe', {
Just: ['value'],
Nothing: []
})
const { Just, Nothing } = Maybe
const data = Just(50)
data.cata({
Just: x => console.log(`Value: ${x}`), // 50
Nothing: () => console.warn("Nothing here")
})
F#
let data = Some(50)
match data with
| Some x -> printf "Value: %i" x
| None -> printf "Nothing here"
打字
关于在 F# 中编写函数类型的简要说明。下面我写了 4 遍完全相同的函数,每次都用不同的方式定义类型。
第一个具有隐式类型,让编译器根据调用者和传递给它的数据推断类型。
第二个定义每个参数的类型,然后定义返回类型。
第三和第四个使用类型签名和匿名函数来定义类型。
所有这些都是有效的,并且每个都可以用于不同的用例。
F#
// inferred types
let add x y = x + y
// explicit types
let add (x: float) (y: float): float = x + y
// explicit inline type signature
let add: float -> float -> float = fun x y -> x + y
// explicit separate type signature
type Add = float -> float -> float
let add: Add = fun x y -> x + y
HTTP 请求
JS 的一大优点是易于利用 Promise 类型执行异步操作,例如 HTTP 请求。
F# 中也内置了 Async 功能,通过使用async
关键字即可。下面是一个等效的 http 请求示例,用于获取页面的 HTML。
JS
const axios = require('axios')
axios
.get('https://github.com/rametta')
.then(({ data }) => console.log(`HTML: ${data}`))
.catch(console.error)
F#
// sync
let html = Http.RequestString("https://github.com/rametta")
// async
async { let! html = Http.AsyncRequestString("https://github.com/rametta")
printfn "%d" html.Length }
|> Async.Start
其他很酷的 F# 内容
简要介绍其他简洁的 F# 功能片段。
范围运算符
使用两个点来快速定义一个范围。
let myList = [ 1..5 ]
// [1; 2; 3; 4; 5]
可变关键字
当想要改变变量时,使用 mutable 关键字作为逃生出口。
let mutable data = 6
data <- 8
Yield 关键字
let mySeq = seq {
for i in 1..10 do
for j in 10..15 do
yield i * j
}
元组
let myTuple = (5, "hello")
let typedTuple: int * string = (5, "hello")
我希望本文能够阐明 F# 与 JS 的相似之处,并希望它能鼓励您在未来的项目中使用它。
如果您想了解有关 F# 的更多信息,请查看fsharpforfunandprofit!
欢迎在 Twitter 上关注我!@rametta
鏂囩珷鏉ユ簮锛�https://dev.to/rametta/f-for-js-devs-2b88