【C#/F#】「函数式编程」第二讲:函数签名,与“诚实”的函数

函数是函数式编程的核心,在之前的内容中,我们介绍了有关F#强大的类型推断能力,但F#其实是一门静态强类型的语言,C#自然也是。对于这种类型的语言,函数和其参数都是有自己的类型。在这样的情况下,我们可以通过一些方式去描述我们的函数。

函数签名

函数签名通常由箭头符号组合而成,让我们从最简单的开始

int AddOne(int a) => a + 1;

这个函数的签名就是

AddOne: int -> int

代表"函数AddOne输入一个int返回一个int"。

对于返回为void或者没有输入参数的函数来说,我们一般使用()表示,例如()->string,代表, 函数不接受任何参数,返回一个string。

以下是一些函数的例子,左侧为函数签名,右侧为C#函数(lambda)

() -> string          () => "Hello, World!"; 
() -> int             () => 42;
(int, int) -> int     (int a, int b) => a + b;
string -> int         (string s) => s.Length;
int -> bool           (int x) => x % 2 == 0;
() -> ()              () => Console.WriteLine("Hello, World!");

还记得我们上一讲说到的高阶函数吗?也可以写出其函数签名

Func<int, int, bool> SwapArgs(Func<int, int, bool> func) 
  => return (a, b) => func(b, a);
  
((int, int) -> int) -> ((int, int) -> int)
((a, b) -> c) -> ((b, a) -> c) //泛型形式

我们可以看到Func<int, int, bool>这种C#的表达方式其实也能够理解为函数签名的一种,但显而易见的,箭头表示会更加简单,也更易懂

下面是F#中,同样的函数签名代表的函数,通常F#的编辑器也会提示出函数签名的信息

let f1() = "Hello World"
let f2() = 1
let f3 a b = a + b
// let f3(a, b) = a + b 
let f4 (s:string) = s.Length
let f5 d = d % 2 = 0
let f6() = printfn "Hello World"

如下图所示

图片

一个函数签名,有时可以有效地表达出这个函数是做什么的,

如果一个函数签名是() -> (), 你可能会非常困惑,这个函数啥也不要,啥也不给,没有任何约束,他可能会去干任何事情(例如格式化你的磁盘(就像编译器中的未定义行为))

如果一个函数签名是 int -> bool, 这时候你可能会有点头绪(尽管它依然可以去做我们上述说的未定义行为),他可能是判断一个人的体重符不符合标准,或是温度适不适合去做某件事情。

如果一个函数签名是PlayerLevel -> bool, 这时你更可以相信你自己的判断,这个函数很可能是判定游戏人物等级是否达成某个条件的函数。

我们再来看一个函数签名

(IEnumerable<T>, (T -> bool)) -> IEnumerable<T>

经过我们前面的学习,我们可以很容易地说出,这是一个接受T元素的列表,和一个接受T返回bool的函数,最后返回一个T元素的列表。

到这里你可能已经有了答案,虽然还不能确定,但这个函数很有可能是Linq中的Where函数!函数签名虽然很难表达出完整的信息,但已经可以让让使用者有着一定的推测了,如果配合好函数本身的名字(Where,Add等), 这个函数本身就会变得更加清晰明确。

不过正如第二个函数签名所说到,函数签名和函数本身做什么事其实并无关系,它可以无意义的接受参数,也可以无意义的返回。这里便需要我们人为的去规定他,如果一个函数是可以通过其函数签名和函数名预测的,那便是诚实的函数,和纯函数有些类似,诚实的函数需要返回一个他声明的类型,也不会抛出异常,也不返回空值。不过诚实更灵活一点,另外诚实的函数也是允许有副作用的(例如io等等),与纯函数相比,诚实这个概念更主观,灵活一点。当然我们也期望能去编写,使用诚实的函数,这会更有利于去使用。

那么如何去编写出尽可能诚实的函数呢?这其实也没有那么容易,因为如果使用原始类型,例如 int, double等,其表达的范围太过于广阔,如果使用的dynamic,那更是海纳万物。所以难免会接收到一些稀奇古怪的输入,对于一个诚实的人来说,想要回答自己知识范围外的问题,我们只能“诚实"的回答 不知道,或是 这个数据不合法,我没办法回答,而不是直接回答一个 默认答案,但是在代码中,我们没有办法这么灵活,最直接的办法可能就是直接抛出一个错误,传出我们遇到的问题,但诚实的函数或是纯函数都不应该抛出错误(因为他并没有返回值)。

例如判断一个人的年龄,对于负数或者过大的年龄,我们如果直接回答false,那么是不诚实的。但抛出错误,也不是不诚实的(调用者想要一个bool, 但函数却抛出了错误)

bool IsAgeYoung(int age)
{
    if (age < 0 || age > 120)
    {
        throw new ArgumentException("Age must be between 0 and 120");
    }
    return age <= 24;
}

这个函数不仅不诚实,而且所有用到类似age的参数时,都需要添加这一段验证

我们该怎么办呢?可以想一想如何在这种情况下,我们仍然保持诚实。

图片

既然输入有可能不合法,那我们让输入总是合法不就行了吗?

没错,这是一种解决方案,我们需要更精确的表达数据。

我们声明一个Age类型

public class Age
{
    public int Value { get; }
    public Age(int value)
    {
        if (value < 0 || value > 120) 
            throw new ArgumentException("Age must be between 0 and 120"); 
        Value = value;
    }
}
bool IsAgeYoung(Age age) => age.Value <= 24;
// Age -> bool

在构造Age的时候,我们便已经限定了数据范围,我们在后面使用Age的时候,自然也就不需要考虑会出现数据范围外的情况了(也无需每次都去验证)。

这是事实上就是函数式的思想,我们更精确的去表达了我们的数据,也表达了函数的定义域。我们确定了定义域,函数也会诚实的返回我们需要的值,是不是函数的值域也有了?这便是函数式编程,我们热衷去精确地表达我们的数据。(不可用的情况放到后面)

类型组合

上面提到了如何用类型精确的表达数据,不过只考虑了一个变量的情况,如果我们还希望将性别纳入考量,函数的签名就变成了

(Age, Gender) -> bool

通常来说性别的取值范围只有男性与女性两种(它可以是一个枚举类型),再加上刚刚我们限定的Age在[0, 120]的区间,我们可以轻松的知道,我们函数的输入有2*121=242种输入情况。这个时候,我们完全可以把两个参数组合到一起

PersonData -> bool

PersonData本身也是242种取值情况。这和输入两个参数的情况本质没有任何区别。在原本参数的范围明确的情况下,我们可以轻松的把数据组合成新的数据,也同样的清晰。

P.S. 组合这个概念在函数式编程中会经常见到,尽管简单的类型组合并不是函数式编程独有的概念,但也可以小窥一下思想

还有其他办法吗?我们可以再想想,上面的办法是封死了不合理输入,我们可以尝试往另一个方向思考,输出。我们有办法,让输出变得合法吗?这个问题留到之后再做讲述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值