50
votes

Comment les langages purement fonctionnels gèrent-ils les algorithmes basés sur l'indice?

J'ai essayé de découvrir la programmation fonctionnelle, mais j'ai toujours du mal à penser comme un programmeur fonctionnel. L'un de ces accrochages est de savoir comment mettre en œuvre des opérations indexes qui reposent fortement sur les boucles / ordre-d'exécution.

Par exemple, considérez le code Java suivant:

public class Main {
    public static void main(String[] args) {
        List<Integer> nums = Arrays.asList(1,2,3,4,5,6,7,8,9);
        System.out.println("Nums:\t"+ nums);
        System.out.println("Prefix:\t"+prefixList(nums));
    }
  
    private static List<Integer> prefixList(List<Integer> nums){
      List<Integer> prefix = new ArrayList<>(nums);
      for(int i = 1; i < prefix.size(); ++i)
        prefix.set(i, prefix.get(i) + prefix.get(i-1));
      return prefix;
    }
}
/*
System.out: 
Nums:   [1, 2, 3, 4, 5, 6, 7, 8, 9]
Prefix: [1, 3, 6, 10, 15, 21, 28, 36, 45]
*/


6 commentaires

Comme indiqué ci-dessous, ce n'est pas un exemple où l'on a vraiment besoin d'indices. En effet, de nombreux algorithmes communs n'ont pas besoin d'indices. Pourtant, certains faire : par exemple, en calculant la séquence 0, a [0], a [a [0]], a [a [a [0]]], ... nécessite un accès aléatoire (au-delà de supposer que tous les éléments sont des indices valides). Dans ces rares circonstances, nous avons recours à ... des tableaux, par exemple data.vector . Haskell peut être utilisé impérativement, en cas de besoin - nous avons rarement besoin de le faire.


Votre question sur la mise en œuvre de votre algorithme dans un langage fonctionnel ou spécifiquement est-elle uniquement sur l'utilisation des ADT (par exemple, comment la liste de Haskell est implémentée en tant que liste liée)? Il existe des langages fonctionnels qui ont des types de valeur qui ne sont pas des ADT, voir Swift array ou Haskell's vector ( mentionné ci-dessus), tandis que ce dernier ne prend pas en charge la mise à jour O (1).


Votre implémentation est déjà parfaitement fonctionnelle. prefixList () ne dépend pas d'aucune autre donnée que celle réalisée dans ses arguments et n'a pas d'autre effet que sa valeur de retour. C'est l'essence d'une fonction pure . Ou cours que certaines langues facilitent l'écriture de code fonctionnel que d'autres, je noterais Java à environ 4,5 des 10 lambdas.


@Feuermurmel Ma question était plus liée à tout algorithme que le préfixe que j'ai fourni à titre d'exemple. J'essaie d'apprendre à appliquer les modèles de réflexion pour s'éloigner de toujours en utilisant des indices.


@Aaronc C'est bien sûr un objectif équitable, essayant d'implémenter des algorithmes sur les listes en termes de transformations de toute la liste au lieu d'accéder explicitement aux éléments individuels par index! C'était le titre qui me dérangeait parce qu'il implique que les langages fonctionnels en soi ne peuvent pas gérer les algorithmes basés sur l'indice.


L'exemple quintessentiel de ce que vous demandez sur les tours fibs n = fs !! n où fs = 0: 1: ft; ft = [fs !! (k-1) + fs !! (k-2) | k <- [2..n]] dans fibs n = fs !! N; fs = 0: 1: zipwith (+) (Drop 1 fs) fs .


8 Réponses :



59
votes

Croyez-le ou non, cette fonction est en fait intégré à Haskell.

scanl :: (b -> a -> b) -> b -> [a] -> [b]
scanl _ q [] = [q]
scanl f a (b:xs) = a : scanl f (f a b) xs

La réponse large est donc très souvent: il y a des primitives liées à la liste à partir desquelles nous construisons plutôt que d'écrire des boucles à la main. Les gens aiment dire que la récursivité est l'analogue le plus proche de FP à "pour les boucles" dans la programmation impérative. Bien que cela puisse être vrai, le programme fonctionnel moyen utilise un lot moins explicite que le programme impératif moyen utilise pour les boucles. La plupart de ce que nous faisons (en particulier avec les listes) est construit de map , Filter , Foldl , et tous les autres (hautement optimisés ) Goodies in data.list , data.folable Code> , data.taversable , et le reste de base .

Quant à la façon dont nous implémentons ces fonctions, vous pouvez consulter la source pour scanl . Celui sur GHC est écrit un peu différemment pour des raisons d'efficacité, mais l'essentiel de base est

> scanl (+) 0 [1..9]
[0,1,3,6,10,15,21,28,36,45]

Vous n'avez pas besoin d'indices. Vous devez garder une trace de la valeur précédente unique lorsque vous créez la liste, et nous pouvons le faire avec un argument de fonction. Si vous avez vraiment un algorithme qui a besoin d'un accès aléatoire à divers indices, nous avons data.vector Pour cela. Mais 99% du temps, la réponse est "Arrêtez de penser aux indices".


8 commentaires

«99% du temps, la réponse est« d'arrêter de penser aux indices »est irréaliste. Beaucoup de choses que les gens font avec les tableaux en c / python / matlab, etc. par ex. Pour la simulation ou la science des données, ne peut pas être exprimé avec vrai avec les opérations de la liste Haskell. Je suis toujours entièrement d'accord pour dire que l'obsession des tableaux avec des indices devrait être surmontée, mais cela nécessite généralement un peu plus de changements de pensée. cs.ox.ac.uk/seminars/2418.html


@LeftaroundAbout, je ne serais pas d'accord. Je dirais que 99,5% de ce que les gens font avec les indices sont insignifiants pour convertir en formes non indexes. Les derniers 0,5% sont en effet intéressants et méritent d'être prêts à prêter attention. Parlant de mon expérience et de mon code que j'ai observé, il est très rare que nous fassions des choses vraiment intéressantes avec des index. La plupart du temps, il est juste ennuyeux en boucle sur les données dans l'ordre. Ils sont tellement ennuyeux que nous ne remarquons même pas que nous les faisons plus. Vous avez raison, cependant, que le dernier 0,5% nécessite souvent un changement fondamental substantiel de perspective.


Un exemple particulièrement gênant serait la cryptographie qui est intentionnellement conçue pour être lente sur les GPU. Ils ont tendance à être construits en accédant à de grands tableaux de manière extrêmement difficile à prévoir. J'avoue que je ne suis pas sûr à 100% de ce que la communauté du langage de programmation fonctionnelle fait avec celles-ci (à part envelopper peut-être les versions impératives, car l'API de haut niveau pour ces choses a tendance à avoir déjà une sensation fonctionnelle pour eux)


@Cortammon, je comprends que FP consiste à réduire l'état pour augmenter le parallélisme et réduire la complexité. Les algos cryptographiques sont conçus pour être difficiles à paralléliser et l'état fait souvent partie intégrante de la conception. Donc, utiliser FP serait une cheville carrée ...


@Aron true, utiliser FP pour résoudre ce problème est probablement un antipattern. Mais écrire un programme FP qui invoque une fonction C pour faire de la crypto est assez courant, et je pense que c'est ce que Cort suggérait dans leur dernière phrase.


@Cortammon Eh bien, c'est peut-être vrai si vous comptez aussi les choses vraiment triviales qui peuvent être faites dans n'importe quelle langue moderne avec une sorte de plage basée sur une gamme pour la vectorisation en boucle / implicite, etc. Mais quiconque utilise encore aujourd'hui des indices pour ce type de tâche mérite d'être giflé sur la tête avec une impression physique de leur code. Le "0,5%" de code que ne traduit pas facilement par des listes récursive-haskell-on inclut encore beaucoup de choses qui sont assez simples avec les tableaux, comme la mise à jour d'une cellule dans un tableau multidimensionnel basé sur tous ses voisins. Aucun exemples de crypto artificiels nécessaires.


(... Oui, il y a aussi des possibilités fonctionnelles soignées pour ces cas - des comonades, n'importe qui? - mais encore une fois, cela nécessite un peu plus d'investissement de pensée initiale, une carte simple / pli / scan / traversée ne coupera pas ce.)


"Vous devez garder une trace de la valeur précédente unique lorsque vous créez la liste, et nous pouvons le faire avec un argument de fonction." ou même sans elle : pSum (a: b: c) = a: PSUM (a + b: c); PSUM x = x .



14
votes

Ce n'est pas une réponse Haskell, mais vous avez marqué la question Lisp alors voici une réponse dans Racket: c'est à la fois entièrement fonctionnel et montre que vous n'avez pas besoin d'indices pour ce problème. Ce que fait cette fonction, c'est prendre un flux de nombres et renvoyer le flux de préfixe pour lui. Tout cela est fait paresseusement.

(stream->list (stream-take (prefix-stream (in-naturals)) 10))
'(0 1 3 6 10 15 21 28 36 45)
> (stream->list (stream-take (stream-tail (prefix-stream (in-naturals)) 1000) 10))
'(500500 501501 502503 503506 504510 505515 506521 507528 508536 509545)

Alors maintenant

> (prefix-stream (in-naturals))
#<stream>

mais bien sûr, vous pouvez également le faire:

> (stream->list (prefix-stream (in-range 1 10)))
'(1 3 6 10 15 21 28 36 45)

Ce n'est évidemment pas un flux que vous pouvez convertir en liste, mais vous pouvez en regarder des parties:

(define (prefix-stream s)
  (let ps-loop ([st s]
                [p 0])
    (if (stream-empty? st)
        empty-stream
        (let ([np (+ p (stream-first st))])
          (stream-cons 
              np 
              (ps-loop 
                  (stream-rest st)
                  np))))))

(Remarque que en nature considère les naturels de commencer à 0, comme c'est bien et propre.)


0 commentaires

6
votes

Dans Clojure, cela pourrait être écrit comme:

(defn prefix-list [nums]
  (loop [i 1
         prefix nums]
    (if (= i (count nums))
      prefix
      (recur (inc i) (assoc prefix i (+ (get prefix i) (get prefix (dec i))))))))

(préfixe-list [1 2 3 4 5 6 7 8 9]) renvoie [ 1 3 6 10 15 21 28 36 45]

Dans Clojure, les données sont généralement immuables (elles ne peuvent pas être modifiées). La fonction association dans ce cas prend un vecteur et renvoie un nouveau vecteur comme l'original, mais avec le i

th

élément modifié. Cela peut sembler inefficace, mais les structures de données sous-jacentes permettent de faire la mise à jour en un temps presque constant ( o (log (n))).

Comme d'autres l'ont souligné, ce problème particulier pourrait être codé sans utiliser de vecteurs indexés, mais je m'efforce de fournir une solution fidèle à votre code Java d'origine dans l'utilisation d'un tableau indexé.


0 commentaires

5
votes

Je sais que vous avez posé des questions sur les langages fonctionnels, mais je voulais juste que le carillon ne soit pas invité pour mentionner que Python, étant une langue multi-paradigme, a également cela comme le joli ordre supérieur itertools.accumulate Fonction.

accumuler prend une collection et renvoie un itérateur de ses sommes partielles, ou toute fonction binaire personnalisée. Le code Python fonctionnel équivalent à votre exemple ne serait que:

from itertools import accumulate
print(list(accumulate(range(1, 10))))

En général, le python itertools et functools / a > Les modules de bibliothèque standard offrent d'excellents outils pour la programmation fonctionnelle.


1 commentaires

Le nom étant partagé avec le std :: accumulater Cela sert le même but.



1
votes

Cette fonctionnalité est également appelée "somme cumulative", et une autre méthode Python consiste à utiliser numpy (qui, étant écrit en c, est ultra rapide):

[1, 3, 6, 10, 15, 21, 28, 36, 45]

sortie:

nums = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9]) # or np.arange(1, 10) or np.arange(9)+1
print(nums.cumsum())

p>


0 commentaires

3
votes

D'autres ont souligné que votre exemple particulier peut être bien géré avec des fonctions comme scanl exécution". Nous pouvons décomposer le problème en trois préoccupations:

  • Indexation
  • Looping
  • Mutation
  • L'indexation dans une structure de données est prise en charge partout où le concept d'indexation est logique. Par exemple, list et vector prennent en charge l'indexation. Comme d'autres l'ont souligné, vector a de meilleures performances si votre index est aléatoire, mais c'est étonnamment rare.

    Les boucles impératives peuvent être remplacées directement par des fonctions récursives (voir wikpedia ) Bien qu'il existe tellement de «fonctions d'ordre supérieur» (fonctions qui prennent des fonctions comme des arguments) implémentées dans les bibliothèques prélude et standard dont vous aurez besoin rarement de récursivité explicite. Scanl en est un exemple. Il vous permet d'éviter un appel récursif explicite en l'élevant à une fonction pré-écrite. Cette fonction, cependant, est définie de manière récursive . Le compilateur peut optimiser cette récursivité dans une boucle lorsqu'elle génère du code machine.

    Enfin, vous pourriez avoir un tableau de nombres et vous voulez vraiment, vraiment muter les valeurs dans le tableau sur une série d'étapes. Tout le monde dans ce fil, y compris moi, s'efforcerait de vous en parler et de vous faire penser d'une manière plus "fonctionnelle". Mais si nous échouons, vous pouvez utiliser le État thread monad . Cela vous donne un moyen de muter vos structures de données localement (effrayantes) dans une fonction tout en interagissant avec les choses en dehors de la fonction uniquement avec des structures de données immuables (pas effrayantes) et garantit donc que la fonction est référentiellement transparent .


    0 commentaires

    0
    votes

    Il existe des fonctions intégrées dans Elixir qui sont similaires aux Haskell: enum.scan / 2 et enum.scan / 3 :

    defmodule Foo do
      def bar(nums, acc \\ [])
    
      # We've reached the end of input: return the accumulator (reversed)
      def bar([], acc), do: Enum.reverse(acc)
    
      # The very first call is special because we do not have a previous value
      def bar([x | tail], []) do
        bar(tail, [x])
      end
    
      # All other calls land here
      def bar([x | tail], [prev | _] = acc) do
        bar(tail, [x + prev | acc])
      end
    end
    
    
    nums =   [1, 2, 3, 4,  5,  6,  7,  8,  9]
    prefix = Foo.bar(nums)
    IO.inspect(prefix)
    
    # Yields the same result:
    # [1, 3, 6, 10, 15, 21, 28, 36, 45]
    


    0 commentaires