Já osobně jsem se poprvé s lambda funkcemi setkal ještě za svých studentských časů na mé Ostravské alma mater, kdy jsem byl hluboce zanořen do samostudia jazyka Scala. Tehda to bylo taky poprvé, co jsem se začal seznamovat se základními koncepty funkcionálního programování, které mi přišly zcela mind-blowing. Krom toho taky mnoho featur Scaly byly mind-blowing samy o sobě a už toho času jsem měl takový ten pocit, který mi říkal: “Jó, tak takhle bude možná Java vypadat za 3 roky”. Trošku jsem se přecijenom zmýlil, protože Java 8 přišla až za 4 roky a některé další featury ze Scaly se plánují snad až do Javy 10. Dnes už je to 2 roky, co máme lambdy a Stream API k dispozici a přesto (k mému údivu) narážím až na překvapivě velké množství Javistů, kteří je buď nepoužívají, anebo ani neví, co to vlastně je. Ba co více, nedávno jsem narazil i na kolegy, kteří mi vyloženě tvrdili, že lambdy jsou hnus a že nechápou, jak do Javy mohl někdo něco takového dát. Právě tento názor, který mě upřímně docela dostává, mě motivoval napsat tento článek, ve kterém se vám pokusím zběžně ukázat, proč jsou lambdy boží a proč zřejmě ten nejpodstatnější důvod, proč si zvolit jakýkoliv jiný jazyk než Javu, je již od verze 8 nenávratně passé.

Proč říct lambda funkcím ano

Dokázal bych najít 2 nejdůležitější důvody (znáte ještě nějaké jiné?), proč lambdy vůbec existují a proč jsou vlastně tak skvělé. Pro jednoduchost postačí, když si zapamatujete, zaprvé, že nezanedbatelným způsobem zkracují a zpřehledňují kód, a zadruhé, umožňují programátorovi přemýšlet v trochu hlubší rovině abstrakce, než je u OOP zvykem. První důvod, je asi ten nejdůležitější a každý, kdo se chce nazývat dobrým programátorem, zkrátka musí vědět, jak lambdy takovým způsobem využít. Druhý důvod je už tak trochu vyšší dívčí a je spíše pro ty z vás, pro které je programování opravdu koníčkem a kteří toužíte být naprostou špičkou ve svém oboru. Konec konců, druhý důvod nakonec vede k důvodu prvnímu, ale to si brzy ukážeme.

Úspora kódu

Příkladů by se dalo najít hodně, např. Streamy bez lambd by byly zcela nepoužitelné, ale zkusme si ukázat, že lambdy se dají použít i na mnoha jiných místech, než kde jsou běžně prezentovány. Mějme tento kód:

    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello world!");
        }
    }).start();

Jinými slovy, chceme si pustit nový thread a v něm si něco vypíšeme. A teď pozor, uděláme malé kouzlo. Ti, kteří lambdy vidí poprvé, mi možná nebudou věřit, že zcela ekvivalentní kód se dá zapsat i takto:

    new Thread(() -> System.out.println("Hello lambda world!")).start();

1 řádek kódu oproti 6ti, tomu říkám docela slušná redukce. Většinou v praxi faktor redukce kódu nebude až tak vysoký, ale je třeba si uvědomit, že to, v čem lambda funkce excelují, jsou především krátké funkce. Lambda funkce s mnoha řádky, mohou být v některých případech nepřehledné, kde se vyplatí napsat si metodu zvlášť a pak na ní použít referenci pomocí :: operátoru.

Již jen u tohoto příkladu si odvažuji říct, že pokud někdo tvrdí, že druhá varianta je oproti první hnusná a nečitelná, je to jakoby mi nějaký potenciální východněji umístěný programátor řekl, že raději bude psát 100 řádkové funkce, protože čistý OOP design je příliš abstraktní a nesrozumitelný 🙂 . Tedy pro něho ano, protože se ho ještě nenaučil. Tzn. domněnka, že lambda funkce jsou zlo, není dle mého názoru validní argument, ale spíše známka jakési mezery v programátorském vzdělání. K čemuž se nedá říct nic jiného než: Tak na co ještě kurňa čekáte? Otevřete si nějaký fajn tutoriál a rubejte to tam! 🙂

Nesrozumitelné stack tracy

Další stížnost na lambda funkce, kterou jsem od kolegů slyšel, jsou nesrozumitelné stack tracy. Ve chvíli, kdy tento článek píšu, z hlavy nevím, co přesně očekávat, a tak pojďme to tedy zkusit a uvidíme:

package com.svetylkovo.lambdasareawesome;

public class LambdasTest {

    public static void main(String[] args) {
        new Thread(() -> {
            String nullString = null;
            nullString.toUpperCase();
        }).start();
    }
}

Po spuštění tohoto kódu dostaneme (záměrně) NullPointerException, kde stack trace vypadá takto:

Exception in thread "Thread-0" java.lang.NullPointerException
	at com.svetylkovo.lambdasareawesome.LambdasTest.lambda$main$0(LambdasTest.java:8)
	at java.lang.Thread.run(Thread.java:745)

Z “at com.svetylkovo.lambdasareawesome.LambdasTest.lambda$main$0(LambdasTest.java:8)” vyplývá, že bych se měl podívat do souboru LambdasTest.java na řádek 8. A když se tam podívám, tak vidím právě ten řádek, na kterém problém nastal, čili: nullString.toUpperCase();

Takže tady bych problém rozhodně neviděl. Zkusme to ještě jednou, tentokrát (zřejmě) horší variantu se Streamy:

package com.svetylkovo.lambdasareawesome;

import java.util.Arrays;
import java.util.stream.Collectors;

public class LambdasTest {

    public static void main(String[] args) {
        Arrays.asList(1, 2, 3, null, 4).stream()
            .map(Object::toString)
            .collect(Collectors.toList());
    }
}

nyní jsme dostali:

Exception in thread "main" java.lang.NullPointerException
	at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
	at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
	at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
	at com.svetylkovo.lambdasareawesome.LambdasTest.main(LambdasTest.java:11)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

Zde je již jediné vodítko “at com.svetylkovo.lambdasareawesome.LambdasTest.main(LambdasTest.java:11)”, které nás dovede na poslední řádek Streamu, kde voláme collect. Tady již přesně nevíme, na kterém řádku nastala ona chyba a uznávám, že zrovna v tom jsou Streamy/lambdy matoucí. Nicméně záchranou může být problematickou lambdu vyextrahovat jako metodu a použít na ni referenci:

package com.svetylkovo.lambdasareawesome;

import java.util.Arrays;
import java.util.stream.Collectors;

public class LambdasTest {

    public static void main(String[] args) {
        Arrays.asList(1, 2, 3, null, 4).stream()
            .map(LambdasTest::toString)
            .collect(Collectors.toList());
    }

    public static String toString(Object o) {
        return o.toString();
    }
}

Teď už nás stack trace dovede přímo na řádek 15, což je náš return o.toString();

Vyšší dívčí

Jakožto zástupce vyššího dívčího bych zvolil pattern funkce, která vrací další funkci (kde těch zanoření by mohlo být samozřejmě i více). Pokud vám samotné lambdy přijdou komplikované, tak teprve teď si připravte mozkové závity, protože se na chvíli půjdeme podívat za hranice naší Sluneční soustavy :-). Avšak nyní si od Javy trochu odpočineme a přesuneme se k Javascriptu, k naprosto úžasné knihovně, která se nazývá ReactJS. V Javě jsem toto jedno zanoření použil myslím jen párkrát, ale už si nevzpomenu kde. Kdežto v Reactu se mi podařilo objevit docela dost dobrý pattern, který funguje výborně pro two way data binding, anebo i pro handlování událostí, které např. stejným způsobem jen upravují různé fieldy statu vaší komponenty. Mimochodem dvojí zanoření jsem viděl snad jen v nějakých tutoriálech o Reduxu, ale jedno je dle mého názoru až až 🙂 . Živý kód nyní nemám k dispozici, a tak to pojďme jen pro ukázku napsat z hlavy, ať je zřejmé, co mám na mysli:

export default class FancyName extends Component {
    state = {
        firstName: '',
        sirName: ''
    }
    
    handleChange = paramName => evt => this.setState({ [paramName]: evt.target.value })

    render() {
        return (
            <div>
                My name is {this.state.firstName} {this.state.sirName}
                <input 
                    value={this.state.firstName} 
                    onChange={this.handleChange('firstName')} 
                />
                <input 
                    value={this.state.sirName} 
                    onChange={this.handleChange('sirName')} 
                />
            </div>
        )
    }
}

Když se nad tím zamyslíme … co po mně onChange={…} chce? Chce po mně nějakou funkci, která má jako parametr event. A jediné, co s tím chci udělat, je uložit hodnotu, kterou ten event v sobě má, do nějakého fieldu ve state mé komponenty. Avšak pro každý input je to jiný název fieldu. V této chvílí se nám parádně hodí nechat si takovou funkci “vygenerovat” pomocí jiné funkce, které řeknu, jaký field to má být, a máme hotovo! Představte si, že byste těch inputů měli 5 nebo 10. Bude se vám chtít dělat 10 samostatných handle-metod pro každý z nich, anebo půjdete trochu do hloubky a zfouknete to pouze jedinou? 🙂 A přesně v tomto tkví kouzlo funkcionálního konceptu programování …