A 2025 egy szép szám, ami felírható úgy, mint az első 9 természetes szám köbének összege. Ezt az alábbi C nyelvű programmal könnyen tudjuk ellenőrizni:
#include <stdio.h>
int main() {
int n = 0;
for (int i = 1; i <= 9; ++i)
+= i * i * i;
n ("%d\n", n);
printf}
Nézzünk meg erre néhány megoldást C++-ban!
Első próbálkozásként ezt gyakorlatilag ugyanúgy meg tudjuk írni
C++-ban is: a nyelv (szinte) tartalmazza a C-t. Az alábbi megoldásban
csak a kiíratás módja más egy kicsit, bár igazából - ugyan nem szokás -
használhattuk volna a printf
-et is.
#include <iostream>
int main() {
int n = 0;
for (int i = 1; i <= 9; ++i)
+= i * i * i;
n std::cout << n << std::endl;
}
A C++, akárcsak az Objective-C, a 80-as években keletkezett, és a legnagyobb újdonság az volt benne, hogy támogatta az objektum-orientált programozást, ami akkoriban még csak néhány nyelvben volt meg, többek közt a Smalltalkban. A fenti program GNU Smalltalkban így nézne ki:
| n |
n := (Interval from: 0 to: 9) fold: [:sum :x | sum + (x raisedTo: 3)].
Transcript show: n printString; cr.
Először készít egy objektumot, ami a 0-tól 9-ig való
intervallumot reprezentálja, majd erre meghívja a fold
metódust, ami a paraméterként kapott műveletet (ami az első
paraméterhez hozzáadja a második köbét) berakja az egyes elemek
közé:
(..((0 + 1^3) + 2^3)..) + 9^3
Ezt megfogalmazhatjuk C++-ban is:
#include <iostream>
struct Interval {
int from, to;
int fold(int (*f)(int sum, int x)) {
if (from == to)
return from;
int result = f(from, from + 1);
for (int i = from + 2; i <= to; ++i)
= f(result, i);
result return result;
}
};
int addCube(int sum, int x) {
return sum + x * x * x;
}
int main() {
int n = Interval{0, 9}.fold(addCube);
std::cout << n << std::endl;
}
Ez még mindig nagyon közel van a C-hez, de látjuk, hogy a
struktúrához lehet metódusokat rendelni - ezek olyan függvények, amelyek
látják a struktúra adattagjait. Itt a fold
metódus egy
függvénypointert kap, és azzal a fent illusztrált módon kiszámítja az
eredményt.
Természetesen az objektum-orientált programozás nem csak a
metódusokról szól. Ha jobban szeretnénk követni a Smalltalk programot,
valójában egy tetszőleges típusú elemeken végigmenő
Iterable
sablon osztályhoz kellene rendelni a
fold
metódust, amit az Interval
örökléssel tudna specializálni… de ez túlmutat ezen a kis
bemutatón.
Egy másik nagyon kedvelt klasszikus paradigma a funkcionális programozás, ami a 2000-es évektől kezdve lassanként elkezdett beszivárogni a “mainstream” nyelvekbe is, és ma már teljesen általánossá vált.
A programnak egy lehetséges implementációja Haskellben az alábbi:
= putStrLn $ show n
main where n = sum $ map (^3) [1..9]
A végéről érdemes kezdeni. Az [1..9]
elkészít egy
listát, amelyben a számok szerepelnek 1-től 9-ig. A map
ezután a köbre emelést alkalmazza minden egyes elemre; ez a hatványozás
operátorból (^
) képzett parciális alkalmazásként
van megvalósítva. Végül az így kapott, köbszámokat tartalmazó listát
összegezzük.
Ugyanez (C++11-es szabványú) C++-ban így nézhet ki:
#include <algorithm>
#include <array>
#include <functional>
#include <iostream>
#include <numeric>
using namespace std::placeholders;
int power(int n, int k) {
int result = 1;
for (int i = 0; i < k; ++i)
*= n;
result return result;
}
int main() {
std::array<int, 9> xs, cubes;
std::iota(xs.begin(), xs.end(), 1);
std::transform(xs.begin(), xs.end(), cubes.begin(), std::bind(power, _1, 3));
auto n = std::accumulate(cubes.begin(), cubes.end(), 0);
std::cout << n << std::endl;
}
A power
függvény akár C is lehetne, kiszámolja
n
-nek a k
-adik hatványát. Ezt nyilván lehetne
ügyesebben is, de ehhez a példához most ez is megfelel.
Itt is az első lépés a számokat tartalmazó tömb elkészítése. A C-s
fix méretű tömb megfelelője a standard libraryban található
array
. Ez összefogja az adatpointert és a tömb méretét,
amit eddig külön voltunk kénytelenek használni. Mivel ez egy általános
(generikus) konstrukció, sablonparaméterekkel mondjuk
meg, hogy most mi 9 db. int
értékre akarjuk használni. A
iota
függvény feltölti ezt az elejétől a végéig, 1-től
kezdve növekvő számokkal.
A transform
végigmegy a megadott intervallumon (ami
jelen esetben az xs
tömb az elejétől a végéig), minden
elemre meghív egy függvényt, és az eredményt bepakolgatja a megadott
helytől kezdve folyamatosan. Ez a hely most a cubes
nevű
tömb eleje, a függvény pedig itt is a hatványozás parciális alkalmazása:
a bind
segítségével lekötjük a második paramérert, hogy
mindig 3 legyen.
Végül már csak össze akarjuk adni a köbszámokat. Ehhez az
accumulate
-et használjuk, ami egy adott kezdőértékhez (itt
0) hozzáad minden elemet.
A tömb-alapú nyelvek már a 60-as években megjelentek, kezdve az APL-el. Ezek lényege, hogy az alapvető típus a tetszőleges dimenziójú tömb, és a függvények ezek különböző szintjein értelmezhetőek (tehát például az összeadás működik számokra, vektorokra, mátrixokra stb., vagy akár egy többdimenziós mátrix almátrixaira is).
Ez az ötlet bekerült a MATLAB-ba és a statisztikában gyakorta használt R nyelvbe is. Az utóbbi években látszik egy nyitás ebbe az irányba a “mainstream” nyelveknél is.
Nézzük meg a problémánk APL megoldását:
+/3*⍨⍳9
)OFF
A ⍳9
(olvasd: iota 9, innen a fenti iota
függvény neve) elkészíti a számokat 1-től 9-ig. A *
operátor a hatványozás, általánosan n*k
az
n
-nek a k
-adik hatványa. A ⍨
viszont megfordítja az előtte levő operátor paramétereinek sorrendjét,
ugyanezt tehát k*⍨n
alakban is felírhatjuk.
A 3*⍨⍳9
jelentése tehát annyi, hogy a 3*⍨
(köbre emelés) függvényt az 1-től 9-ig való számokat tartalmazó vektorra
alkalmazzuk.
Végül a /
az előtte levő operátort a
fold
-hoz hasonlóan berakja a vektor elemei (mátrix sorai
stb.) közé. Ez az operátor itt az összeadás, a +/
tehát a
(köböket tartalmazó) vektor összegét adja.
Ennek megfelel nagyjából az alábbi (C++23 szabványt alkalmazó) megoldás:
#include <algorithm>
#include <functional>
#include <iostream>
#include <ranges>
int power(int n, int k) {
int result = 1;
for (int i = 0; i < k; ++i)
*= n;
result return result;
}
int main() {
auto r =
std::ranges::iota_view(1, 10)
| std::views::transform([](int k) { return power(k, 3); });
int n = std::ranges::fold_left(r, 0, std::plus<int>());
std::cout << n << std::endl;
}
A power
függvény nem változott. A iota_view
elkészíti az 1-től 9-ig való intervallumot, majd a |
(olvasd: pipe) operátor ennek az eredményét összeköti a
transform
függvénnyel, ami hasonló az előző verzióban
látotthoz, azonban itt nem egy parciális alkalmazást adunk át, hanem egy
anonim (lambda) függvényt: ez egy olyan függvény, amit csak itt
használunk, ezért még nevet sem adunk neki.
Az így kapott r
objektum valójában nem tartalmazza a
köbszámokat, csak “tudja”, hogy hogyan kell kiszámolni őket - ezt
lusta kiértékelésnek hívják, és ilyen szempontból ez közelebb
áll a fenti Haskell megoldáshoz.
A fold_left
a korábbi accumulate
általánosítása: a 0 kezdőértéken kívül átadjuk még az elvégzendő
műveletet is, ami itt az összeadás.
Ahogy a fentiekből látszik, a C++ nyelv nagyon sokat változott az elmúlt évtizedek alatt (kissé a szintaxis kárára), és eközben magába szívta az éppen népszerű paradigmákat is, így egy-egy problémát sokféle módon meg lehet oldani.