F#:如何在不牺牲运行时性能的情况下为元组、三元组和四元组提供 fst

F# : How to provide fst for tuples, triples and quadruples without sacrifying runtime performance

对于具有 2 个元素的基本元组,我们有 fst 和 snd:

let t2 = (2, 3)
fst t2 // = 2
snd t2 // = 3

简单。现在有 3 个元素

let t3 = (2, 3, 4)

我们如何访问第三个元素? msdn 有答案(http://msdn.microsoft.com/en-us/library/dd233200.aspx):

let third (_, _, c) = c

还是很简单。但实际上是骗人的,因为我们不能在这样的三元组上使用 fst 和 snd:

fst t3

error FS0001: Type mismatch. Expecting a int * int but given a int * int * int    
The tuples have differing lengths of 2 and 3

因此有挑战性的问题:我们如何在不牺牲运行时性能的情况下为元组、三元组和四元组提供函数 fst

无效的方法:

A) 只需添加 fst(,,_)

let fst (a,_,_) = a // hides fst (a,b), so tuples dont work anymore

B) 任何反射技巧,因为性能会受到影响或复杂的函数组合会导致在 jitted 代码级别进行额外的调用。

C) 扩展方法(没有带我到任何地方,也许其他人有更好的想法)

type Tuple<'T1, 'T2, 'T3> with
    member o.fst(a,b,c) = a
    static member fst(a,b,c) = a

除了具有其他签名 (t3.fst()) 之外,我什至无法获得使用元组的扩展,尽管它们是 类(不是结构)。

D)

let fst = function 
          | (a,b) -> a
          | (a,b,c) -> a // FS0001: Type mismatch

这个问题是按原样提出的,不是为了解决问题。实际上我认为答案是不可能提供对元组进行操作的通用函数。

这是一个很酷的问题。 我认为问题在于 F# 文档暗示它编译为 Tuple<...> 对象之一。所以你必须问问自己,类型 Tuple<T1, T2>Tuple<T1, T2, T3> 之间的关系是什么?答:除了名字什么都没有。完全没有关系。

所以您可以使用上面的库,或者您可以尝试自己创建包含关系的库。 可能有更好的方法来做到这一点,但这是直截了当的:

type XTuple1<'a>(a: 'a) =
    member public this.fst = a
    abstract member strContents: unit->string
    default this.strContents() = this.fst.ToString()
    override this.ToString() = "(" + this.strContents() + ")"
type XTuple2<'a, 'b>(a:'a, b:'b) =
    inherit XTuple1<'a>(a)
    member public this.snd = b
    override this.strContents() = base.strContents() + ", " + b.ToString()
type XTuple3<'a, 'b, 'c>(a:'a, b:'b, c:'c) =
    inherit XTuple2<'a, 'b>(a, b)
    member public this.thrd = c
    override this.strContents() = base.strContents() + ", " + c.ToString()

let inline xfst<'a> (xt:XTuple1<'a>) = xt.fst
let inline xsnd<'a,'b> (xt:XTuple2<'a,'b>) = xt.snd
let inline xthrd<'a,'b,'c> (xt:XTuple3<'a,'b,'c>) = xt.thrd

[<EntryPoint>]
let main argv = 
    let t3 = XTuple3(1, 'a', "word")
    let a = xfst t3 // strongly typed: a is an int
    let b = xsnd t3
    let c = xthrd t3
    printfn "%s" (t3.ToString()) // -> "(1, a, word)"
    0 // return an integer exit code

正如其他人已经提到的那样,有(或多或少复杂和丑陋的)方法可以做到这一点,但我认为这里应该说的重要一点是,如果你很好地使用 F#,你 不需要这个.

如果您需要一个可以包含两个、三个或四个元素的数据结构,那么您应该使用列表而不是使用元组(在其他语言中,元组有时用于此,但在 F# 中不使用)。

如果您在 F# 中使用元组,那么您通常会使用少量元素(两个或三个),然后使用模式匹配分解它们更具可读性。比较以下内容:

// Pattern matching makes you name things, so this is quite clear
// (we don't need the third element, so we're ignoring it)
let width, height, _ = tuple
width * height

// If you had fst3 and snd3, then you could write it using just
// a single line, but the code becomes hard to follow
(fst3 tuple) * (snd3 tuple)

所以,我认为您根本不应该这样做。现在,如果您使用更复杂的数据结构(具有更多元素),那么最好使用记录之类的东西。如果您的第一个元素始终是 "name" 并且您希望具有访问名称的函数,则可以使用接口(或可区分的联合和模式匹配)。

type INamed = 
  abstract Name : string

type Person = 
  { SomeName : string }
  interface INamed with
    member this.Name = this.SomeName

type FancyPerson = 
  { SomeName : string; Title : string }
  interface INamed with
    member this.Name = this.SomeName

正确的方法取决于您在做什么。但无论如何,很可能有更好的方式来表达您的需求。

使用 this technique 方法重载结合内联函数在运行时完全没有损失,因为重载解析发生在编译时并且 overloaded 函数是在调用站点内联。

在库中有更复杂的方法来组织这些通用元组函数,@MauricioScheffer 已经发布了一些链接,显示了使用基本相同技术的更多实验。

这是一个非常短的独立代码片段,用于 重载 函数 fst 以使其与其他元组大小一起工作:

type Fst = Fst with
    static member ($) (Fst, (x1,_)) = x1
    static member ($) (Fst, (x1,_,_)) = x1
    static member ($) (Fst, (x1,_,_,_)) = x1
    // more overloads

let inline fst x = Fst $ x