Функціональний стиль на Java

Сьогодні простий пост, ідеальний, щоб почитати зранку, поки ви п’єте каву. Ніяких складних концепцій (напевно), тільки мій досвід у формі потоку думок, який ніжно порізаний на абзаци для вашого прискіпливого читання.


Я декілька років працюю джавістом, тобто всякого побачив. Різні проекти, різні технології, але джава завжди мене супроводжувала, хоч з неї я не починав свою подорож у світі інженерії. Незважачаючи на те, що джава не була моєї першої мовою, я однаково мислю в її парадигмі, коли мені потрібно розв’язати задачку. В ній є свої безумовні переваги і мінуси, про які ви точно чули. Наприклад всі чули про: “Write once, run everywhere“ і разом з тим “Java is so verbose…“. Для мене джава ніколи не була verbose (Джаву до лямбда-виразів ми просто ігноруємо), вона саме настільки детальна, наскільки має бути. Мені спокійно від того, що мінімум речей відбувається “під капотом“ і що мені надають стільки контролю скільки потрібно. Коли я програмую на Котліні, або на Пайтоні, то я боюсь зазирнути в ліст для інтеджерів і побачити там ромський табір, або випадково втопитись у меморі ліках.

You can't do a loop if you care about performance in python @John Carmack

В джава є лямбда-вирази і це те місце де все почалось. Лямбда-вирази добавляють у джаву того функціонального смаку, якого їй довгий час бракувало (ігноруємо, що під капотом свторюються анонімні класи). Саме ця функціональність дозволяє писати чистий, читабельний і чарівний код в джаві.

Було:

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        numbers.stream()
               .filter(new EvenPredicate())
               .forEach(new NumberConsumer());
    }
    
    static class EvenPredicate implements java.util.function.Predicate<Integer>  {
        @Override
        public boolean test(Integer number) {
            return number % 2 == 0;
        }
    }
    
    static class NumberConsumer implements java.util.function.Consumer<Integer> {
        @Override
        public void accept(Integer number) {
            System.out.println(number);
        }
    }
}

Стало:

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        numbers.stream()
               .filter(n -> n % 2 == 0)
               .forEach(System.out::println);
    }
}

Було не чарівно, а стало чарівно.

У своїх програмах я стараюсь писати локально функціонально і об’єктно-орієнтовано (якщо так можна сказати) глобально. Тобто я використовую абстракції і принципи SOLID для побудови надійних і довгоживучих ієрархійних систем, а функціональне програмування для швидкої і зручної імплементації.

Приклади використання функціонального програмування локально

Патерн білдер

public class Main {
    public static void main(String[] args) {
        Person person = new Person(p -> {
            p.setName("John");
            p.setAge(30);
            p.setAddress("123 Main St");
        });

        System.out.println(person);
    }
}

Або

public class Main {
    public static void main(String[] args) {
        Person person = Person.builder()
                             .name("John")
                             .age(30)
                             .address("123 Main St")
                             .build();

        System.out.println(person);
    }
}

HTTP Server

import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.net.InetSocketAddress;

public class Main {
    public static void main(String[] args) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);

        server.createContext("/hello", exchange -> {
            String response = "Hello, world!";
            exchange.sendResponseHeaders(200, response.getBytes().length);
            exchange.getResponseBody().write(response.getBytes());
            exchange.getResponseBody().close();
        });

        server.createContext("/greet", exchange -> {
            String name = exchange.getRequestURI().getQuery().split("=")[1];
            String response = "Hello, " + name + "!";
            exchange.sendResponseHeaders(200, response.getBytes().length);
            exchange.getResponseBody().write(response.getBytes());
            exchange.getResponseBody().close();
        });

        server.start();
        System.out.println("Server is running on port 8000");
    }
}

Написання тестів

public class Main {
    public static void main(String[] args) {
        TestResult result = TestRunner.start()
                                      .setInit(log -> log.append("Initializing test environment...\n"))
                                      .checkA(tr -> tr.toString())
                                      .checkB(tr -> tr.toString())
                                      .clear();
                                      
        System.out.println(result);
    }
}
У тестах це може бути дуже корисно, якщо у вас наприклад величезний проект і вам потрібно писати великі, конфігуровані тести, то такий підхід написання у функціональному стилі, зробить їх читабельними і дозволятиме швидко писати.

Що робить програмування функціональним?

В основному я розглядав стиль написання, але не сильно згадував, що такого особливого має бути в коді, щоб його можна було назвати функціональним. Колись мені це було настільки цікаво, що аж затянуло місяць дивитись відео по теорії категорій і пробувати щось писати на Haskell.

https://www.youtube.com/watch?v=I8LbkfSSR58&list=PLbgaMIhjbmEnaH_LTkxLI7FMa2HsnawM_

Я опираюсь спокусі посвятити ще один пост похвалі математиці, тому просто продовжу.

Власне програмуванян робить функціональним те, що основним юнітом з яким ми працюємо є функція, якщо передавати аргумент - функція, якщо приймати - функція (Функції вищого порядку), таким чином ми відходимо від стейту подальше. Саме відсутність стейту дозволяє створювати чисті функції, які дають Haskell таку перевагу в швидкості обчислень. У функціональному прорамуванін все крутиться навколо функцій.

Then, you have something like Haskell that is often faster than C in the benchmarks. It's very close to C when it's not faster. It's not like, "Oh, it's twice as slow." No, it's right there, it's within a few percentage of C, and very often it's on the other side, it's faster than C.

https://ericnormand.me/podcast/how-is-haskell-faster-than-c


У кінцевому результаті меседж, який я хочу донести полягає у правильній, гібридизації інструментів для вирішення ваших завдань. Розширюйте ваш світогляд, щоб розглядати проблеми і їхнє вирішення максимально широко.

Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
Yaroslav Kutsela
Yaroslav Kutsela@penrose

Java Software Engineer

3.2KПрочитань
1Автори
50Читачі
На Друкарні з 26 квітня

Більше від автора

  • Рівні ізоляції транзакцій у БД

    Доволі детальний огляд аномалій у БД, рівнів ізоляції, які дозволяються уникнути аномалії, та імплементації цих рівнів. Багато використовую джерела та свої коментарі, в кінці декілька чит-шитів.

    Теми цього довгочиту:

    Бази Даних
  • Функціональна залежність у БД

    Пост про функціональну залежність в реляційних множинах. Визначення. Повторення значень в атрибуті. Приклад з п'ятьма атрибутами. Тривіальна залежність. Замикання. залежностей та атрибутів. Незвідні множини. Використання

    Теми цього довгочиту:

    Програмування

Вам також сподобається

Коментарі (5)

ОГО, Хаскель швидкий? Функції вищого порядку мали би бути ресурсоємними… Я просто зараз колупаю одну нову мовочку на логічній основі( Picat ), і там сказано, що такі функції все-таки ресурсоємні. Взявся, бо хтілося закодити певні задачі на декларативний пошук з бектрекінгом… (Це про superpermutations, але з вибором перестановки згідно фільтрації. Я побачив деяку евристику і регулярність.)

Вам також сподобається