9

I'm trying to create an F# function that will return the sum of a list of ints of arbitrary nestedness. Ie. it will work for a list<int>, a list<list<int>> and a list<list<list<list<list<list<int>>>>>>.

In Haskell I would write something like:

class HasSum a where
    getSum :: a -> Integer

instance HasSum Integer where
    getSum = id

instance HasSum a => HasSum [a] where
    getSum = sum . map getSum

which would let me do:

list :: a -> [a]
list = replicate 6

nestedList :: [[[[[[[[[[Integer]]]]]]]]]]
nestedList =
    list $ list $ list $ list $ list $
    list $ list $ list $ list $ list (1 :: Integer)

sumNestedList :: Integer
sumNestedList = getSum nestedList

Link to runnable code.

How can I achieve this in F#?

runeks
  • 1,745
  • 2
  • 17
  • 26
  • 1
    I don't know F# enough -- I don't know if it supports something like Haskell's typeclasses. In the worst case, you should be able to pass explicit dictionaries even if it's not as convenient as in Haskell where the compiler infers the right dictionaries for you. The F# code in such case would be something like `getSum (dictList (dictList (..... (dictList dictInt)))) nestedList` where the number of `dictList` matches the number of `[]` in the type of `nestedList`. – chi Feb 13 '20 at 12:25
  • Could you make this haskell code runnable on a REPL? – Filipe Carvalho Feb 13 '20 at 14:43
  • here you go... https://repl.it/repls/BlondCoolParallelport – karakfa Feb 13 '20 at 19:31
  • F# don't have type classes (https://github.com/fsharp/fslang-suggestions/issues/243). I tried the operator overloading trick that in theory could work but I just managed to crash the compiler but perhaps you can make something of the trick: https://stackoverflow.com/a/8376001/418488 – Just another metaprogrammer Feb 14 '20 at 07:13
  • It is possible to solve this using reflection but I don't think this is the answer you are looking for. – Just another metaprogrammer Feb 14 '20 at 07:17
  • 2
    I cannot imagine any realistic F# codebase where you would need this. What was your motivation for doing this? I would probably change the design so that you do not get into a situation like this - it will probably make your F# code better anyway. – Tomas Petricek Feb 14 '20 at 15:09
  • @FilipeCarvalho I've added a link to a runnable piece of code to the question. – runeks Mar 25 '21 at 12:14

2 Answers2

4

UPDATE

I found a simpler version using an operator ($) instead of a member. Inspired by https://stackoverflow.com/a/7224269/4550898 :

type SumOperations = SumOperations 

let inline getSum b = SumOperations $ b // <-- puting this here avoids defaulting to int

type SumOperations with
    static member inline ($) (SumOperations, x  : int     ) = x 
    static member inline ($) (SumOperations, xl : _   list) = xl |> List.sumBy getSum

The rest of the explanation still applies and it is useful...

I found a way to make it possible:

let inline getSum0< ^t, ^a when (^t or ^a) : (static member Sum : ^a -> int)> a : int = 
    ((^t or ^a) : (static member Sum : ^a -> int) a)

type SumOperations =
    static member inline Sum( x : float   ) = int x
    static member inline Sum( x : int     ) =  x 
    static member inline Sum(lx : _   list) = lx |> List.sumBy getSum0<SumOperations, _>

let inline getSum x = getSum0<SumOperations, _> x

2                  |> getSum |> printfn "%d" // = 2
[ 2 ; 1 ]          |> getSum |> printfn "%d" // = 3
[[2; 3] ; [4; 5] ] |> getSum |> printfn "%d" // = 14

Running your example:

let list v = List.replicate 6 v

1
|> list |> list |> list |> list |> list
|> list |> list |> list |> list |> list
|> getSum |> printfn "%d" // = 60466176

This is based on using SRTPs with member constraints: static member Sum, the constraint requires the type to have a member called Sum that returns an int. When using SRTPs generic functions need to be inline.

That is not the difficult part. The hard part is "adding" Sum member to an existing type like int and List which is not allowed. But, we can add it to a new type SumOperations and include in the constraint (^t or ^a) where ^t is always going to be SumOperations.

  • getSum0 declares the Sum member constraint and invokes it.
  • getSum passes SumOperations as the first type parameter to getSum0

The line static member inline Sum(x : float ) = int x was added to convince the compiler to use a generic dynamic function call and not just default to static member inline Sum(x : int ) when calling List.sumBy

As you can see is a bit convoluted, the syntax is complex and it was necessary to work around some quirks on the compiler but in the end it was possible.

This method can be extended to work with Arrays, tuples, options, etc. or any combination of them by adding more definitions to SumOperations:

type SumOperations with
    static member inline ($) (SumOperations, lx : _   []  ) = lx |> Array.sumBy getSum
    static member inline ($) (SumOperations, a  : ^a * ^b ) = match a with a, b -> getSum a + getSum b 
    static member inline ($) (SumOperations, ox : _ option) = ox |> Option.map getSum |> Option.defaultValue 0

(Some 3, [| 2 ; 1 |]) |> getSum |> printfn "%d" // = 6

https://dotnetfiddle.net/03rVWT

AMieres
  • 4,944
  • 1
  • 14
  • 19
  • this is a great solution! but why not just recursion or fold? – s952163 Feb 14 '20 at 14:12
  • 4
    Recursion and fold cannot handle varying types. When a generic recursive function is instantiated it fixes the type of the parameters. In this case every call to `Sum` is done with a simpler type: `Sum`, `Sum`, `Sum`, `Sum`. – AMieres Feb 14 '20 at 14:23
2

Here is the runtime version, would work with all .net collections. However, exchanges compiler errors in AMieres' answer for runtime exceptions and AMieres' is 36x faster too.

let list v = List.replicate 6 v

let rec getSum (input:IEnumerable) =
    match input with
    | :? IEnumerable<int> as l -> l |> Seq.sum
    | e -> 
        e 
        |> Seq.cast<IEnumerable> // will runtime exception if not nested IEnumerable Types
        |> Seq.sumBy getSum


1 |> list |> list |> list |> list |> list
|> list |> list |> list |> list |> list |> getSum // = 60466176

Benchmarks

|    Method |        Mean |     Error |    StdDev |
|---------- |------------:|----------:|----------:|
| WeirdSumC |    76.09 ms |  0.398 ms |  0.373 ms |
| WeirdSumR | 2,779.98 ms | 22.849 ms | 21.373 ms |

// * Legends *
  Mean   : Arithmetic mean of all measurements
  Error  : Half of 99.9% confidence interval
  StdDev : Standard deviation of all measurements
  1 ms   : 1 Millisecond (0.001 sec)
jbtule
  • 31,383
  • 12
  • 95
  • 128
  • 1
    It works well, although is noticeably slower: running it 10 times took 56 seconds compared to 1 second with the other solution. – AMieres Feb 14 '20 at 22:50
  • 1
    https://benchmarkdotnet.org/articles/guides/getting-started.html – jbtule Feb 16 '20 at 15:05