Diferença entre dobrar e reduzir?

121

Tentando aprender F #, mas fiquei confuso ao tentar distinguir entre fold e reduzir . Fold parece fazer a mesma coisa, mas requer um parâmetro extra. Existe uma razão legítima para essas duas funções existirem ou elas existem para acomodar pessoas com diferentes origens? (Por exemplo: String e string em C #)

Aqui está o trecho de código copiado da amostra:

let sumAList list =
    List.reduce (fun acc elem -> acc + elem) list

let sumAFoldingList list =
    List.fold (fun acc elem -> acc + elem) 0 list

printfn "Are these two the same? %A " 
             (sumAList [2; 4; 10] = sumAFoldingList [2; 4; 10])
Wallace
fonte
1
Você pode escrever reduzir e dobrar em termos um do outro, por exemplo, fold f a lpode ser escrito como reduce f a::l.
Neil
9
@ Neil - Implementar foldem termos de reduceé mais complicado do que isso - o tipo de acumulador de foldnão precisa ser o mesmo que o tipo de coisas da lista!
Tomas Petricek
@TomasPetricek Meu erro, eu pretendia originalmente escrever o contrário.
Neil

Respostas:

171

Foldusa um valor inicial explícito para o acumulador enquanto reduceusa o primeiro elemento da lista de entrada como o valor inicial do acumulador.

Isso significa que o acumulador e, portanto, o tipo de resultado devem corresponder ao tipo de elemento da lista, enquanto eles podem diferir, foldpois o acumulador é fornecido separadamente. Isso se reflete nos tipos:

List.fold : ('State -> 'T -> 'State) -> 'State -> 'T list -> 'State
List.reduce : ('T -> 'T -> 'T) -> 'T list -> 'T

Além disso, reducelança uma exceção em uma lista de entrada vazia.

Lee
fonte
Então, basicamente, em vez de fazer fold, você pode simplesmente adicionar esse valor inicial ao início da lista e fazer reduce? Qual é o objetivo foldentão?
Pacerier
2
@Pacerier - A função acumuladora de dobra tem um tipo diferente: 'state -> 'a -> 'statepara dobra vs 'a -> 'a -> 'apara reduzir, portanto, reduz o tipo de resultado ao mesmo que o tipo de elemento. Veja a resposta de Tomas Petricek abaixo.
Lee
178

Além do que Lee disse, você pode definir reduceem termos de fold, mas não (facilmente) o contrário:

let reduce f list = 
  match list with
  | head::tail -> List.fold f head tail
  | [] -> failwith "The list was empty!"

O fato de foldusar um valor inicial explícito para o acumulador também significa que o resultado da foldfunção pode ter um tipo diferente do tipo de valores na lista. Por exemplo, você pode usar o acumulador do tipo stringpara concatenar todos os números em uma lista em uma representação textual:

[1 .. 10] |> List.fold (fun str n -> str + "," + (string n)) ""

Ao usar reduce, o tipo de acumulador é igual ao tipo de valores na lista - isso significa que, se você tiver uma lista de números, o resultado deverá ser um número. Para implementar a amostra anterior, você teria que converter os números em stringprimeiro e depois acumular:

[1 .. 10] |> List.map string
          |> List.reduce (fun s1 s2 -> s1 + "," + s2)
Tomas Petricek
fonte
2
Por que definir reduzir de forma que possa ocorrer erro no tempo de execução?
Fresheyeball
+1 para a nota sobre a generalidade de fold' & its ability to express reduzir '. Algumas línguas têm um conceito de quiralidade estrutural (Haskell, estou olhando para você). Você pode dobrar para a esquerda ou para a direita, visualmente representado neste wiki ( en.wikipedia.org/wiki/Fold_%28higher-order_function ). Com uma construção de identidade, os outros dois operadores FP "fundamentais" (filter e fmap) também são implementáveis ​​com uma construção de linguagem de primeira classe `fold 'existente (todas elas são construções isomórficas). ( Cs.nott.ac.uk/~pszgmh/fold.pdf ) Ver: Hott, Princeton (Esta seção de comentários é demasiado pequeno para conter ..)
Andrew
Por curiosidade .. isso tornaria o desempenho da redução mais rápido que o dobro, porque está sob menos suposições sobre tipos e exceções?
Sksallaj 28/06/19
19

Vejamos suas assinaturas:

> List.reduce;;
val it : (('a -> 'a -> 'a) -> 'a list -> 'a) = <fun:clo@1>
> List.fold;;
val it : (('a -> 'b -> 'a) -> 'a -> 'b list -> 'a) = <fun:clo@2-1>

Existem algumas diferenças importantes:

  • Embora reducefuncione apenas em um tipo de elemento, o acumulador e os elementos da lista foldpodem estar em tipos diferentes.
  • Com reduce, você aplica uma função fa todos os elementos da lista, começando pelo primeiro:

    f (... (f i0 i1) i2 ...) iN.

    Com fold, você aplica fa partir do acumulador s:

    f (... (f s i0) i1 ...) iN.

Portanto, reduceresulta em uma ArgumentExceptionlista vazia. Além disso, foldé mais genérico que reduce; você pode usar foldpara implementar reducefacilmente.

Em alguns casos, o uso reduceé mais sucinto:

// Return the last element in the list
let last xs = List.reduce (fun _ x -> x) xs

ou mais conveniente se não houver um acumulador razoável:

// Intersect a list of sets altogether
let intersectMany xss = List.reduce (fun acc xs -> Set.intersect acc xs) xss

Em geral, foldé mais poderoso com um acumulador de um tipo arbitrário:

// Reverse a list using an empty list as the accumulator
let rev xs = List.fold (fun acc x -> x::acc) [] xs
almofada
fonte
18

foldé uma função muito mais valiosa do que reduce. Você pode definir muitas funções diferentes em termos de fold.

reduceé apenas um subconjunto de fold.

Definição de fold:

let rec fold f v xs =
    match xs with 
    | [] -> v
    | (x::xs) -> f (x) (fold f v xs )

Exemplos de funções definidas em termos de dobra:

let sum xs = fold (fun x y -> x + y) 0 xs

let product xs = fold (fun x y -> x * y) 1 xs

let length xs = fold (fun _ y -> 1 + y) 0 xs

let all p xs = fold (fun x y -> (p x) && y) true xs

let reverse xs = fold (fun x y -> y @ [x]) [] xs

let map f xs = fold (fun x y -> f x :: y) [] xs

let append xs ys = fold (fun x y -> x :: y) [] [xs;ys]

let any p xs = fold (fun x y -> (p x) || y) false xs 

let filter p xs = 
    let func x y =
        match (p x) with
        | true -> x::y
        | _ -> y
    fold func [] xs
Raz Megrelidze
fonte
1
Você define o seu foldde forma diferente de List.foldcomo o tipo de List.foldé ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a, mas no seu caso ('a -> 'b -> 'b) -> 'b -> 'a list -> 'b. Apenas para deixar explícito. Além disso, sua implementação do append está errada. Funcionaria se você adicionar uma ligação a ela, por exemplo List.collect id (fold (fun x y -> x :: y) [] [xs;ys]), ou substituir contras pelo operador de acréscimo. Portanto, anexar não é o melhor exemplo nesta lista.
Jpe # 16/15