Sumar sobre listas de niveles arbitrarios de anidamiento en F #

10

Estoy tratando de crear una función F # que devolverá la suma de una lista de ints de anidamiento arbitrario. Es decir. funcionará para a list<int>, a list<list<int>>y a list<list<list<list<list<list<int>>>>>>.

En Haskell escribiría algo como:

class HasSum a where
    getSum :: a -> Integer

instance HasSum Integer where
    getSum = id

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

lo que me dejaría hacer:

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

¿Cómo puedo lograr esto en F #?

runas
fuente
1
No sé F # lo suficiente: no sé si admite algo como las clases tipográficas de Haskell. En el peor de los casos, debería poder pasar diccionarios explícitos incluso si no es tan conveniente como en Haskell, donde el compilador le ofrece los diccionarios correctos. El código F # en tal caso sería algo así como getSum (dictList (dictList (..... (dictList dictInt)))) nestedListel número de dictListcoincidencias con el número del []tipo de nestedList.
chi
¿Podría hacer que este código haskell se ejecute en un REPL?
Filipe Carvalho
aquí tienes ... repl.it/repls/BlondCoolParallelport
karakfa
F # no tiene clases de tipo ( github.com/fsharp/fslang-suggestions/issues/243 ). Intenté el truco de sobrecarga del operador que en teoría podría funcionar, pero logré bloquear el compilador, pero tal vez puedas hacer algo con el truco: stackoverflow.com/a/8376001/418488
Solo otro metaprogramador
2
No puedo imaginar una base de código F # realista en la que necesite esto. ¿Cuál fue su motivación para hacer esto? Probablemente cambiaría el diseño para que no se encuentre en una situación como esta; de todos modos, probablemente mejorará su código F #.
Tomas Petricek

Respuestas:

4

ACTUALIZAR

Encontré una versión más simple usando un operador en ($)lugar de un miembro. Inspirado en 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

El resto de la explicación aún se aplica y es útil ...

Encontré una manera de hacerlo posible:

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

Ejecutando su ejemplo:

let list v = List.replicate 6 v

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

Esto se basa en el uso de SRTP con restricciones de miembros: static member Sumla restricción requiere que el tipo tenga un miembro llamado Sum que devuelva un int. Cuando se usan SRTP, las funciones genéricas deben ser inline.

Esa no es la parte difícil. La parte difícil es "agregar" Summiembros a un tipo existente como inty Listque no está permitido. Pero, podemos agregarlo a un nuevo tipo SumOperationse incluir en la restricción (^t or ^a) dónde ^tsiempre va a estar SumOperations.

  • getSum0declara la Sumrestricción de miembro y la invoca.
  • getSum pasa SumOperationscomo el primer parámetro de tipo agetSum0

La línea static member inline Sum(x : float ) = int xse agregó para convencer al compilador de que use una llamada de función dinámica genérica y no solo por defecto static member inline Sum(x : int )al llamarList.sumBy

Como puede ver es un poco complicado, la sintaxis es compleja y fue necesario solucionar algunas peculiaridades en el compilador, pero al final fue posible.

Este método se puede extender para trabajar con matrices, tuplas, opciones, etc. o cualquier combinación de ellas agregando más definiciones a 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
fuente
Esta es una gran solución! pero ¿por qué no solo recursión o pliegue?
s952163
44
La recursión y el plegado no pueden manejar diferentes tipos. Cuando se instancia una función recursiva genérica, corrige el tipo de los parámetros. En este caso, cada llamada a Sumse realiza con un tipo más simple: Sum<int list list list>, Sum<int list list>, Sum<int list>, Sum<int>.
AMieres
2

Aquí está la versión en tiempo de ejecución, funcionaría con todas las colecciones .net. Sin embargo, intercambia errores del compilador en la respuesta de AMieres por excepciones de tiempo de ejecución y AMieres también es 36 veces más rápido.

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

Puntos de referencia

|    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
fuente
1
Funciona bien, aunque es notablemente más lento: ejecutarlo 10 veces tomó 56 segundos en comparación con 1 segundo con la otra solución.
AMieres
¡Benchmarking impresionante! ¿que usaste?
AMieres