活动模式小结

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

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

  • 若输入字符串是18位数字,理解为身份证号码,返回IDNumber模式
  • 若输入字符串以GE开头,后接8位数字,理解为护照号码,返回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进行正则表达式匹配:

  • 若输入字符串为空,返回Option类型值None
  • 若输入字符串不为空,且不能被正则表达式通过pattern进行匹配,返回Option类型值None
  • 若输入字符串不为空,且能被正则表达式通过pattern进行匹配,返回所有匹配组的值列表

参数部分模式为实际的项目应用提供了极大的灵活性,但活动模式的参数化会导致模式匹配同一性的损失,因此编译器无法对其执行冗余或完整性分析,所以在同一个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#中本质就是函数,所以自然也属于头等公民。既然活动模式是头等公民,那它就可以被当成“值”用于“值”可用的地方。略举二例如下:

  • 活动模式作为函数的参数

    复用单例完全模式中的LoginCredentials活动模式,易得login函数:

    let login (LoginCredentials(phone,password)) = ...
    

    该函数的签名为Account -> unit ,它接受一个Account类型的参数,但此参数在函数定义时就被活动模式LoginCredentials分解为phonepassword两个值,函数体内可直接使用这两个值,而Account类型的参数中其他字段值则被函数忽略。此例再次演示了单例完全模式常被用作数据筛选的“视图”。

  • 活动模式作为值参与运算

    当活动模式在模式匹配中被用于条件判断时,可以带上输出作为值进行组合运算,如下例:

    let (|DividedBy|) d y = y % d = 0
    let isLeapYear year = 
        match year with
        | DividedBy 400 true -> true
        | DividedBy 4 true & DividedBy 100 false -> true
        | _ -> false
    

    其中DividedBy 4 trueDividedBy 100 false两个模式进行了与运算,而模式匹配本身就是或运算。

结语

  • 活动模式是F#中针对通用异构数据的抽象表示基于模式匹配的语言扩展;

  • 活动模式可分为单例完全模式、多例完全模式、单例部分模式和参数部分模式四种类型;

  • 活动模式是F#中的头等公民,其本质是函数,可作为参数传入高阶函数,亦可带输出作为值进行组合运算。

参考资料

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容