Tandis que la récursivité "classique" est réductrice :
fact n | n==0 = 1 -- e finita la commedia
| otherwise = n*fact(n-1)
la co-récursion, ou apomorphisme, ne demande pas la terminaison (au contraire...), mais chaque pas doit faire progresser l'algorithme.
fact n = rfact!!n where (!!) indexe la liste : rfact[n] rfact = rf 0 1 where rf n r = r : rf (n+1) (r*(n+1))
rfact
est une "liste infinie", implémentable car ses éléments sont créés au moment de leur usage, quand on demande leur valeur (call by need).
Si une "publicité" de Haskell, affirme qu'un langage paresseux peut opérer avec des listes infinies (comme si quelqu'un en avait besoin)...
Ce n'est pas ça... L'enjeu ici est différent.
Le codage "traditionnel", la liste : \(\c y=[\sin(x_0), \sin(x_0+\omega h),\) \(\c \sin(x_0+2\omega h), \ldots]\,\), est possible, mais la Nature (une corde de guitare, une flûte, Maria Callas, etc.) ne marche pas comme ça, un oscillateur en hardware n'a pas d'horloge qui mesure le temps (l'argument du sinus), et il s'arrête pour des raisons extérieures.
Il y a longtemps, j'ai présenté ici des modèles physiques (guides d'onde) d'instruments musicaux, implémentés comme des flots paresseux, co-récursifs. Le plus simple oscilateur monochromatique, est la solution de l'équation :
\(\c y'' = - \omega^2 y\), ce qui se réduit à deux équations d'ordre 1 : \(\color{ora}y' = \omega u\) et \(\c u' = -\omega y\). Leur solution par la méthode d'Euler modifiée et stable s'implémente de manière aussi courte :
$$
\c u_{n+1}=u_n -\omega h y_{n}\, \quad y_{n+1}=y_n + \omega h u_{n+1} \,
$$
mais dont le codage sera (par ex.; autres formes sont possibles)
y = y0 : ((1-(om*h)^2)*>y + om*h*>u) where
u = u0 : (u - om*h*>y) -- *> mult. flot par une const.
Ici y est une liste Haskell, une structure de données qui facilite les opérations, comme l'arithmétique, les preuves (co)-inductives de leurs propriétés, la bi-simulation, etc. D'autres modèles de flots existent : Streams, Conduits (similaires aux générateurs Python), etc., et tout ceci progresse, mais restons élémentaires. Les flots peuvent être manipulés itérativement, par ex. ajoutés élément par élément, soumis à des fonction, etc., et ces opérations souvent libèrent le programmeur d'indexer explicitement les éléments, et éliminent les boucles.
Tout ceci simplifie le codage et la présentation des algorithmes.
Un flot \(\c u_0, u_1, u_2, \ldots \leftarrow (u_0 :> \bar{u}) \) peut représenter une série infinie formelle : \(\c u_0 + u_1 x + u_2 x^2 + \cdots \leftarrow (u_0 + x \bar{u}) \), et de telles séries sont l'essence même de plusieurs calculs de perturbation. Leur manipulation est étonnement simple. ((+)
et (-)
sont triviaux, "zips".)
(u0 :> uq) * v@(v0 :> vq) = u0*v0 :> u0*>vq + v*uq
(u0 :> uq) / v@(v0 :> vq) = w0 :> wq where
w0=u0/v0; wq = (uq - w0*>vq)/v
(v@expr nomme une expr. réutilisée, (:>)
a le même sens que (:)
).
exp u@(u0:>_) = w where -- exp(u)'=exp(u)*u'
w = integ (exp u0) (w * diff u)
On peut aussi opérer avec des flots - expressions arithmétiques ensemble avec toutes leurs dérivées : [v, v', v'', ...] par rapport à une "variable", qui est également un flot : [v, 1, 0, 0, ...], tandis que les constantes avaient la forme [v, 0, 0, ...].
La différentiation est une trivialité structurelle : df (e0:eq) = eq .
e@(e0 :- ep) * f@(f0 :- fp) = (e0*f0 :- ep*f + fp*e) e@(e0 :- ep) / f@(f0 :- fp) = (e0/f0 :- ep/f - e*fp/(f*f)) exp e@(e0:ep) = r where r = exp e0 :- (ep*r)etc. (l'op.
(:-)
est pour eviter la confusion avec d'autres listes).
exp u@(u0:-uq) = w where w = exp u0 :- uq*w log u@(u0:-uq) = log u0 :- uq/u sqrt u@(u0:-uq) = w where w = sqrt u0 :- 0.5*>(uq/w)Ceci permet d'implémenter numériquement une fonction définie par des récurrences différentielles, par ex., Hermite :
w = 0 :- exp(-w)/(1+w)
. [0, 1, 2, 9, 64, 625, ...]
.
dfs u = map df u
.
Alors le codage est la transcription mécanique des formules analytiques ci-dessus :
s0p = sqrt q; s0b=df s0p up = (-0.5)*>(s0b :> dfs vp) / (s0p :> vp) vp = p where p=((-0.5)/s0p) *> (up^2 + dfs up +> p*p) (a0:>aq) +> b = a0 :> (aq+b)(L'opération \( \c a +> b \) signifie \(\c a+ \epsilon^2 b\)).
Il s'agit de résoudre l'équation de Schrödinger
\(\c (H_0 + \lambda V)\ket{\psi} = E \ket{\psi}\), où \(\c \lambda\) est un petit paramètre modifiant le Hamiltonien (opérateur d'énergie) ; le point de départ est : \(\c H_0\ket{\psi^{(0)}_k} = E^{(0)}_k \ket{\psi^{(0)}_k}\). Ici \(\c \ket{\psi}\) dénotent les états quantiques, des vecteurs typiquement infiniment-dimensionnels (ça commence bien...), et \(\c E\) sont les énergies que l'on cherche.
Présentons un simple modèle archétypique, dont les variantes sont très importantes : l'oscillateur anharmonique.
On connait la solution non-perturbée, les états d'un oscillateur harmonique. Il est le plus important système quantique de tous, c'est la base de presque tous les systèmes périodiques, et de la théorie des champs en particulier : les niveaux quantiques (d'excitation) sont énumérés : 0, 1, 2, ... N, ... et ces valeurs dénotent les nombres de quanta (photons, phonons, ou autres Schtroumpfs).
Les états appropriés sont notés comme \(\c \ket{0}, \ket{1}, \ket{2},\ldots\).
ebar = v 0 0 :> sum [v 0 k *> psi k | k <- [2,4]] psi k | k>0 = (v k 0 :> sum [v k m *> psi m | m <- [k-4,k-2 .. k+4]]) / ((-k):>ebar) | otherwise = 0La valeur de
ebar
est la série : [0.75, -2.625, 20.8125, -241.289, 3580.98, -63982.813 ...]
.
v k m
\(\, = \c \avg{k}{V}{m}\) ; \(\c V=x^4\).
cok k m | k>=0 && m>=0 && k==m = 1 | otherwise = 0alors l'application partielle
cok k
représente \(\c \bra{k}\), et une autre fonction, qui agit sur les premières : ket n = \f -> f n
, est \(\c \ket{n}\). Il est facile de prouver que ce sont des vecteurs, comme des fonctions dont le codomaine est numérique : \(\c(f+g)x = f x + g x\), mais de plus, les kets sont des fonctions linéaires. On a aussi (ket n)(cok m)
\(\, = \c\braket{m}{n}\).
<+>
), multiplication par nombres (c#f)
: \(\c (cf)x=c(f x)\), etc. via applications jusqu'à la réduction aux nombres. Passons aux opérateurs agissant sur les kets, ou les co-kets ("bra"s). Nous aurons besoin surtout de l'algèbre de Weyl, des opérateurs \(\c a, a^{+}\) :
ann_C cq n = sqrt n * cq (n-1) cre_C cq n = sqrt (n+1) * cq (n+1) ann = boost ann_C cre = boost cre_C -- où (par dualité) : boost f g x = g (f x)La seule chose qui reste, c'est appliquer le formalisme à la théorie de l'oscillateur, où on apprend que \(\c H_0 = a^{+}a\), et que \(\c x = \frac{1}{\sqrt{2}}(a+a^{+})\). Chez nous :
v k m = (x . x . x . x) (ket m) (cok k) where x = (sqrt 0.5) # (ann <+> cre)Le code Haskell et la présentation symbolique, sont très similaires.
psi k | k>0 = lpsi !! k -- indexation, lpsi[k] | otherwise = 0 lpsi = 0 : ps 1 where ps k = ((v k 0:>sum[v k m*>psi m|m<-[k-4,k-2..k+4]])/ ((-k) :> ebar)) : ps(k+1)La gestion de mémoire des programmes paresseux demande parfois de beaucoup de discipline et de connaissance de quelques "astuces du métier".
p = 1 : p^5
, et la solution est 1, 1, 5, 35, 285, 2530, 23751, 231880, 2330445, ... Mais un polynôme de degré 5 a 5 racines ; notre solution en donne une seule.
q0=1
. Afin d'obtenir \(\c q(\eta)\), nous allons inverser fonctionnellement
\(\c \eta(r) = r - r^5\), où r=[1,1]
: r-r^5 = [0,-4,-10,-10,-5,-1,0,...]
. q = srev (r-r^5)
, où :
srev (_ : v1 : vd) = sscale (1/v1) t where vx = (1/v1)*>vd; t = 0 : m; m = 1 : (-m*m*scomp vx t) sscale c (u0 : uq) = u0 : sscale c (c*>uq) -- u(x)->u(c*x) scomp u (_ : vq) = sc u where -- Composition des séries sc (u0 : uq) = u0 : vq * sc uq
q0=1
est mis à la main).
q=integ 1 (1/(1-q^4))
...
hd
), car tout le reste disparaît avec \(\c J=0\). C'est tout.
g2 = map hd (map tl phi) where
phi = (0 :- 1) :> (1%2)*>(dfs phi + phi^2)
ce qui donne :
$$\c
g_2 = 1 + \gamma^2 + \frac{25}{8}\gamma^4 + 15 \gamma^6 + \frac{12155}{128}\gamma^8
+ \frac{11865}{16}\gamma^{10} + \cdots
$$
ptan = -srev (0 :> (-3*>v)^(1/3)) where x=0.0:>1; _:>_:>_:>v = x - tan xdont la valeur : $$\c\small{ -x + \frac{2}{15}x^3 - \frac{3}{175}x^5 + \frac{2}{1575}x^7 + \frac{16}{202125} x^9 - \frac{362}{9384375}x^{11} \\ \c + \frac{49711}{12415528125} x^{13} + \cdots} $$