嵌套元组的优雅模式匹配任意长度

时间:2016-08-11 21:58:49

标签: f# pattern-matching tuples

我正在用F#开发一个可组合的功能UI库,我遇到了一种情况,我需要能够创建异类型项目的“集合”。我不想通过动态编程并将所有内容都转换为obj来实现这一点(在技术上可行,尤其是因为我正在使用Fable进行编译)。相反,我希望保留尽可能多的类型安全。

我想出的解决方案是创建一个简单的自定义运算符%%%来构建元组,然后按如下方式使用它:

let x = 4 %%% "string" %%% () %%% 2.4

这会生成以下类型的值:

val x: (((int * string) * unit) * float)

结果类型看起来有点乱(特别是随着值的数量增加),但它确保了我的场景的强类型安全性,并且(理想情况下)对于库的用户有些隐藏。

但是我试图找出一种与这些嵌套元组类型进行模式匹配的优雅方法,因为库用户有时需要在这些值上编写函数。显然,这可以手动完成,如

match x with
| (((a,b),c),d) -> ...

并且编译器推断出abcd的正确类型。但是,我不希望用户不必担心所有嵌套。我希望能够做点什么,

match x with
| a %%% b %%% c %%% d -> ...

并让编译器只是想出一切。有没有办法用F#使用活动模式(或其他一些功能)来完成类似的事情?

编辑:

我应该澄清一点,我不是想在运行时匹配未知“arity”的元组值。我只想在编译时知道元素的数量(和类型)时这样做。如果我正在做前者,我会采用动态方法。

目前,我已经创建了活动模式:

let (|Tuple2|) = function | (a,b)-> (a,b)
let (|Tuple3|) = function | ((a,b),c) -> (a,b,c)
let (|Tuple4|) = function | (((a,b),c),d) -> (a,b,c,d)
...

可以这样使用:

let x = 4 %%% "string" %%% () %%% 2.4
let y = match x with | Tuple4 (a,b,c,d) -> ...

这可能是最好的,对用户来说真的不是那么糟糕(只需要计算元组的“arity”然后使用正确的TupleN模式)。然而它仍然让我感到困惑,因为它看起来并不像它那样优雅。在创建x时,您不必指定元素的数量,为什么在匹配时必须这样做?似乎对我不对称,但我没有办法避免它。

为什么我的原创想法不适用于F#(或一般的静态类型语言)有更深层次的原因吗?是否有可能的功能语言?

2 个答案:

答案 0 :(得分:9)

看起来你正试图建立某种语义模型,虽然我并不完全清楚它是什么。

正如John Palmer所暗示的那样,通常在静态类型函数式编程语言中进行的方法是定义一种类型来保存模型的异构值。在这种情况下,它可能是这样的:

type Model =
| Integer of int
| Text of string
| Nothing
| Float of float

(对于模糊命名的道歉,但如上所述,我并不清楚你究竟想要建模什么。)

您现在可以构建此类型的值:

let x = [Integer 4; Text "string"; Nothing; Float 2.4]

在这种情况下,x的类型为Model list。您现在拥有一个可以轻松模式匹配的数据类型:

match x with
| [Integer i; Text s; Nothing; Float f] -> ...

如果你能提出比我在这里选择的名字更好的名字,这甚至可以使API变得有用和直观。

答案 1 :(得分:7)

Mark Seeman给了你正确的答案。相反,我会做一些完全不同的事情,并告诉你为什么你尝试用复杂元组做的事情实际上不会起作用,即使你尝试过"硬编码每种可能数量的项目的模式"你不喜欢的方法。以下是实施您的想法的一些尝试,赢得工作:

尝试#1

首先,让我们尝试编写一个函数,它将递归地丢弃元组的所有尾部元素,直到它向下到第一对,然后返回该对。换句话说,像List.take 2之类的东西。如果这样做,我们可以应用类似的技术来提取复杂元组的其他部分。但这不会起作用,其原因非常有启发性。这是功能:

let rec decompose tuple =
    match tuple with
    | ((a,b),c) -> decompose (a,b)
    | (a,b) -> (a,b)

如果我将该功能输入到一个好的F#IDE中(我使用带有Ionide插件的VS Code),我会在递归a调用中的decompose (a,b)下看到一个红色的波浪形。那是因为编译器在此时抛出了以下错误:

Type mismatch. Expecting a
    'a * 'b
but given a
    'a
The resulting type would be infinite when unifying ''a' and ''a * 'b'

这是为什么这不起作用的第一个线索。当我将鼠标悬停在VS Code中的tuple参数上时,Ionide会向我显示F#推断tuple的类型:

val tuple : ('a * 'b) * 'b
等等,什么?为什么'b用于组成元组的最后部分?不应该是('a * 'b) * 'c吗?好吧,那是因为以下匹配线:

| ((a,b),c) -> decompose (a,b)

这里我们说tuple参数及其类型必须是一个可以匹配此行的形状。因此,tuple必须是2元组,因为我们在此特定调用中将2元组作为参数传递给decompose。因此,该2元组的第二部分必须与b的类型匹配,否则以decompose作为参数调用(a,b)会出现类型错误。因此,模式中的c(2元组的第二部分)和模式中的b("内部" 2元组的第二部分)必须具有相同的类型,以及decompose的类型被限制为('a * 'b) * 'b而不是('a * 'b) * 'c的原因。

如果这有意义,那么我们可以继续讨论为什么会发生类型不匹配错误。因为现在,我们需要匹配递归a调用的decompose (a,b)部分。由于我们传递给decompose 的元组必须匹配其类型签名,这意味着a必须匹配2元组的第一部分,并且我们已经知道(因为tuple参数必须能够匹配((a,b),c)语句中的match模式,否则该语句不会编译)2元组的第一部分本身是另一个2 -tuple,类型'a * 'b。正确?

嗯,这就是问题所在。我们知道decompose参数的第一部分必须是类型为'a * 'b的2元组。但是匹配模式还将参数a限制为'a类型,因为我们会将类型为('a * 'b) * 'b的内容与((a,b),c)进行匹配。因此,该行的一部分强制a具有类型'a,而另一部分强制它具有类型('a * 'b)。这两种类型无法协调,因此类型系统会抛出编译错误。

尝试#2

但等等!活动模式怎么样?也许他们可以拯救我们?好吧,让我们来看看我尝试过的另一件事,我认为这样可行。当它失败时,它实际上教会了我更多关于F#的类型系统,以及为什么你想要的东西是不可能的。我们马上谈论原因;但首先,这里是代码:

let (|Tuple2|_|) t =
    match t with
    | (a,b) -> Some (a,b)
    | _ -> None

let (|Tuple3|_|) t =
    match t with
    | ((a,b),c) -> Some (a,b,c)
    | _ -> None

let (|Tuple4|_|) t =
    match t with
    | (((a,b),c),d) -> Some (a,b,c,d)
    | _ -> None

let (|Tuple5|_|) t =
    match t with
    | ((((a,b),c),d),e) -> Some (a,b,c,d,e)
    | _ -> None

在IDE中键入它,您将看到一个令人鼓舞的迹象。它汇编!如果您将鼠标悬停在每个活动模式中的t参数上,您会看到F#已经确定了正确的#34;形状"每个t。那么现在,我们应该可以做这样的事情,对吗?

let (%%%) a b = (a,b)

let complicated = 5 %%% "foo" %%% true %%% [1;2;3]

let result =
    match complicated with
    | Tuple5 (a,b,c,d,e) -> sprintf "5-tuple of (%A,%A,%A,%A,%A)" a b c d e
    | Tuple4 (a,b,c,d) -> sprintf "4-tuple of (%A,%A,%A,%A)" a b c d
    | Tuple3 (a,b,c) -> sprintf "3-tuple of (%A,%A,%A)" a b c
    | Tuple2 (a,b) -> sprintf "2-tuple of (%A,%A)" a b
    | _ -> "Not matched"

(注意顺序:因为所有复杂元组都是2元组,复杂元组作为2元组的第一部分,Tuple2模式将匹配任何这样的元组(如果它是第一个)。)

这似乎很有希望,但它也不会起作用。键入(或粘贴)到您的IDE中,您将在Tuple5 (a,b,c,d,e)模式(match语句的第一个模式)下看到红色波浪形。我会在一分钟内告诉您错误是什么,但首先,让我们将鼠标悬停在complicated的定义上并确保其正确无误:

val complicated : ((int * string) * bool) * int list

是的,这看起来是正确的。因此,因为它可能与Tuple5活动模式匹配,为什么该活动模式不会返回None并让您转到Tuple4模式( 工作)?好吧,让我们看一下错误:

Type mismatch. Expecting a
    ((int * string) * bool) * int list -> 'a option
but given a
    ((('b * 'c) * 'd) * 'e) * 'f -> ('b * 'c * 'd * 'e * 'f) option
The type 'int' does not match the type ''a * 'b'

两种不匹配类型中的任何一种都没有'a'a来自哪里?好吧,如果您专门将鼠标悬停在该行中的Tuple5字词上,则会看到Tuple5的类型签名:

active recognizer Tuple5: ((('a * 'b) * 'c) * 'd) * 'e -> ('a * 'b * 'c * 'd * 'e) option

'a来自的地方。但更重要的是,错误消息告诉您complicated的第一部分int无法与2元组匹配。为什么会这样做呢?同样,因为 match表达式必须匹配他们匹配的内容的类型,因此他们会约束该类型。正如我们在decompose函数中看到的那样,它也在这里发生。您可以通过将let result变量更改为函数来更好地查看它,如下所示:

let showArity t =
    match t with
    | Tuple5 (a,b,c,d,e) -> sprintf "5-tuple of (%A,%A,%A,%A,%A)" a b c d e
    | Tuple4 (a,b,c,d) -> sprintf "4-tuple of (%A,%A,%A,%A)" a b c d
    | Tuple3 (a,b,c) -> sprintf "3-tuple of (%A,%A,%A)" a b c
    | Tuple2 (a,b) -> sprintf "2-tuple of (%A,%A)" a b
    | _ -> "Not matched"

showArity complicated

showArity函数现在编译没有错误;您可能会喜欢高兴,但是您会发现它无法使用我们之前定义的complicated值进行调用,并且您会遇到相同的类型不匹配错误(最终,{ {1}}无法匹配int)。但为什么'a * 'b编译没有错误?好吧,将鼠标悬停在其showArity参数的类型上:

t

所以val t : ((('a * 'b) * 'c) * 'd) * 'e 被限制为我称之为"复杂的5元组" (通过第一个t模式(实际上仍然只是一个2元组,请记住)。其他Tuple5Tuple4Tuple3模式将匹配,因为它们实际上匹配2元组。为了表明这一点,请从Tuple2函数中删除Tuple5行,并在F#Interactive中运行showArity时查看其结果(您必须重新运行F#Interactive的定义) showArity complicated也是如此)。你会得到:

showArity

看起来不错,但等待:现在删除"4-tuple of (5,"foo",true,[1; 2; 3])" 行,然后重新运行Tuple4的定义以及showArity行。这一次,它产生:

showArity complicated

看看它是如何匹配的,但并没有分解最内层的"元组("3-tuple of ((5, "foo"),true,[1; 2; 3])" )?这就是为什么你需要订购正确的原因。再次运行它,只剩下int * string行,您将获得:

Tuple2

所以这种方法也不会起作用:你实际上无法确定"假的arity"一个复杂的元组。 ("假的arity"在恐慌引用中,因为所有这些元组的arity实际上是2,但我们正试图将它们视为3或4或5元组)。因为任何模式的"假arity"小于你处理它的复杂元组仍将匹配,但它不会分解复杂元组的某些部分。虽然任何模式的"假arity"是更大比你正在处理它的复杂元组更好>,因为它会在最内层之间创建一个类型不匹配你和你匹配的元组。

我希望通过阅读这些内容可以让您更好地理解F#型系统的复杂性;我知道写它确实教会了我很多。