在汇编编程中调用函数是将参数压入堆栈(或寄存器,取决于调用约定)然后跳转到函数地址的过程。
在 Java、C#、C 等更高级的语言中,这个过程对我们来说或多或少是隐藏的。但是,当我们调用不带参数的函数时,我们会看到它的痕迹,即void。
在 F#、haskell 等函数式编程语言中,函数的概念更接近于从单个输入产生答案的数学函数。
要查看 F# 中的所有函数都接受单个输入,让我们看看以下函数:
// val f: (int*int) -> int
let f (x,y) = x + y
f 接受一对整数并产生一个整数。从 f 的角度来看,这对是一个单独的值,它解构以产生答案。
// val g: int -> int -> int
let g x y = x + y
这显然看起来像是一个接受两个整数来产生一个整数的函数,但如果我们稍微重写一下,我们会发现情况并非如此:
// val h: int -> int -> int
let h x = fun y -> x + y
h 等价于g,但这里我们看到h 实际上采用一个整数,该整数产生一个函数,该函数接受一个整数来产生一个整数。
签名中的-> 是右关联的,我们添加了括号,我们可以更清楚地看到g 和h 实际上是采用单个输入。
// val g: int -> (int -> int)
let g x y = x + y
// val h: int -> (int -> int)
let h x = fun y -> x + y
let gx = g 1 2
let gy = (g 1) 2
let hx = h 1 2
let hy = (h 1) 2
在我看来,F# 中的函数比 C#/Java 中的函数具有更高的抽象级别,因为 C#/Java 函数在概念上比 F# 函数更接近汇编语言函数。
此外,如果每个函数都需要一个参数,那么对于不接受任何参数的函数来说意义不大。
但是这个函数呢?
// val i: unit -> int
let i () = 3
它不接受产生 3 的任何参数吗?不,它接受单位值(),这只是unit 类型中的值。
在 Haskell 中,它们为接受 void 并产生答案的函数命名:
absurd :: Void -> a
值也许可以看作是一个不带参数的函数,但我不是范畴论专家。
回到示例代码:
type Function =
| UnitFunction of (unit -> unit)
| OperandFunction of (unit16 -> unit)
函数式方法是这样的:
type Abstraction =
| Concrete of obj
| Function of Abstraction -> Abstraction
即Abstraction 是一个值或一个函数。
从代码来看,它似乎模拟了一些类似于汇编语言的东西,因此在这种情况下,可以将函数视为推送参数并跳转到一个地址。
type Function =
| VoidFunction of (unit -> unit)
| UnaryFunction of (unit16 -> unit)
| BinaryFunction of (unit16 -> unit16 -> unit)
希望这很有趣。
PS。
看起来unit 类型是一个小细节,但在 IMO 中它可以带来很多好处。
- 无需声明。
- 简化泛型编程(
void 情况通常需要特殊情况,请考虑Task<'T> 和Task)。
- 让我们可以像数学函数一样思考函数,而不是跳转到内存中的地址。