Skip to content

Instantly share code, notes, and snippets.

@v6ak
Last active August 29, 2015 14:20
Show Gist options
  • Save v6ak/9a7edb5a7de5a5be9b54 to your computer and use it in GitHub Desktop.
Save v6ak/9a7edb5a7de5a5be9b54 to your computer and use it in GitHub Desktop.

Tak ukážu příklad v jazyce Scala. Nedávno jsem psal nějaké zpracování akordama označkovaného textu a konverzi do PDF. Controller by v imperativní verzi (tj. s callbacky) mohl vypadat zhruba takto:

def processChordbook(songText: String) {
	convertToPdf(songText).onSuccess{ pdfData =>
		setHeader("Content-type"->"application/pdf")
		out.write(pdfData)
		out.close()
	}
}

To je podobné tomu, co píšeš (s malým rozdílem, že tady používám Future jako mezikrok), a mělo by to být dobře funkční. I když… To vlastně není dobře. Pokud by nastala chyba, uživatel se nedozví ani nějaké internal server error, místo toho bude čekat na nějaký timeout. Správně by to mělo být asi takto:

def processChordbook(songText: String) {
	convertToPdf(songText).onSuccess{ pdfData =>
		setHeader("Content-type"->"application/pdf")
		out.write(pdfData)
		out.close()
	}.onError{ e =>
		reportError(e)
		out.write(createErrorPage(e))
		out.close()
	}
}

Jak by ale mohla vypadat funkcionálnější verze? Metoda by vracela Future[HttpResponse]. Uvnitř by nejdřív zavolala convertToPdf(songText), která by vrátila Array[Byte] a její výsledek by následně konvertovala do HttpResponse (skrze metodu map by se vytvořila Future[HttpResponse]) a vrátila ven:

def processChordbook(songText: String) = {
	convertToPdf(songText).map{ pdfData =>
		Ok(pdfData).withHeaders("Content-type"->"application/pdf")
	}
}

To je v nejjednodušší verzi celé a příklad zhruba odpovídá API, které nabízí Play! framework. Na funkci convertToPdf není zvenku skoro nic imperativního, vrací nějakou budoucí hodnotu (tj. Future[_]) a nijak nemění okolní stav. Jak jsem již komentoval, na Future se taky můžeme z jistého úhlu abstrakce dívat funkcionálně (pokud nebudeme volat metody jako isCompleted). Na volání metody map toho není (navenek) moc imperativního, prostě pomocí čisté funkce F => T vytvořím ze staré Future[F] novou Future[T]. Starou Future[F] nijak nezničím, mohu ji použít znovu.

Malou vadou na funkcionální kráse je, že metoda map chce taky nějaký ExecutionContext, což beru jako nutné zlo, aspoň dokud někdo nevymyslí něco lepšího. (Prakticky mi to nijak nepřekáželo, typicky importuju nějaký obecný ExecutionContext, kompilátor ho tam napasuje automaticky skrze implicitní parametr a já nic neřeším.) Obávám se ale, že narážíme na nějaký tradeoff a pokud bychom se chtěli odstínit od faktu, že máme nějaká vlákna, zbavili bychom se možnosti efektivně řešit paralelizaci apod.

Příjemný vedlejší účinek tohoto funkcionálního přístupu je, že nemusím řešit chyby, ke kterým nemám nějaké speciální zpracování a jen bych je chtěl probublat do internal server error. Ty totiž do výsledné Future[HttpResponse], která vznikla metodou map, probublají automaticky. Možná tady už trošku odbočím od původní otázky, ale můžu i explicitně zpracovat některé druhy chyb a zbytek nechat probublat:

def processChordbook(songText: String) = {
	convertToPdf(songText).map{ pdfData =>
		Ok(pdfData).withHeaders("Content-type"->"application/pdf")
	} recover {
		case e: FormatException => Ok(Txt(e.getMessage))
	}
}

Můj skutečný controller se liší ještě v dalších věcech, jako že třeba řeším případné timeouty nebo postupné streamování odpovědi, což není problém tady dodělat, ale to už jsou spíš detaily z tohoto pohledu. Mimochodem, ani streamování není v rozporu s funkcionálním přístupem, když se udělá vhodně.

Vím, dá se namítnout, že ani tento způsob není 100% funkcionální. Ale funkcionální vs. imperativní není černobílé. Už C bylo oproti ASM jistý posun, protože umožňuje psát výrazy jako c*c == a*a + b*b funkcionálně, aniž by člověk řešil registry. Podobně tady jsme udělali jistý posun od imperativních callbacků k map a čistým funkcím, které transformují jednu Future do druhé. Z několika důvodů nevěřím, že 100% funkcionální pohled bude široce užitečný – pokud budeme chtít řešit takové požadavky jako obsluhu timeoutů, side channels nebo vhodnou paralelizaci, těžko se oprostíme od toho, že výpočty probíhají v nějakém čase a musí se na něčem ve vhodnou chvíli vykonat. (Mohl bych rozebírat, jaký problém je zkombinovat čistě funkcionální přístup s paralelismem, ale to by bylo možná trošku OT.) Funkcionální přístup je holt dobrý sluha, ale zlý pán.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment