活动模式小例(一)

Applied Active Pattern in F# (1)

原创:顾远山
著作权归作者所有,转载请标明出处。

Active patterns enable you to define named partitions that subdivide input data, so that you can use these names in a pattern matching expression just as you would for a discriminated union. You can use active patterns to decompose data in a customized manner for each partition.

F# Language Reference

活动模式是F#语言的一个特性。根据上述官方语言指南,活动模式允许你通过定义命名分区分割输入数据,继而在模式匹配表达式中像可区分联合那样使用这些名字。你可以使用活动模式给每个分区按定制化的方式分解数据。

这句话读起来多少有点晦涩难懂,命名分区是什么鬼,模式匹配又是什么鬼,可区分联合又是什么鬼,每个字都认识,连在一起就不知道什么意思了。在揭示活动模式的本质之前,我们最好直观地观察一下它是怎么用的,知其然再知其所以然。为了方便理解,以下举个简单的例子,这个例子通过活动模式来判断输入字符串是身份证号码、护照号码还是未知号码。

let (|IDNumber|PassportNumber|UnknownNumber|) input = 
    match Regex(@"\d{18}").Match(input).Success with
    | true -> IDNumber(input)
    | _ ->
        match Regex(@"G|E\d{8}").Match(input).Success with
        | true -> PassportNumber(input)
        | _ -> UnknownNumber(input)

光看上面的代码,感觉用普通函数也能实现类似的功能,似乎普通函数比它甚至还简洁许多,如下:

let isIDNumber input = Regex(@"\d{18}").Match(input).Success
let isPassportNumber input = Regex(@"G|E\d{8}").Match(input).Success
let isUnknownNumber input = not isIDNumber && not isPassportNumber

仔细对比的话,两者还是有区别的。活动模式的返回值不仅包含了对输入数据的分类,还包含了输入数据的拆分(本例是原值);而普通函数只对输入数据进行判断,只返回一个代表是否的布尔型结果,没有分类(甚至都不知道有几类),也没有输入数据(当然如果非要返回元组的话也可以带上输入数据,但就和函数名的意义冲突,显得画蛇添足了)。

如果接下来的业务逻辑是基于号码类型的判断进一步在号码上做处理的话,差异会更明显一些,比如,对于身份证号码则提取出生年份并输出,对于护照号码则把护照类型并输出,对于未知号码则原值输出。使用活动模式可以这么做:

let print input = 
    match input with
    | IDNumber(num) -> printfn "Birth Year: %s" num.Substring(6,4) //这里用的是分解后的值
    | PassportNumber(num) -> 
        match num.StartsWith("G") with
        | true -> printfn "Paper Passport!"
        | _ -> printfn "E passport!"
    | UnknownNumber(num) -> printfn "Unknown Number: %s" num

但我们用刚才的普通函数实现,可以有:

let print' input = 
    match isIDNumber input with
    | true -> printfn "Birth Year: %s" input.Substring(6,4)  //这里用的是原值
    | _ -> 
        match isPassportNumber input with
        | true -> 
            match input.StartsWith("G") with
            | true -> printfn "Paper Passport!"
            | _ -> printfn "E passport!"
        | _ -> printfn "Unknown Number: %s" num

到目前为止看起来也还好,有区别,但不明显。现在我们稍作变化,还是一样的业务逻辑,但把最开始的活动模式修改成以下:

let (|IDBirthInfo|Passport|Unknown|) input = 
    match Regex(@"\d{18}").Match(input).Success with
    | true -> 
        let year = input.Substring(6,4)
        let month = input.Substring(10,2)
        let day = input.Substring(12,2)
        IDBirthInfo(year,month,day)  //分解输入数据并以年,月,日三元组的方式返回值
    | _ ->
        match Regex(@"G|E\d{8}").Match(input).Success with
        | true -> 
            let category = input.Substring(0,1)
            let number = input.Substring(1)
            Passport(category,number)  //分解输入数据并以护照类型,护照号码二元组的方式返回值
        | _ -> Unknown(input)

而基于新改的活动模式,同样的业务逻辑便能简化如下:

let print'' input = 
    match input with
    | IDBirthInfo(year,_,_) -> printfn "Birth Year: %s" year //这里用的是分解后的值
    | Passport("G",_) -> printfn "Paper Passport!" //这里用的是分解后的值进行模式匹配
    | Passport(_,_) ->  printfn "E Passport!"
    | Unknown(num) -> printfn "Unknown Number: %s" num

这时候的活动模式,就和普通函数长得相当不一样了。由于数据分解的方式不一,返回模式的形态也各异,如本例,它返回一个三元组模式,同时又返回一个二元组模式,同时再可以返回原值模式,为后续进行模式匹配提供了极大的灵活性。现实中其他的编程语言,尤其是面向对象的编程语言,相同的函数名需要通过重载才能实现不同的返回值类型(二元组和三元组是不同的数据类型,如C#中的TupleTuple),而活动模式这种返回值类型不确定的动态情况,在面向对象的编程语言里是罕见的,类似下面的C#代码应该是不能编译通过的:

public static ImpossibleType Parse (string input)
{
    if(isIDNumber(input))
        return new Tuple(input.Substring(6,4),input.Substring(10,2),input.Substring(12,2));
    
    if(isPassportNumber(input))
        return new Tuple(input.Substring(0,1),input.Substring(1));
    
    return input;
}

我们再回来看一眼官方语言指南中那句话:活动模式允许你通过定义命名分区 分割 输入数据,继而在模式匹配表达式中像可区分联合那样使用这些名字。现在应该就很好理解了,命名分区相当于告诉后面的模式匹配这个输入数据是什么(可以是业务人员也能看懂的领域术语),同时分割 提供远比函数更丰富的多样性,相当于告诉后面的应用逻辑这个输入数据可以被怎么利用。综上简言之,活动模式是F#语言的一个特性,它可用于按不同方式把数据分割成不同模式继而被后续模式匹配逻辑所利用,灵活且强大。

你可能感兴趣的:(活动模式小例(一))