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

fun*_*bda 11 f# tuples pattern-matching

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

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

let x = 4 %%% "string" %%% () %%% 2.4
Run Code Online (Sandbox Code Playgroud)

这将生成以下类型的值:

val x: (((int * string) * unit) * float)
Run Code Online (Sandbox Code Playgroud)

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

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

match x with
| (((a,b),c),d) -> ...
Run Code Online (Sandbox Code Playgroud)

和编译器推断正确的类型a,b,c,和d.但是,我不希望用户不必担心所有嵌套.我很乐意能做点什么,

match x with
| a %%% b %%% c %%% d -> ...
Run Code Online (Sandbox Code Playgroud)

并让编译器只是想出一切.有没有办法用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)
...
Run Code Online (Sandbox Code Playgroud)

哪个可以这样使用:

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

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

我的原创想法在F#(或一般的静态类型语言)中不起作用是否有更深层次的原因?是否有可能的功能语言?

Mar*_*ann 9

看起来你正在尝试构建某种语义模型,尽管我并不完全清楚它到底是什么.

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

type Model =
| Integer of int
| Text of string
| Nothing
| Float of float
Run Code Online (Sandbox Code Playgroud)

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

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

let x = [Integer 4; Text "string"; Nothing; Float 2.4]
Run Code Online (Sandbox Code Playgroud)

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

match x with
| [Integer i; Text s; Nothing; Float f] -> ...
Run Code Online (Sandbox Code Playgroud)

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


rmu*_*unn 7

马克·西曼给了你正确的答案.我宁愿做一些完全不同的事情,并告诉你为什么你试图用复杂元组做的事情实际上不会起作用,即使你尝试"每个可能的项目的硬编码模式"的方法你不喜欢.以下是一些实现您的想法的尝试,这些尝试无效:

尝试#1

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

let rec decompose tuple =
    match tuple with
    | ((a,b),c) -> decompose (a,b)
    | (a,b) -> (a,b)
Run Code Online (Sandbox Code Playgroud)

如果我将该函数输入到一个好的F#IDE中(我使用带有Ionide插件的VS代码),我会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'
Run Code Online (Sandbox Code Playgroud)

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

val tuple : ('a * 'b) * 'b
Run Code Online (Sandbox Code Playgroud)

等等,什么?为什么一个'b组成元组的最后部分?不应该('a * 'b) * 'c吗?那么,这是因为以下匹配线:

| ((a,b),c) -> decompose (a,b)
Run Code Online (Sandbox Code Playgroud)

这里我们说的是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元组'a * 'b.对?

嗯,这就是问题所在.我们知道decompose参数的第一部分必须是2元组的类型'a * 'b.但是匹配模式也将参数约束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
Run Code Online (Sandbox Code Playgroud)

在IDE中键入它,您将看到一个令人鼓舞的迹象.它汇编!如果将鼠标悬停t在每个活动模式中的参数上,您将看到F#已确定每个模式中的正确"形状" 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"
Run Code Online (Sandbox Code Playgroud)

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

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

val complicated : ((int * string) * bool) * int list
Run Code Online (Sandbox Code Playgroud)

是的,这看起来是正确的.因此,由于这不可能与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'
Run Code Online (Sandbox Code Playgroud)

有没有'a在任何两个不匹配的类型.它'a来自哪里?好吧,如果你专门将鼠标悬停Tuple5在该行中的单词上,你会看到Tuple5类型签名:

active recognizer Tuple5: ((('a * 'b) * 'c) * 'd) * 'e -> ('a * 'b * 'c * 'd * 'e) option
Run Code Online (Sandbox Code Playgroud)

这就是它的'a来源.但更重要的是,错误消息告诉您complicated,an 的第一部分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
Run Code Online (Sandbox Code Playgroud)

showArity函数现在编译没有错误; 你可能会喜欢高兴,但你会发现它不能用complicated我们之前定义的值调用,并且你得到相同的类型不匹配错误(最终,哪里int不匹配'a * 'b).但为什么showArity编译没有错误?好吧,将鼠标悬停在其t参数的类型上:

val t : ((('a * 'b) * 'c) * 'd) * 'e
Run Code Online (Sandbox Code Playgroud)

所以t被第一个模式限制为我称之为"复杂的5元组"(它实际上只是一个2元组,记得)Tuple5.而另外Tuple4,Tuple3Tuple2模式匹配,因为他们实际上是匹配在现实中2元组.要显示这一点,请在F#Interactive中运行时Tuple5showArity函数中删除该行并查看其结果showArity complicated(您还必须重新运行该定义showArity).你会得到:

"4-tuple of (5,"foo",true,[1; 2; 3])"
Run Code Online (Sandbox Code Playgroud)

看起来不错,但等待:现在删除Tuple4线,并重新运行的定义showArity,以及该showArity complicated行.这一次,它产生:

"3-tuple of ((5, "foo"),true,[1; 2; 3])"
Run Code Online (Sandbox Code Playgroud)

看看它是如何匹配的,但没有分解"最里面"的元组(of int * string)?这就是为什么你需要正确的订购.只剩下Tuple2剩下的一行再运行一次,你会得到:

"2-tuple of (((5, "foo"), true),[1; 2; 3])"
Run Code Online (Sandbox Code Playgroud)

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

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