如何在F#中转换列表和元组?

24
有没有一种方法可以在 F# List 和 F# Tuple 之间进行转换?
例如:
[1;2;3] -> (1,2,3)    
(1,2,3,4) -> [1;2;3;4]

我需要两个函数来完成这个任务:

let listToTuple list = ...
let tupleToList tuple = ...

提前感谢。


4
由于元组的大小通常是静态的,而列表的大小通常是动态的,因此这将是一个困难的问题。如果您确实需要这样的解决方案,我认为sepp2k的答案可能是最好的......为每个大小定义一个函数,直到您需要的最大列表大小,然后根据列表长度选择要调用的函数。 - Nels Beckman
5个回答

22
除了 listToTuple,pblasucci 给出的答案是正确的。 但是,如果你不了解涉及的类型或者想要进行大量的打包和解包操作,你可能不会对结果感到满意。
let tupleToList t = 
    if Microsoft.FSharp.Reflection.FSharpType.IsTuple(t.GetType()) 
        then Some (Microsoft.FSharp.Reflection.FSharpValue.GetTupleFields t |> Array.toList)
        else None

let listToTuple l =
    let l' = List.toArray l
    let types = l' |> Array.map (fun o -> o.GetType())
    let tupleType = Microsoft.FSharp.Reflection.FSharpType.MakeTupleType types
    Microsoft.FSharp.Reflection.FSharpValue.MakeTuple (l' , tupleType)

16
正如已经指出的那样,这是一个棘手的问题,因为元组不是单一类型 - 它是一族类型,例如 int * int * intint * int,F# 并没有提供任何以整个类型族作为参数的方法。你可以编写许多类似的函数(非常不便),或者使用反射(速度有点慢且不安全)。
或者,你可以将函数限制为具有某种结构的元组 - 例如,而不是处理 (1, 2, 3, 4),你可以使用嵌套的元组,如(1, (2, (3, 4)))。这有点不太方便,但它保持了类型安全性,而且并不糟糕。
然后,你可以轻松地编写合子来动态构建转换函数:
// creates function for converting tuple (possibly with a nested 
// tuple in the second component to list
let tl f (a, b) = a::(f b)
// converts last element of the tuple to singleton list
let te a = [a]

然后,您可以将函数tlte组合在一起,创建一个类型安全的函数,将包含4个元素的嵌套元组转换为列表,如下所示:

let l = (1, (2, (3, 4))) |> (tl (tl (tl te)))

同样地,你可以创建将列表转换为元组的函数 - 需要注意的是,如果列表与预期格式不符,这可能会引发异常:

let le = function
  | [x] -> x
  | _ -> failwith "incompatible"
let lt f = function
  | [] -> failwith "incompatible"
  | x::xs -> (x, f xs) 

// convert list to a tuple of four elements
let t = [1; 2; 3; 4] |> lt (lt (lt le))

我想这可能是将元组和列表之间进行类型安全且可重复使用的函数最接近的实现方式。它并不完美(完全不是),但这是因为你试图实现一个非常少用的操作。在 F# 中,元组和列表之间的区别比例如 Python(它是动态的,因此不必处理静态类型安全)更加清晰。


当然,元组不仅可以是(int * int * int),还可以是(int * float * string),这与列表的结合并不好。 - Benjol
我正在努力寻找以下语法的任何文档。请帮忙吗? let le = function | [x] -> x | _ -> failwith "incompatible" - Trident D'Gao
@TridentD'Gao 函数语法是 match 的一种语法糖。 - Istvan

4
实际上,您需要 2*n 个函数,其中 n 是您想要支持的最大元组大小。一个包含三个整数的元组与一个包含四个整数的元组具有完全不同的类型,因此您需要分别为每个元组编写一个 tupleToList 和 listToTuple 函数。
另请注意,对于 listToTuple,您需要在编译时知道要获取的元组大小。也就是说,您不能创建一个函数,根据输入列表的长度决定返回 (int, int, int) 还是 (int, int) (因为如我所说,它们是完全不同的类型)。您必须拥有一个函数 listToNTuple,该函数接受至少 N 个元素的列表并返回一个 N 元组。
通过使用反射可能可以为此编写大小独立的函数,但是由于您无法在编译时知道任何由这样的函数返回的元组的类型,因此使用起来会非常麻烦。

2
利用 PropertyInfo 结构可以递归地构建列表。这种方法的问题在于类型信息丢失,结果会以 obj 列表的形式生成。尽管如此,这确实解决了将元组转换为列表的部分问题。
let tupleToList tpl = 
    let rec loop tpl counter acc =
        let getItemPropertyInfo t n = t.GetType().GetProperty(sprintf "Item%d" n)
        let getItem t n = (getItemPropertyInfo t n).GetValue(t,null)
        match counter with
        | 8 -> 
            match tpl.GetType().GetProperty("Rest") with
            | null -> acc
            | _ as r ->
                let rest = r.GetValue(tpl,null)
                loop rest 2 ((getItem rest 1) :: acc)
        | _ as n -> 
            match getItemPropertyInfo tpl n with
            | null -> acc
            | _ as item -> loop tpl (counter+1) (item.GetValue(tpl,null) :: acc)
    loop tpl 1 [] |> List.rev

-1

好吧,它不太美观,但是:

let tuple_to_array input =
    let temp_str = input.ToString()
    temp_str.Substring(1, temp_str.Length - 2)
    |> Array.map (fun (x:string) -> x.TrimStart(' '))

我不确定如何反向操作。而且我真的不确定这是否明智,但如果你真的必须这样做。


1
嗯,这似乎只适用于字符串可序列化的元组容器。我不确定我喜欢这个解决方案。 - Daniel
是的,这个方法非常“hacky”,但我只有五分钟来证明这个概念。Huusom的解决方案好了无数倍。 - Massif

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接