Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

tuples

inline

f#

For basic Tuples with 2 elements we have fst and snd:

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

Easy. Now with 3 elements

let t3 = (2, 3, 4)

how do we access the 3rd element? msdn has an answer (http://msdn.microsoft.com/en-us/library/dd233200.aspx):

let third (_, _, c) = c

Still easy. But actually deceptive, because we cannot use fst and snd on such triples:

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

Hence the challenging question: How can we provide function fst for tuples, triples and quadruples without sacrifying runtime performance?

Approaches that do not work:

A) simply adding fst(,,_)

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

B) Any Reflection tricks, since performance is affected or tricky function composition that leads to additional calls at the jitted code level.

C) extension methods (did not lead me anywhere, maybe someone else has better ideas)

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

besides having other signature (t3.fst()) I could not even get extensions to work with Tuples, although they are classes (not structs).

D)

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

The question is meant as it is formulated, not for workarounds. Actually I believe the answer is that it is not possible to provide common functions that operate on Tuples.

like image 933
citykid Avatar asked Dec 05 '22 23:12

citykid


2 Answers

Using this technique of method overload combined with an inline function has no penalty at all at runtime, since the overload resolution occurs at compile time and the overloaded function is inlined at the call site.

There are more complex ways to organise these kind of generic tuple functions in libraries, @MauricioScheffer already posted some links showing more experiments making use of basically the same technique.

Here's a very short standalone code fragment to overload the function fst to make it work with other tuple sizes:

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
like image 109
Gus Avatar answered Jan 21 '23 01:01

Gus


As others already mentioned, there are (more or less elaborate and ugly) ways to do this, but I think the important thing that should be said here is that, if you're using F# well, you don't need this.

If you need a data structure that can have two, three or four elements, then you should probably use a list instead of using tuples (in other languages, tuples are sometimes used for this, but not in F#).

If you're working with tuples in F#, then you'll be typically working with a small number of elements (two or three) and then it is more readable to decompose them using pattern matching. Compare the following:

// 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)

So, I think you simply shouldn't be doing this. Now, if you were using more complex data structures (with larger number of elements), then it's better to use something like a record. If your first element is always a "name" and you want to have a function for accessing the name, you can use interfaces (or discriminated unions and pattern matching).

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

The right approach depends on what you're doing. But in any case, there is most likely a better way of expressing what you need.

like image 34
Tomas Petricek Avatar answered Jan 21 '23 00:01

Tomas Petricek