Yield a generátory: protože načítat vše najednou je jako jíst celý dort na posezení
Toto je druhý díl série! První najdeš zde.
Představ si, že máš funkci, která vrací prvních milion čísel Fibonacciho posloupnosti. Bez yield by tato funkce musela nejdříve vygenerovat všech milion čísel, uložit je do paměti a teprve pak ti je předat. To je jako kdyby sis před snídaní uvařil celý hrnec polévky — jen proto, že si chceš dát jedno sousto.
Yield říká: "Dám ti jedno číslo. Až ho zpracuješ, zavolej mě a dám ti další." Data se generují postupně, jen tehdy, kdy jsou potřeba. Paměť se netlačí, aplikace je rychlejší a ty jsi spokojenější.
Pojďme se tedy podívat, jak si s tímto konceptem poradí různé jazyky — na příkladu Fibonacciho posloupnosti. Zadání je jednoduché: nekonečný generátor, který vydává Fibonacciho čísla jedno po druhém. Vypisovat budeme všechna čísla menší než 100.
1. C++
C++ coroutines přišly až v C++20 a upřímně — bylo by lepší, kdyby ještě chvíli počkaly.
#include <coroutine>
#include <iostream>
struct Generator
{
struct promise_type
{
long long current_value;
Generator get_return_object()
{
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(long long value)
{
current_value = value;
return {};
}
void return_void() {}
void unhandled_exception() {}
};
std::coroutine_handle<promise_type> handle;
Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
long long next()
{
handle.resume();
return handle.promise().current_value;
}
};
Generator fibonacci()
{
long long a = 0, b = 1;
while (true) {
co_yield a;
auto tmp = a + b;
a = b;
b = tmp;
}
}
int main()
{
auto gen = fibonacci();
long long n = gen.next();
while (n < 100) {
std::cout << n << " ";
n = gen.next();
}
return 0;
}
Jak vidíš, tak C++ coroutines vyžadují celou pomocnou strukturu Generator s vnořeným promise_type, aby vůbec mohly
fungovat. Samotná funkce fibonacci() pak vypadá relativně rozumně, ale ta infrastruktura kolem... Ta je přímý
kandidát na místo v muzeu hrůzy. A to vše jen proto, aby C++ mohlo říct, že coroutines podporuje.
Přehlednost: 2/10
Počet řádků: 57
2. C#
using System;
using System.Collections.Generic;
class Program
{
static IEnumerable<long> Fibonacci()
{
long a = 0, b = 1;
while (true)
{
yield return a;
(a, b) = (b, a + b);
}
}
static void Main()
{
foreach (long n in Fibonacci())
{
if (n >= 100) break;
Console.Write(n + " ");
}
}
}
C# to zvládá elegantně. yield return je čitelné, smyčka je přirozená a nikde žádné hieroglyfy. Podmínka
ukončení (if (n >= 100) break) je přirozená součást smyčky — čteš ji jako větu: "pro každé n ve Fibonacci,
pokud je n větší nebo rovno 100, skonči." Narozdíl od minulého dílu zde si C# vede lépe a není tak ukecaný.
Přehlednost: 8/10
Počet řádků: 24
3. Java
Java nativní generátory nemá. Žádný yield, žádné coroutines. Nejblíže, co Java nabízí, je implementace
rozhraní Iterator — tedy ručně si napsat vše, co by za tebe yield udělal automaticky.
import java.util.Iterator;
class Main
{
public static void main(String[] args)
{
FibonacciIterator fib = new FibonacciIterator();
long n;
while ((n = fib.next()) < 100) {
System.out.print(n + " ");
}
}
}
class FibonacciIterator implements Iterator<Long>
{
private long a = 0;
private long b = 1;
@Override
public boolean hasNext()
{
return true;
}
@Override
public Long next()
{
long value = a;
long tmp = a + b;
a = b;
b = tmp;
return value;
}
}
Funguje to. Ale za tu cenu? Musíš napsat celou třídu, implementovat dvě metody (hasNext a next) a ručně
udržovat stav mezi voláními. Přesně tohle dělá yield automaticky v ostatních jazycích. Java říká:
"Chceš generátor? Vybuduj si ho sám."
Přehlednost: 5/10
Počet řádků: 35
4. Rust
Rust také nemá yield ve stable verzi. Místo toho používá implementaci Iterator traitu — iterátory jsou
v Rustu first-class citizen. Na papíře to zní dobře. V praxi to vypadá takhle:
struct Fibonacci
{
a: u64,
b: u64,
}
impl Fibonacci
{
fn new() -> Self
{
Fibonacci { a: 0, b: 1 }
}
}
impl Iterator for Fibonacci
{
type Item = u64;
fn next(&mut self) -> Option<u64>
{
let value = self.a;
let tmp = self.a + self.b;
self.a = self.b;
self.b = tmp;
Some(value)
}
}
fn main()
{
let mut fib = Fibonacci::new();
while let Some(n) = fib.next() {
if n >= 100 { break; }
print!("{} ", n);
}
}
Kde začít. fn místo function, &mut self (reference na měnitelný ukazatel na sebe sama — co?),
Option<u64> protože Rust nikdy nevrátí jen číslo, vždy ho zabalí do Option, aby se nemuselo řešit
null. To jsou tři věci na jednom řádku, které vyžadují znalost Rustu, aby dávaly smysl.
Navíc — podmínkové ukončení iterace (while let Some(n) = fib.next()) je idiomatické, ale pro někoho
zvenčí vypadá jako překlep. Srovnej si to s Pythonem: while True: yield a. Jasné, přirozené, pochopitelné
bez manuálu.
Přehlednost: 3/10
Počet řádků: 36
5. JavaScript
function* fibonacci()
{
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const gen = fibonacci();
let result = gen.next();
while (result.value < 100) {
process.stdout.write(result.value + " ");
result = gen.next();
}
JavaScript generátory označuje hvězdičkou za klíčovým slovem function*, což je trochu zvláštní, ale
rychle si na to zvykneš. Samotný yield je pak přirozený a čitelný. Nevýhodou je, že k získání hodnoty
musíš volat gen.next().value, což je o poznání méně elegantní než prosté next(gen) v Pythonu.
Přehlednost: 8/10
Počet řádků: 15
6. Python
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
gen = fibonacci()
n = next(gen)
while n < 100:
print(n, end=" ")
n = next(gen)
Python. Jasné, stručné, čitelné. yield prostě funguje a kód vypadá přesně tak, jak by jsi ho popsal
slovně. Funkce next(gen) je přirozená a intuitivní. Python si tady nezaslouží žádnou kritiku — udělal vše
správně. Jediná nevýhoda je volání n = next(gen) na dvou místech.
Přehlednost: 10/10
Počet řádků: 11
7. PHP
<?php
function fibonacci(): Generator
{
$a = 0;
$b = 1;
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
$gen = fibonacci();
do {
echo $gen->current() . " ";
} while ($gen->send(null) < 100);
PHP má yield podobně jako Python, ale generátor se chová jako objekt. Zajímavostí je metoda send(),
která do generátoru zároveň pošle hodnotu, posune ho na další yield a vrátí nově vytvořenou hodnotu —
všechno najednou. To nám umožňuje elegantní do-while: vypiš aktuální hodnotu, posuň generátor a zkontroluj,
jestli nová hodnota ještě splňuje podmínku. Navíc PHP dovoluje označit návratový typ jako Generator,
což pomáhá IDE i kolegům pochopit, co funkce vrací.
Přehlednost: 8/10
Počet řádků: 14
Závěr
| Jazyk | Přehlednost | Počet řádků |
|---|---|---|
| C++ | 2/10 | 52 |
| C# | 8/10 | 23 |
| Java | 5/10 | 32 |
| Rust | 3/10 | 37 |
| JavaScript | 8/10 | 15 |
| Python | 10/10 | 10 |
| PHP | 8/10 | 15 |
Vítěz v přehlednosti je jednoznačně Python — jednoduché, přirozené a bez zbytečností. Těsně za ním
se dělí C#, JavaScript a PHP, které zvládají yield taky velmi slušně.
V počtu řádků vede taktéž Python, následovaný JavaScriptem a PHP. Na druhém konci žebříčku
straší C++ s hromadou řádků a celou konstrukcí promise_type, kterou musíš napsat jen proto,
aby coroutines vůbec fungovaly.
Java a Rust si zaslouží zvláštní zmínku — jako jediné jazyky v tomto srovnání nemají yield vůbec
a musíš generátor emulovat ručně. Rozdíl je v tom, že Rust to dělá idiomaticky a elegantně, zatímco Java
říká prostě: "Chceš generátor? Vybuduj si ho sám."
Tak užívej a já jdu generovat poslední Fibonacciho číslo.