2

I want to create a type safe recursive function for flattening out tuples. However I can not get below the first recursion level in terms of type safety

type Flatten = Flatten
with
    static member inline ($) (Flatten, (a: 'a, b: 'b)) : 'x list = 
        List.concat [ Flatten.Flat a; Flatten.Flat b]
    static member inline($) (Flatten, (a: 'a, b: 'b, c: 'c))  : 'x list = 
        List.concat [Flatten.Flat a; Flatten.Flat b; Flatten.Flat c]
    static member inline Flat(x: obj) : 'x list = 
        match x with
        | :? Tuple<'a, 'b> as t -> Flatten $ (t.Item1, t.Item2)
        | :? Tuple<'a, 'b, 'c> as t ->Flatten $ (t.Item1, t.Item2, t.Item3)
        | _ -> [x]
let inline flatten x  = Flatten $ x
let a1 = flatten (1, (2, 2, 3), (3,3))
//this compiles 
let a2 = flatten (1, (2, 2, 3, 4), (3,3))
//                             ^ but this too

I tried another approach

type Flatten = Flatten
with
    static member inline ($) (Flatten, (a: 'a, b: 'b)) = List.concat [ Flat $ a; Flat $ b]
    static member inline ($) (Flatten, (a: 'a, b: 'b, c: 'c)) = List.concat [Flat $ a; Flat $ b; Flat $ c]

and Flat = Flat
with
    static member inline ($) (Flat, a: 'a) = [a]
    static member inline ($) (Flat, x: ('a *'b)) = 
        let (a, b) = x
        List.concat [ Flatten $ a; Flatten $ b]
    static member inline($) (Flat, x : ('a * 'b * 'c)) = 
        let (a, b, c) = x
        List.concat [Flatten $ a; Flatten $ b; Flatten $ c]

let inline flatten x  = Flatten $ x
let a = flatten (1, 1)
let a1 = flatten (1, 1, 3)
let a2 = flatten (1, 1, (3, 3))

but I cant get that one to type check.

Does anybody have a clue?

One Additional Requirement

The reason I am doing all of this is partly because I want

let a1 = flatten (1, (2, 2, 3), (3,3))

to yield

val a1 : int list

That is because when I feed in a tuple of tuple of int then the only sensible result should be a int list. at the moment I get an obj list int the first example a compile error in the second.

Best regards

robkuz
  • 9,488
  • 5
  • 29
  • 50
  • 1
    For sure you can't for all tuple sizes. At some point you have to specify as many overloads as possible tuple sizes. – Gus May 12 '17 at 09:57
  • 1
    I'd just forfeit static typing guarantees and use reflection. – scrwtp May 12 '17 at 10:22
  • 1
    @Gustavo - Since F# only has tuple sizes 1-8 and "fakes it" for larger tuple sizes with nested tuples, I think it actually is possible to write this for all tuple sizes. I've never felt the need to do so, though, since there are usually better (IMHO) solutions to whatever problem is causing you to want to flatten tuples of unknown arity. – rmunn May 12 '17 at 10:30
  • I didn't put in the the overloads to artificially constrain the example and to show that the _sub-tuple_ of 4 items shouldn't compile – robkuz May 12 '17 at 11:34
  • 2
    @rmunn Think about this. How would the signature of such function looks like? I see your solution uses ``obj``, but I'm thinking in a type safe way. The underlying type of the F# tuple doesn't help, for the type checker a tuple is a syntactic tuple. – Gus May 12 '17 at 11:34
  • 4
    Why do you want to do this? – Tomas Petricek May 12 '17 at 12:35
  • 1
    Take a look at http://stackoverflow.com/questions/43919568/f-flatten-nested-tuples/43920984#43920984 It uses reflection, but is also type safe. – Sean May 12 '17 at 15:11
  • @robkuz - Ah, I understand. The `// and this too` comment communicated the *opposite of your intent* to me: I thought you meant "And this example (the 4-tuple) should also compile". – rmunn May 12 '17 at 17:25
  • @Sean eventually this post was exactly what initiated mine as I thought: "Can this not be made more typesafe?" – robkuz May 13 '17 at 11:20

2 Answers2

4

The .Net Tuple class has arities from 1 to 8 in its number of type parameters. I believe that in F#, if you have a tuple of 8 or more elements, it's treated as a tuple of seven elements plus a nested tuple in the eight slot, e.g. (a,b,c,d,e,f,g,h,i,j) is really (a,b,c,d,e,f,g,(h,i,j)), a tuple of type System.Tuple<'T1,'T2,'T3,'T4,'T5,'T6,'T7,System.Tuple<'T8,'T9,'T10>>.

However, your first approach only handles arities 2 and 3, yet you're testing it with an arity-4 tuple when you do flatten (1, (2, 2, 3, 4), (3,3)). What if you rewrite your first Flat function as follows?

static member inline Flat(x: obj) : 'x list = 
    match x with
    | :? Tuple<'a> as t -> Flatten $ (t.Item1)
    | :? Tuple<'a, 'b> as t -> Flatten $ (t.Item1, t.Item2)
    | :? Tuple<'a, 'b, 'c> as t ->Flatten $ (t.Item1, t.Item2, t.Item3)
    | :? Tuple<'a, 'b, 'c, 'd> as t -> Flatten $ (t.Item1, t.Item2, t.Item3, t.Item4)
    | :? Tuple<'a, 'b, 'c, 'd, 'e, 'f> as t -> Flatten $ (t.Item1, t.Item2, t.Item3, t.Item4, t.Item5, t.Item6)
    | :? Tuple<'a, 'b, 'c, 'd, 'e, 'f, 'g> as t -> Flatten $ (t.Item1, t.Item2, t.Item3, t.Item4, t.Item5, t.Item6, t.Item7)
    | :? Tuple<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> as t -> Flatten $ (t.Item1, t.Item2, t.Item3, t.Item4, t.Item5, t.Item6, t.Item7, t.Item8)
    | _ -> [x]

And, of course, you'd need corresponding static member inline ($) implementations for each of these arities from 1 through 8. Does that solve your problem?

P.S. Note that I only just typed this code in to the answer window in Stack Overflow; I haven't actually tested it yet.

rmunn
  • 34,942
  • 10
  • 74
  • 105
  • thx for the answer - sadly its not really what I wanted. Pls have look on my comments above. upvoted anyways – robkuz May 12 '17 at 11:35
2

I would like to wager the guess that this cannot be done in a type-safe way without a runtime type test.

module Tuple =
    open Microsoft.FSharp.Reflection
    let rec collect<'T> (x : obj) = [|
        if FSharpType.IsTuple <| x.GetType() then
            for y in FSharpValue.GetTupleFields x do
                yield! collect y 
        elif x :? 'T then yield x :?> 'T |]

Tuple.collect<int> (((100,101,102),"X"),1,2,3,(4,5))
// val it : int [] = [|100; 101; 102; 1; 2; 3; 4; 5|]

Inline overload resolution does not work, because F#'s type system isn't expressive enough to discern between a type 'T and a tuple 'T*'T by way of member constraints; the tuple is necessarily treated as an atomic unit 'T. Therefore, the compile-time scenario would always resolve to the atomic case and never to the tuples.

kaefer
  • 5,491
  • 1
  • 15
  • 20