架构算法设计模式和编程理论函数式编程 FP Functional Progamming · Lambda 演算 · Y 组合子F#

活动模式小结

2021-02-08  本文已影响0人  顾远山

Recap Active Pattern in F#

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

笔者早前针对F#中活动模式的应用列举了五个小例,分别为活动模式的简介(小例一)、以及把活动模式应用到各类数据的解析场景,包括日期(小例二)、指令(小例三)、网页(小例四)和结构化数据(小例五)等。然而很多时候知其然是不够的,为了知其所以然,笔者对活动模式进行了简单的小结,分为三个部分:活动模式的由来,活动模式的分类,以及活动模式的本质,以便大家能深刻理解活动模式并灵活自如地把它应用到项目实践中。

活动模式的由来

模式匹配对于函数式编程语言来说是标配,参考F#语言指南:模式匹配我们可以发现,F#中的模式匹配只支持以下十七种模式:

模式匹配

众所周知,以上有限的模式匹配与抽象的交互很差,所以F#的设计者们决定引入活动模式,以支持对通用异构数据的抽象表示进行模式匹配。活动模式在设计上结合了完全分解数据的视图功能和部分分解数据的其他匹配功能,在实现上则是基于F#语言一个简单轻巧的语法扩展,其官方基本语法如下:

// Active pattern of one choice.
let (|identifier|) [arguments] valueToMatch = expression

// Active Pattern with multiple choices.
// Uses a FSharp.Core.Choice<_,...,_> based on the number of case names. 
// In F#, the limitation n <= 7 applies.
let (|identifer1|identifier2|...|) valueToMatch = expression

// Partial active pattern definition.
// Uses a FSharp.Core.option<_> to represent if the type is satisfied at the call site.
let (|identifier|_|) [arguments ] valueToMatch = expression

活动模式的分类

上文提到活动模式适用三种语法,但从实践角度我们习惯把它分为四类:

现针对其语法及适用场景逐个分述。

单例完全模式 (Single-case Total Pattern)

单例完全模式语法为:let (|ActivePatternName|) input = ...

活动模式需要被(||)进行定义,(||)又俗称"香蕉夹",其中(||)之间的字面量则是活动模式的名称。因为是单例完全,所以香蕉夹之间只有一个字面量。如下例:

type Account = {Phone:string; Password:string; Name:string; Address:string; Email:string}
let (|LoginCredentials|) user = (user.Phone,user.Password)

LoginCredentials就是活动模式的名字,其目的是从Account类型的记录中抽取PhonePassword字段组成二元组并返回。

单例完全模式适合“视图”功能的场景,当需要从已有类型中抽取特定的字段进行组合及计算时,可使用单例完全模式。再如下段代码中的Area活动模式:

type Shape = 
    | Circle of Radius:float
    | Rectangle of Width:float * Height:float    
let (|Area|) shape = 
    match shape with
    | Circle radius -> radius * radius * 3.14
    | Rectangle (width,height) -> width * height

多例完全模式 (Multiple-case Total Pattern)

多例完全模式的语法为:let (|APN1|APN2|APN3|...|APNm|) input = ...,其中字面量APN1...APNm是所有活动模式的名称。由于是多例完全,所以香蕉夹里有m个字面量。如以前举过的例子:

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

上述活动模式对数据进行判断及分类:

多例完全模式适合条件判断的场景,当某个输入要么是甲要么是乙要么是丙要么是丁时,可使用多例完全模式。再如以下代码中的``活动模式:

let (|ADD|SUB|MUL|DIV|REM|) input = 
    let elements = Regex(@"(\d+)\s*([\+\-\*\/\%])\s*(\d+)").Match(input).Groups
    let operand1 = elements.[1].Value
    let operator = elements.[2].Value
    let operand2 = elements.[3].Value    
    match operator with
    | "+" -> ADD(operand1,operand2)
    | "-" -> SUB(operand1,operand2)
    | "*" -> MUL(operand1,operand2)
    | "/" -> DIV(operand1,operand2)
    | "%" -> REM(operand1,operand2)

上述活动模式把诸如"12 + 5""100 % 3"之类的字符串通过正则表达式转换成抽象语法树片段。

单例部分模式 (Single-case Partial Pattern)

单例部分模式的语法为:let (|ActivePatternName|_|) input = ...

正如香蕉夹中的内容所示,单例部分模式由两种(且只有两种)模式组合而成,其中第二种模式总是为通配符,实际上单例部分模式可以理解为多例完全模式在二维上的一个特例。

比如常见的整型解析,如下:

let (|ParseInt|_|) input = 
    match Int32.TryParse(input.ToString()) with
    | true, i -> Some (i)
    | _ -> None

单例部分模式非常适合处理Option类型值的场景,可提高函数式代码的纯粹性,是替代传统try...with异常处理的良好实践。

从活动模式设计者发表的论文易知,他们没有发掘到多例部分模式的好处,所以F#并不支持多例部分模式。

参数部分模式 (Parameterized Partial Pattern)

参数部分模式的语法为:let (|ActivePatternName|_|) param1 ... paramN input = ...

参数部分模式是单例部分模式的扩展,在活动模式名称和输入之间,可定义一到多个参数,其中param1 ... paramN是参数的名称。如在小例中应用多次的正则表达式匹配活动模式:

let (|RegexMatch|_|) pattern input =
      match input with
      | null -> None
      | _    -> let m = Regex.Match(input, pattern)
                match m.Success with
                | false -> None
                | _     -> Some ([for x in m.Groups -> x.Value] |> List.tail)

上述活动模式通过pattern这个参数对输入字符串input进行正则表达式匹配:

参数部分模式为实际的项目应用提供了极大的灵活性,但活动模式的参数化会导致模式匹配同一性的损失,因此编译器无法对其执行冗余或完整性分析,所以在同一个match块内,就算每个模式在语法上都有相同的参数,它们出现的时候仍需要被重新求值。比如之前演示过的日期解析器代码:

let parseDate dstr = 
      let p1 = @"^(\d{4}|\d{2})([/\-\.])(\d{1,2})\2(\d{1,2})$"
      let p2 = @"^(\d{1,2})([/\-\.])(.+?)\2(\d{4}|\d{2})$"
      let p3 = @"^(.+?)\s(\d{1,2})\,\s*(\d{4}|\d{2})$"
      let p4 = @"^(.+?)\s(\d{1,2}).{2}\,\s*(\d{4}|\d{2})$"
      let p5 = @"^(\d{4}|\d{2})年(\d{1,2})月(\d{1,2})日$"
      match dstr with
      | RegexMatch p1 [_;Year y;_;Month m;Day d] 
      | RegexMatch p2 [_;Day d;_;Month m;Year y]
      | RegexMatch p3 [_;Month m;Day d;Year y]
      | RegexMatch p4 [_;Month m;Day d;Year y]
      | RegexMatch p5 [_;Year y;Month m;Day d]
          -> Some {Year=y; Month=m; Day=d}
      | _ -> None

虽然match块内只有五个形式一致的RegexMatch p [...]模式,但由于参数化的缘故,每一个模式都会被求值,另外,最后也需要手动添加通配符模式以保证匹配的完整性。

活动模式的本质

无论是单例完全模式、多例完全模式、单例部分模式还是参数部分模式,严格意义上来说,都是针对F#模式匹配的语言扩展,也就是语法糖,究其本质,所有的活动模式都是函数,只是类型有异,如下:

活动模式

在函数式编程语言中,函数是头等公民。活动模式在F#中本质就是函数,所以自然也属于头等公民。既然活动模式是头等公民,那它就可以被当成“值”用于“值”可用的地方。略举二例如下:

结语

参考资料

Extensible Pattern Matching Via a Lightweight Language Extension
(Nearly) Everything You Ever Wanted to Know About F# Active Patterns
F#语言指南:活动模式
活动模式小例(一)
活动模式小例(二)
活动模式小例(三)
活动模式小例(四)
活动模式小例(五)

上一篇 下一篇

猜你喜欢

热点阅读