Somando listas de níveis arbitrários de aninhamento em F #

10

Estou tentando criar uma função F # que retornará a soma de uma lista de ints de aninhamento arbitrário. Ou seja. funcionará para a list<int>, a list<list<int>>e a list<list<list<list<list<list<int>>>>>>.

Em Haskell, eu escreveria 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

o que me deixaria fazer:

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

Como posso conseguir isso em F #?

runas
fonte
11
Eu não sei o F # o suficiente - não sei se ele suporta algo como as classes de tipo de Haskell. Na pior das hipóteses, você poderá transmitir dicionários explícitos, mesmo que não seja tão conveniente quanto em Haskell, onde o compilador infere os dicionários certos para você. O código F # nesse caso seria algo como getSum (dictList (dictList (..... (dictList dictInt)))) nestedListonde o número de dictListcorresponde ao número de []no tipo de nestedList.
chi
Você poderia tornar esse código haskell executável em um REPL?
Filipe Carvalho
aqui vai você ... repl.it/repls/BlondCoolParallelport
karakfa
F # não tem classes de tipo ( github.com/fsharp/fslang-suggestions/issues/243 ). Eu tentei o truque de sobrecarga do operador que, em teoria, poderia funcionar, mas apenas consegui travar o compilador, mas talvez você possa fazer algo do truque: stackoverflow.com/a/8376001/418488
Apenas outro metaprogramador
2
Não consigo imaginar nenhuma base de código F # realista em que você precisaria disso. Qual foi a sua motivação para fazer isso? Provavelmente eu mudaria o design para que você não entre em uma situação como essa - provavelmente tornará seu código F # melhor de qualquer maneira.
Tomas Petricek

Respostas:

4

ATUALIZAR

Encontrei uma versão mais simples usando um operador em ($)vez de um membro. Inspirado em 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

O restante da explicação ainda se aplica e é útil ...

Eu encontrei uma maneira de tornar isso possível:

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

Executando seu exemplo:

let list v = List.replicate 6 v

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

Isso se baseia no uso de SRTPs com restrições de membro:, static member Suma restrição requer que o tipo tenha um membro chamado Sum que retorne um int. Ao usar SRTPs, funções genéricas precisam ser inline.

Essa não é a parte difícil. A parte difícil é "adicionar" Summembro a um tipo existente como inte Listque não é permitido. Porém, podemos adicioná-lo a um novo tipo SumOperationse incluir na restrição (^t or ^a) onde ^tsempre estará SumOperations.

  • getSum0declara a Sumrestrição de membro e a invoca.
  • getSum passa SumOperationscomo o primeiro parâmetro de tipo paragetSum0

A linha static member inline Sum(x : float ) = int xfoi adicionada para convencer o compilador a usar uma chamada de função dinâmica genérica e não apenas o padrão ao static member inline Sum(x : int )chamarList.sumBy

Como você pode ver, é um pouco complicado, a sintaxe é complexa e foi necessário contornar algumas peculiaridades no compilador, mas no final foi possível.

Esse método pode ser estendido para trabalhar com matrizes, tuplas, opções etc. ou qualquer combinação delas, adicionando mais definições 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
fonte
Esta é uma otima soluçao! mas por que não apenas recursão ou dobra?
s952163 14/02
4
Recursão e dobra não podem lidar com tipos diferentes. Quando uma função recursiva genérica é instanciada, ela corrige o tipo dos parâmetros. Neste caso, cada chamada para Sumé feito com um tipo mais simples: Sum<int list list list>, Sum<int list list>, Sum<int list>, Sum<int>.
AMieres 14/02
2

Aqui está a versão em tempo de execução, funcionaria com todas as coleções .net. No entanto, as trocas de erros do compilador na resposta do AMieres para exceções de tempo de execução e no AMieres também são 36x mais rápidas.

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
fonte
11
Funciona bem, embora seja visivelmente mais lento: executá-lo 10 vezes levou 56 segundos em comparação com 1 segundo com a outra solução.
AMieres 14/02
Benchmarking impressionante! o que você usou?
AMieres 16/02