6
votes

Traduire le flux de contrôle impératif avec Break-S / Continu-s à Haskell

Considérez le code impératif suivant qui trouve le plus grand palindrome parmi les produits de nombres à 3 chiffres (oui, c'est l'une des premières tâches de «projet de [mathématicien exceptionnel du 18ème siècle]»): xxx

Comme je l'apprenant sur HASKELL, ma question est de savoir comment traduisez-vous cela ( et fondamentalement toute construction impérative qui contient quelque chose de plus complexe que simplement une simple itération forte>, par exemple des pauses, continue , Variables temporaires et tout cela) à Haskell? P>

Ma version est P>

maxpal i curmax
    | i < 100 = curmax
    | otherwise = maxpal (i-1) (innerloop 999)
    where 
        innerloop j
            | (j < 100) || (p < curmax) = curmax
            | pal p = p
            | otherwise = innerloop (j-1)
            where p = i*j
main = print $ maxpal 999 0


3 commentaires

Eh bien, si vous essayez de traduire un flux de contrôle impératif dans HASKELLL, vous sera dans l'impératif Uglytown, à peu près par définition.


C'est la question actuelle: non comment traduire aveuglément le code impératif à la syntaxe HASKELLL, mais quelles approches sont utilisées dans la programmation fonctionnelle afin de traiter élégamment les flux complexes.


C'est peut-être un peu gnomique, mais plutôt que de traduire d'impératif à fonctionnel, je reviens habituellement à la spécification (les spécifications ne se soucient souvent pas du flux de contrôle) et de travailler à partir de cela. Lorsque vous avez un flux de contrôle complexe, un style de passage de continuation (CPS) et une évaluation paresseux (qui peut séparer la production de la consommation de valeurs intermédiaires) sont deux techniques précieuses pour résumer la complexité.


9 Réponses :


2
votes

Dans mon esprit une plage correspond à une liste. Par exemple: xxx

maintenant f est défini comme la séquence des nombres de 999 à 100.

pour pour Les boucles correspondent à différents concepts fonctionnels, en fonction de ce que vous faites dans chaque itération. Parfois, une carte est l'analogique approprié, parfois un pli , parfois quelque chose d'autre. Souvent, c'est une combinaison de choses. Dans ce cas, vous combinez efficacement deux listes. Une façon de le faire dans HASKELLL est une compréhension de liste: xxx

ici g représente une liste du produit de chaque élément de la séquence définie précédemment combiné avec lui-même. En d'autres termes, à peu près ce que vous avez entré dans votre pour boucle.

d'ici, vous voudrez probablement filtre La séquence résultante ne contient que des valeurs qui sont des palindromes, puis Calculez le Maximum valeur de cet ensemble.


0 commentaires

5
votes

Si nous éloignons de toute optimisation et que nous multiplions simplement toutes les combinaisons de nombres compris entre 100 et 999, filtrez les non-palindromes et prenez le maximum de cela, nous pouvons écrire la fonction très concistance comme suit:

import Data.Maybe
import Data.List

maxpal i curmax
    | i < 100 = curmax
    | otherwise = maxpal (i-1) newmax
    where newmax = fromMaybe curmax (find pal bigger)
          bigger = takeWhile (> curmax) (map (*i) [999, 998 ..])


3 commentaires

Votre deuxième solution est ce que je cherchais. Il met en évidence plusieurs primitives d'itération comme Trouvez et à prendre en charge, ce qui est bon pour un débutant comme moi et fait exactement ce que fait mon code impératif.


C'est frappant pour moi à quel point tout le monde a commencé à conseiller une compréhension de la liste (qui - j'aurais dû ajouter que à la question - je suis au courant). Il ne fonctionne clairement pas bien dans ce cas: Changez «Nombres à 3 chiffres», contrainte à 4 chiffres et vous attendez à jamais. Peu importe la compréhension intelligente, vous ne pouvez pas fuir de O (n ^ 2) dans ce cas spécifique.


@dorseg: Cependant, c'est que dans ce cas, la taille d'entrée est corrigée et pour la taille d'entrée donnée, la compréhension de la liste fonctionne raisonnablement bien. Il n'y a aucune raison d'optimiser si vous savez que la taille d'entrée ne va jamais grandir. De plus, le problème de la première solution n'est pas que c'est une compréhension de la liste, mais qu'il est quadratique. La carte (* I) [999, 998 ..] pourrait aussi être écrite comme une compréhension de liste ( [x * i | x <- [999, 998 ..]] < / code>) Sans perdre aucune performance.



7
votes

Réponse similaire à Daniel et à SEPP2K's:

La programmation fonctionnelle paresseuse vous permet d'écrire des programmes de manière beaucoup plus modulaire que ce que vous voyez dans un flux de contrôle impératif comme celui de votre question. Par exemple, former la liste des facteurs 999 ... 100, puis tous les produits, puis filtrer pour ne conserver que les palindromes, puis calculer le maximum. Grâce à la paresse em>, ces listes intermédiaires n'auront qu'en cas de besoin et seront incrémentalement recyclées. P>

Pour plus d'explications et d'exemples, voir le papier classique de John Hughes Pourquoi la programmation fonctionnelle compte em> . P>

maxpal :: Int
maxpal = maximum [i*j | i <- factors, j <- factors, pal (i*j) ]

factors :: [Int]
factors = [999,998..100]

pal :: Show a => a -> Bool
pal = palL . show

palL :: (Eq a) => [a] -> Bool
palL xs = xs == reverse xs


5 commentaires

Hm. La version de Dorserg teste d'abord la taille du produit et alors essaie le test du palindrome, ce qui pourrait être une optimisation importante manquante des suggestions de Daniel, Sepp2k et mes suggestions (toutes très similaires).


En fait, il ne manque pas de ma deuxième solution plus longue.


@ SEPP2K: Désolé de ne pas être plus précis dans mes comparaisons. L'optimisation est manquante dans les versions élégantes et composacieuses données jusqu'à présent, y compris la vôtre. Je me demande comment élégamment et modulaire, on pouvait écrire maxpal tout en ayant cette optimisation.


Je ressens que la compréhension de la liste + filtrage est un outil qui dans la plupart des cas où le produit cartésien de deux sources ou plus est produit, n'est pas vraiment "Code de production". C'est à dire. Trop lent - en quelque sorte comme un jouet. C'est dangereux car la syntaxe est tellement succincte. J'aimerais voir que les compréhensions de liste soient étendues pour spécifier la manière dont la recherche doit être effectuée. Si le programmeur ne peut pas spécifier comment la recherche doit être effectuée, il est probablement vraiment lent.


@ user239558: Avez-vous des preuves pour soutenir votre sentiment sur la compréhension de la liste et le filtrage exécutant lentement? Savez-vous que les compréhensions éliminées (désutuges) à la compilation? Avez-vous lu des papiers et avez-vous vu des statistiques sur l'optimisation des frameworks de fusion? Il y a beaucoup d'informations et expérience là-bas.



1
votes

gah. Battu par Sepp2k, mais je vais répondre à votre question générale:

Les variables temporaires peuvent également être exprimées à l'aide de l'état Monad ou de St monad si vous en avez beaucoup. FP gagne souvent dans la succincintness et la clarté, mais dans certains cas, cela ne l'a pas, par exemple. quand il y a plusieurs variables locales à jongler.

La paresse peut imiter de nombreuses pauses, mais lorsqu'il s'agit d'IO, vous devez généralement utiliser une récursion explicite. Cependant, le package «Liste» (de Hackage) est plutôt intelligent à vous permettre d'écrire des boucles IO dans un style fonctionnel.


0 commentaires

2
votes

Il n'y a pas de réponse unique - toutes les réponses ici. Mais passons à travers cet exemple spécifique:

Premièrement, considérez la boucle extérieure: nous effectuons toujours toute la gamme complète et nous ne nous soucions que du maximum final, c'est suffisamment facile: P>

checkMax ij defer = if pal ij then ij else defer


2 commentaires

+1 Je suis un novice Haskell Novice, mais je suis surpris que cette réponse n'avait pas de votes, apparemment, elle imite très bien l'intention du code Python et qu'il est toujours clair et compact. Une suggestion mineure, je pense [je i, i-1) * i .. curmax] serait suffisant dans la deuxième ligne (de sorte que vous ne vérifiez pas à la fois un B et B * A) .


@tokland: Cela semble correct, mais comme vous l'avez remarqué, je imitatait la structure conceptuelle (plutôt que syntaxique) de la version Python, donc je laisserai le code tel quel. Bonne observation, cependant!



2
votes

Alors, pensant fonctionnellement, vous devriez rechercher des moyens de ne pas vous poser de problème de ne pas se lancer dans des boucles et des étapes, mais dans des fonctions.

Donc, si nous avions une fonction maxhere f xs code> qui a renvoyé le plus grand x code> pour lequel fx code> est vrai, nous pourrions écrire: p> xxx pré>

une implémentation naïf de maxe est p>

maxWhere f xs = foldl' r 0 xs
    where r a x
       | x > a     = if f x then x else a
       | otherwise = a


0 commentaires

1
votes

Ce type de boucle se prête facilement à une compréhension de la liste, comme ceci: xxx pré>

où nous pourrions écrire ispalindrome comme ceci: p> xxx pré> p> C'est vraiment assez rapide, bien que la sorte de peu demartypantsy, donc d'abord, nous remarquerons que nous vérifions les chiffres deux fois. Disons que a * b est le plus grand palindrome, alors nous vérifierons à la fois le cas où x == a, y == b code> et x == b, y == A code>. Nous arrêtons donc d'abord cela en limitant les chiffres que nous recherchons uniquement aux cas où x> = y, comme ceci: p> xxx pré>

ceci coupe les chiffres à tester en deux. P>

Dans votre solution Python, vous avez également lié à Y ci-dessous par le plus grand numéro que nous avons trouvé jusqu'à présent divisé par le X ( x * y => curmax code>), également que vous ne cherchez jamais au-delà du premier y trouvé (briser la boucle interne si Curmax est mise à jour). Nous pouvons réduire la recherche en ne poursuivant pas si le premier élément que nous vérifions (x carré) est inférieur à notre réponse actuelle, car toutes les vérifications suivantes sont plus petites, mais ceci est au-delà de ce qui semble bien dans une compréhension de la liste afin que nous déplacions notre recherche. C'est sa propre fonction: p>

*Main> search 999 0
906609


0 commentaires

0
votes

Comme indiqué par Stephen Tetley Strong> Dans son commentaire, vous pouvez utiliser le style de passage de continuation pour gérer le flux de contrôle complexe ( cont code> monad plus son callcc Code> qui est en quelque sorte similaire à pause code>. ... ou même goto code> - Abus de CPS peut conduire à un code plutôt incompréhensible - voir mon exemple ci-dessous):

import Control.Monad.Cont
import Control.Monad.State.Strict

solcs = runCont (execStateT comp 0) id
    where   
      comp = forM_ range $ \i -> callCC $ \break ->
                forM_ range $ \j -> do
                  let ij = i*j
                  m <- get
                  when (ij < m) (break ())
                  when (pal ij) (put ij)  


0 commentaires

0
votes

Je pense que vous pouvez faire ce que vous voulez utiliser deux fonctions mutuellement récursives.

Voici un exemple beaucoup plus simple (extrait de un tutoriel sur ATS ): xxx

Le code écrit ci-dessus est très similaire à ce que tu voudrais Ecrire en C (extrait de la même page):

int Main (int Argc, char * argv []) { int i, j; xxx

}

Comme vous le voyez, les boucles imbriquées deviennent des fonctions mutuellement récursives; et des variables mutables I et J Devenir des variables d'induction. LOOP1 correspond à la boucle extérieure, tandis que la boucle2 à la boucle interne.


0 commentaires