Functional Programming in Java (Korean)

Gyuhang ShimGyuhang Shim
17 min read

Table of contents

Support for Functional Programming in Java

  • Java 8 부터 Functional Programming 을 지원한다.

  • Functional Programming 기법은 lambda 를 사용하여 디자인하고 구현한다.

  • java.util.function 에 대부분의 Functional Programming library 가 준비되어 있다.

Imperative Programming

  • Declarative Programming 과 반대되는 개념

  • Programming 의 State 와 State 를 변경 시키는 구문의 관점에서 연산을 설명하는 Programming Paradigm

  • 최초의 명령형 언어는 기계어(Assembly) 였다.

  • 20년 간 많은 C/C++, Java 와 같은 고급 언어가 개발 됐지만 이 언어들도 Imperative (명령형) 언어이다.

Declarative Programming

  • Declarative: 프로그램이 어떤 “방법”으로 해야 하는 지가 아니라 “무엇”과 같은 지를 설명하는 경우

  • 예: 웹페이지; 제목, 글꼴, 본문 그림과 같이 “무엇”이 나타나야 하는 지를 묘사, 어떤 방법으로 컴퓨터 화면에 페이지를 나타내는 것이 아님

  • Imperative (명령형) 프로그래밍 언어는 알고리즘을 명시하고 목표를 명시하지 않는다.

  • Declarative (선언형) 프로그래밍 언어는 목표를 명시하고 알고리즘을 명시하지 않는다.

Functional Programming

  • 문제해결을 위해 순수한 함수형 접근법을 지원하기 위해서 생겨난 Paradigm

  • Declarative Programming 중의 한 형태

  • First-Class 로서 함수를 취급하기 때문에 다른 함수의 인자로 함수를 전달하거나 반환값으로 함수를 돌려줄 수 있다.

  • 함수의 결과값은 함수의 입력값에만 영향을 받는다.

  • Side Effect 가 생기지 않도록 한다.

Functional vs Imperative

특징ImperativeFunctional
프로그래머 관점작업을 어떻게 수행하고, 상태는 어떻게 추적할 것인가?어떤 정보가 필요하고, 어떤 변형이 필요한가?
상태의 변화중요존재하지 않음
실행의 순서중요덜 중요함
주요 흐름 제어loop, 조건 분기, 함수 Call함수 Call, 재귀 포함
주요 취급 단위Structure 나 Class 의 InstanceFirst-class 로서의 함수와 데이터 Collections

The Habitual Way

boolean found = false;
for(String city : cities) {
    if(city.equals("Chicago")) {
        found = true;
        break;
    }
}
System.out.println("Found chicago?:" + found);

A Better Way

System.out.println("Found chicago?:" + cities.contains(“Chicago"));

Tangible (눈에 띄는) Improvements

  • mutable 변수가 없다.

  • loop 문이 없어졌다.

  • 코드가 적다.

  • 보다 명확하다: 우리의 관심에 집중할 수 있다.

  • 실수를 범할 확률이 줄어든다.

  • 이해하기 쉽고 유지 보수가 쉽다.

The Old Way

public class Prices {
    public static final List<BigDecimal> prices = Arrays.asList(
            new BigDecimal("10"), new BigDecimal("30"), new BigDecimal("17"),
            new BigDecimal("20"), new BigDecimal("15"), new BigDecimal("18"),
            new BigDecimal("45"), new BigDecimal("12"));

    public static void main(String[] args) {
        BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO;
        for(BigDecimal price : prices) {
            if(price.compareTo(BigDecimal.valueOf(20)) > 0)
                totalOfDiscountedPrices =
                        totalOfDiscountedPrices.add(price.multiply(BigDecimal.valueOf(0.9)));
        }
        System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);
        // primitive type 을 직접 계산하지 말고, Class 를 따로 생성하여
        // operation 들을 api 로 제공하는 방법을 사용하여야 한다
    }
}

The Old Way: problems

  • Primitive Obsession

  • Single Responsibility Principle (SRP) 을 위배

Primitive Obsession: When the code relies too much on primitives. And primitive value controls the logic in a class and this primitive value is not type safe.

Single Responsibility Principle: There should never be more than one reason for a class to chang

A Better Way again

final BigDecimal totalOfDiscountedPrices =
    prices.stream()
        .filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0)
        .map(price -> price.multiply(BigDecimal.valueOf(0.9)))
        .reduce(BigDecimal.ZERO, BigDecimal::add);

System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);

map

filter

reduce

Reduce (foldLeft)

The Improvements

  • 코드가 어지럽지 않고 잘 구성되었다.

  • 저수준의 연산 작업에 대해서 신경 쓸 필요가 없다.

  • Logic 을 개선하거나 변경하는 것이 쉬워졌다.

  • Library method 로 반복문을 제어한다.

  • 효율적 이다: loop 에 lazy evaluation 이 적용된다.

  • 원하는 부분은 병렬로 실행 하도록 고치기 쉽다.

The Big Gains of Functional-Style Code

  • mutable 한 변수를 사용하지 않거나 변수에 재 할당하지 않기 때문에 bug 가 적고, 보다 쉽게 동시성을 고려하여 코드를 작성할 수 있다.

  • mutable 한 변수가 적으면 그만큼 Code 의 Error 도 적어진다.

  • 쉽게 동시성 Code 를 작성할 수 있기 때문에 Thread-safety 관련한 고민을 줄일 수 있다.

  • 우리의 생각을 잘 표현할 수 있도록 코드를 작성할 수 있다.

  • Concise (간결한) 코드는 작성/읽기를 줄여주고, 유지 보수를 쉽게 한다.

Why Code in Functional Style?

  • Iteration on Steroids

  • Enforcing Policies

  • Extending Policies

  • Hassle-Free Concurrency

Iteration on Steroids

  • 우리들은 객체들의 List 들을 다루기 위해서 set 과 map 을 이용하여 loop 을 이용한다.

  • 단순한 Iteration 을 하기 위해서도 for loop 을 이용해야만 했다.

  • 하지만, Java 8 부터는 다양한 방법으로 이러한 객체들의 List 를 다루기 위한 방법을 제공한다.

public static int sumByImperativeWay() {
    int sum = 0; // mutable variable
    for (int i : list) { // iteration
        sum += i;
    }
    return sum;
}

public static int sumByFunctionalWay() {
    // no iteration, no mutable variable, concise
    return list.stream().mapToInt(Integer::intValue).sum();
}

Enforcing Policies

  • Enterprise 용 응용프로그램은 정책에 의해서 유지 보수 된다.

  • Examples

    • 특정 연산이 보안상 적당한 자격을 갖추었는지 확인하는 작업

    • 보통 transaction 작업이 빠르고 update 가 재대로 수행되는 지를 확인하여야 한다.

Transaction transaction = getFromTransactionFactory();
// operation to run within the transaction ..
checkProgressAndCommitOrRollbackTransaction();
UpdateAuditTrail();
  • 코드의 중복을 야기하여 유지 보수 비용이 증가할 수 있다.

  • 코드가 Exception 을 발생할 수 있기 때문에 Transaction 이 실패하게 할 수 있다. 이는 누군가 해당 코드를 수정할 때마다 우리는 이 코드가 문제를 발생 시키지 않는 지 확인하는 작업을 추가로 요구하게 한다.

runWithinTransaction((Transaction transaction) -> {
    // .. operation to run within the transaction ..
});
  • transaction 을 얻는 대신, 잘 관리되는 함수를 이용하여 우리가 원하는 작업만 수행하도록 할 수 있다.

  • Status 와 Update 를 관리하는 Policy 코드는 추상화, 캡슐화된다.

  • 우리는 Exception 이나 Transaction 관련 문제를 생각할 필요가 없다.

Extending Policies

  • 기업의 정책은 변경이 되고 확장이 된다. (여러 개의 연산을 삭제/추가)

  • 그래서 Core Logic 을 분석하고 이를 잘 사용할 수 있도록 코드를 작성해야 한다.

  • 이러한 복잡한 일련의 절차는 종종 한 개 이상의 Interface 들을 사용하여 구현한다.

  • 이러한 방법은 효율적이나 많은 수의 Interface 를 생산하게 되어 유지 보수가 어려워지는 단점이 존재한다.

Extending Policies: In Functional style

  • 기존의 이렇게 정책을 확장하는 방법을 Functional Interface 와 Lambda Expression 을 사용하여 대신할 수 있다.

  • 추가적인 Interface 생성이 필요 없기 때문에 Method 를 구현할 필요가 없다.

  • 그래서 우리는 핵심적인 Logic 만 구현하면 된다.

public class Camera {
    .. 
    public void setFilters(final Function<Color, Color>... filters) {
        filter =
            Stream.of(filters)
                .reduce((filter, next) -> filter.compose(next))
                .orElseGet(Function::identity);
    }
    ..
}
camera.setFilters(Color::brighter, Color::darker);

  • Lambda Expression 을 Chaining 하여 Decorator Pattern 을 구현

  • Policy 를 확장 하는 것도 마찬가지로 Lambda Expression 을 Chaining 하여 손쉽게 확장이 가능하다.

  • Interface 들을 디자인하여 class 구조를 파악하는 작업을 하지 않아도 된다.

Hassle-Free Concurrency

  • 대규모 응용프로그램이 배포가 될 시점에 성능 문제가 발생하여 살펴보았더니 가장 큰 Bottleneck 은 덩치가 큰 데이터를 처리하는 모듈에 있다는 것을 발견하였다.

  • 그래서 이 모듈이 다수의 Core 로 처리를 하면 바로 성능 향상을 할 수 있다고 제안한다. (Sequential 하게 처리하고 있었음)

  • 이러한 경우 우리는 해당 모듈이 병렬로 작동할 수 있도록 코드를 변경해야만 하지만, Imperative Style 로 작성한 Code 의 경우에는 고려해야 할 것이 많다.

  • 하지만, Functional Style 로 작성한 코드의 경우는 변경할 것이 별로 없다.

// sequential: 단일 thread 로 처리
sequential: findHighPriced(items.stream());

// parallel: thread pool 로 관리되는 multi threads 로 처리, Simple!!
parallel: findHighPriced(items.parallelStream());

Parallelize

  • Lambda 수식이 동시에 수행되어야 하는 지 결정

  • Race Condition 과 Side Effect 가 없이 실행될 수 있는 지 판단.

  • 동시에 실행되는 Lambda Expression 의 결과가 처리 순서에 의존하는 지 확인

  • 시간/수가 적은 Collection 은 Sequential 이 유리

  • 시간/수가 많은 Collection 은 Parallel 이 유리

Side Effect

  • 실행 환경에서 상태의 변화를 가져오는 행위

  • I/O 함수 혹은 다른 함수를 호출하여 객체를 수정하거나, lvalue 의 변경하는 행위

x = 1 + 2; // side effect lvalue: x
y = i++; // side effect, i, y
1 * 2; // no side effect
func(Object object) { // side effect on object
    object.setValue(1);
}

Side Effect 의 문제점

public class RandomGenerator {
    public int seed { get; set; }
    public int getRandomValue() {
        Random(seed);
        seed++; // seed 의 상태변경을 알수가 없다. 디버그가 힘들다.
    }
}

Side Effect 없을 때, 장점

  • javac 컴파일러는 Side Effect 가 없는 함수들에 대해서 최적화를 더 잘 수행.

  • Side Effect 가 없는 함수들은 실행 순서를 조정하기 편하고 최적화도 쉽다.

  • 예를 들어 F1, F2 가 서로 독립적인 작업을 수행하는 함수라면 아래와 같이 함수 호출을 최적화 하거나, 실행 순서를 조정하기 편하다.

Anonymous Inner Class

button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        System.out.println(“Action added”);
    }
});
  • ActionListener interface 를 구현할 Class 를 선언할 필요 없이 바로 해당 method 를 구현하여 사용 가능하다.

  • 주로 Event Handler Logic 을 추가할 때 많이 사용.

  • 함수를 인자로 넘길 수 없기 때문에 이러한 코드를 작성해야 하는 불편함이 존재

Lambda Expression

button.addActionListener(
    event -> System.out.println(“Action added”)
);
  • 함수이지만 이름이 없이 인자와 함수 Body 로 이루어진 Expression

  • (arg1, arg2, ..) -> { body }

  • (type arg1, type arg2, ..) -> { body }

Lambda Expression vs Anonymous Class

Lambda Expression

Anonymous Class

this.

lambda 수식이 사용된 class

anonymous class

compiled

private method 로 변환

method signature 와 동일

instruction

invokedynamic

instruction 변환 원리와 동일

Lambda Expression

(int a, int b) -> { return a + b; }

() -> System.out.println("Hello World”);

(String s) -> { System.out.println(s); }

() -> 42 // 아무 인자도 없이 42 를 return

() -> { return 3.1415 };

Functional Interface

  • 하나의 추상 Method 로만 선언된 Interface

  • Examples

    • java.lang.Runnable

    • java.awt.event.ActionListener

  • anonymous inner class 를 이용하여 functional interface 의 instance 를 생성하여 사용 가능

  • 더 좋은 방법으로는 Lambda 수식을 사용하여 간단하게 나타낼 수 있음.

// FunctionalInterface annotation 은 compiler 에서 해당
// interface 가 FunctionalInterface 인지 확인하게 해준다
@FunctionalInterface
public interface Worker {
    public void doSomeWork();
}

public class WorkerTester {
    public static void execute(Worker worker) {
        worker.doSomeWork();
    }

    public static void main(String[] args) {
        execute(new Worker() {
            @Override
            public void doSomeWork() {
                System.out.printf("do some heavy work");
            }
        });
        execute(() -> System.out.printf("do some work by lambda"));
    }
}

Lambda for Collection

// Data
final List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju","Sara", “Scott");

Collection Iteration

// The habitual way
for(String name : friends) {
    System.out.print(name);
}

// In lambda way, type inference, name: String name
friends.forEach(name -> System.out.println(name));

Collection Transform

// The habitual way
List<String> upperCaseList = new ArrayList<String>();
for(String name : friends) {
    upperCaseList.add(name.toUpperCase());
}

// In lambda way
friends.stream().map(name -> name.toUpperCase());

Function Composition

/*
객체들을 여러단계에 걸쳐서 변경을 하기 위해서는 함수들을 아래와같이 조합할 수 있다. 
함수형 언어에서는 연산의 결합을 위해서 아래와 같이 Function Composition 을 
사용하여 코드를 작성한다.
*/
symbols.map(Goods::getPrice)
    .filter(Goods.isPriceLessThan(500))
    .reduce(Goods::pickHigh)
    .get();

Collection - Find Elements

// The habitual way
List<String> nameStartsWithN = new ArrayList<String>();
for(String name : friends) {
    if (name.startsWith(“N”)
        nameStartsWith.add(name);
}

// In lambda way
friends.stream().filter(name -> name.startsWith(“N”));

Creating Fluent Interfaces using Lambda Expressions

public class Mailer {
    public void from(final String address) { /*... */ }
    public void to(final String address) { /*... */ }
    public void subject(final String line) { /*... */ }
    public void body(final String message) { /*... */ }
    public void send() { System.out.println("sending..."); }
    …
    public static void main(final String[] args) {
        Mailer mailer = new Mailer(); // 객체의 수명이 불확실
        mailer.from("build@agiledeveloper.com"); // 반복되는 객체
        mailer.to("starblood@agiledeveloper.com"); // 반복되는 객체
        mailer.subject("build notification"); // 반복되는 객체
        mailer.body("...your code sucks..."); // 반복되는 객체
        mailer.send();
    }

Two smells

  1. 반복되는 reference 변수 사용

  2. mailer 객체의 수명 주기 - 어떻게 처리해야 하는지? 재 사용 가능한지?

개선 Point

  1. 반복되는 reference 변수 사용

    • 객체의 문맥에서 대화형의 상태를 유지하는 방법을 사용하면 좋다.

    • Method Chaining 혹은 Cascade Method Pattern 을 사용하여 개선할 수 있다.

Doing method chaining

  • Method 의 return type 을 void 로 하지 않고 객체의 instance 를 return 하게 변경

  • 이렇게 return 된 Object 를 이용하여 Method Chain 을 구성

public class MailBuilder {
    public MailBuilder from(final String address) { /*... */; return this; }
    public MailBuilder to(final String address) { /*... */; return this; }
    public MailBuilder subject(final String line) { /*... */; return this; }
    public MailBuilder body(final String message) { /*... */; return this; }
    public void send() { System.out.println("sending..."); }

    // lambda 를 사용하여 보다 유연하게 바꾸어보자!
    public static void main(final String[] args) {
        new MailBuilder() // 누군가 객체를 저장할 수 있다.
                .from("build@agiledeveloper.com")
                .to("starblood@agiledeveloper.com")
                // subject 를 여러번 호출할 수 있는 위험이 있다.
                .subject("build notification")
                .body("...it sucks less...")
                .send();
    }
}

개선된 버전

public class FluentMailer {
    private FluentMailer() {} // 직접적인 객체생성을 금지
    public FluentMailer from(final String address) { /*... */; return this; }
    public FluentMailer to(final String address) { /*... */; return this; }
    public FluentMailer subject(final String line) { /*... */; return this; }
    public FluentMailer body(final String message) { /*... */; return this; }

    // FluentMailer Instance 의 코드 블록을 인자로 받아들임
    public static void send(final Consumer<FluentMailer> block) {
        final FluentMailer mailer = new FluentMailer();
        block.accept(mailer);
        System.out.println("sending...");
    }

    public static void main(final String[] args) {
        // lambda 식에서 mailer 라는 객체를 빌려서 사용 - loan pattern
        FluentMailer.send(mailer -> // new 호출이 사라짐
                mailer.from("build@agiledeveloper.com")
                        .to("starblood@agiledeveloper.com")
                        // subject 를 여러번 호출할 확률도 줄어든다.
                        .subject("build notification")
                        .body("...much better..."));
    }

Lazy Evaluation

  • 연산 cost 가 큰 작업은 최소한으로 실행하는 것이 목적

  • func1() || func2() : func2 는 func1 이 true 이면 실행되지 않음 (short-circuiting)

  • func(func1(), func2()): func 을 수행하기 위해서 func1, func2 두 개의 함수를 모두 수행해야만 한다.

  • lambda 수식을 사용하여 이를 lazy evaluation 하도록 할 수 있다.

public static boolean evaluate(final int value) {
    System.out.println("evaluating ..." + value);
    simulateTimeConsumingOp(2000);
    return value > 100;
}

public static void eagerEvaluator(final boolean input1, final boolean input2) {
    System.out.println("eagerEvaluator called...");
    System.out.println("accept?: " + (input1 && input2));
}

public static void lazyEvaluator(final Supplier<Boolean> input1, final Supplier<Boolean> input2) {
    System.out.println("lazyEvaluator called...");
    System.out.println("accept?: " + (input1.get() && input2.get()));
}

System.out.println("//" + "START:EAGER_OUTPUT");
eagerEvaluator(evaluate(1), evaluate(2));
System.out.println("//" + "END:EAGER_OUTPUT");

System.out.println("//" + "START:LAZY_OUTPUT");
lazyEvaluator(() -> evaluate(1), () -> evaluate(2));
System.out.println("//" + "END:LAZY_OUTPUT");

//START:EAGER_OUTPUT
evaluating ...1
evaluating ...2
eagerEvaluator called...
accept?: false
//END:EAGER_OUTPUT
//START:LAZY_OUTPUT
lazyEvaluator called...
evaluating ...1
accept?: false
//END:LAZY_OUTPUT

Laziness of Streams

  • Stream 은 Intermediate/Terminal Operation method type 이 있다.

  • Intermediate Operation Method 의 Chain 을 구성할 수 있다.

  • Terminal Operation 은 Chain 의 마지막에 위치

  • map/filter 등은 Intermediate method 이다.

  • findFirst/reduce 등은 Terminal method 이다.

Leveraging laziness of Stream

// lazy evaluation 을 확인하기 위한 helper method
private static int length(final String name) {
    System.out.println("getting length for " + name);
    return name.length();
}
// lazy evaluation 을 확인하기 위한 helper method
private static String toUpper(final String name) {
    System.out.println("converting to uppercase: " + name);
    return name.toUpperCase();
}

List<String> names = Arrays.asList("Brad", "Kate", "Kim", "Jack", "Joe", 
        "Mike", "Susan", "George", "Robert", "Julia", "Parker", "Benson");

System.out.println("//" + "START:CHAIN_OUTPUT");
final String firstNameWith3Letters = 
        names.stream()
                .filter(name -> length(name) == 3)
                .map(name -> toUpper(name))
                .findFirst()
                .get();
System.out.println(firstNameWith3Letters);

Hypothetical eager evaluation of operations

  • filter, map Methods are lazy

  • filter, map Method 는 Lambda 수식을 다음 call chain 으로 전달

  • Terminal 작업을 수행하는 findFirst 가 호출될 때만 수식을 평가

public static boolean isPrime(final int number) {
    return number > 1 &&
            IntStream.rangeClosed(2, (int) Math.sqrt(number))
                    .noneMatch(divisor -> number % divisor == 0);
}
// rangeClosed(1, 10): 1, 2, 3, … 10
// noneMatch(Predicate): 명제가 거짓이면 true 를 return

Creating an infinite Stream of prime numbers

private static int primeAfter(final int number) {
    if (isPrime(number + 1))
        return number + 1;
    else
        return primeAfter(number + 1);
}

public static List<Integer> primes(final int fromNumber, final int
        count) {
    return Stream.iterate(primeAfter(fromNumber - 1), Primes::primeAfter)
            .limit(count)
            .collect(Collectors.<Integer>toList());
}
// Stream.iterate(seed, UnaryOperator): 무한의 배열을 만들어 낸다.
// UnaryOperator: 한개의 인자를 받아서 하나의 결과를 출력하는 함수
// limit(count): count 만큼의 배열을 return

System.out.println("10 primes from 1: " + primes(1, 10));
// 결과: 10 primes from 1: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

Pure OOP vs Hybrid OOP-Functional Style

  • OOP: 객체는 상태의 변화를 갖는다.

  • Hybrid: 경량의 객체가 다른 객체로 변형된다.

Performance Concerns

  • 새로운 특징들이 성능에 영향을 미치는가? Yes

  • 하지만 대부분의 경우에는 이러한 특징들이 더 좋은 성능을 내도록 한다.

  • 3% 정도의 시간이 걸리는 비중이 작은 효율성 문제는 고려하지 않아도 된다.

  • Java 8 부터는 Compiler 최적화, 새롭게 단장한 invokedynamic, 최적화된 bytecode instruction 때문에 Lambda Expression 이 빠르게 수행된다.

// 2 가지 primesCount 를 계산하는데 거의 비슷한 시간이 소요된다.
long primesCount = 0; // 0.0250944s
for(long number : numbers) {
    if(isPrime(number)) primesCount += 1;
}

final long primesCount = // 0.0253816s
    numbers.stream()
        .filter(number -> isPrime(number))
        .count();

Essential Practices to Succeed with Functional Style

  • More Declarative, Less Imperative

  • Favor Immutability

  • Reduce Side Effects

  • Prefer Expressions Over Statements

  • Design with Higher-Order Functions

Lexical Scoping in Closures

public static Predicate<String> checkIfStartsWith(final String letter) {
    return name -> name.startsWith(letter);
}
  • Compiler 가 method 단위 안에 있는 모든 Scope 를 검사하여 ‘letter’ 가 method 의 인자인 것을 찾아준다. (Lexical Scoping)

  • letter 라는 변수는 checkIfStartsWith method 의 Scope 에 존재한다.

  • Lambda Expression 에서 Method Level 의 Scope 에 있는 변수를 사용할 수 있다 .

  • 이러한 Lambda Expression 을 closures 라고 부른다.

Passing a Method Reference of an Instance Method

// parameterized lambda expression method call
friends.stream().map(name -> name.toUpperCase());
// 목표로 하는 클래스의 instance 의 method 호출을 아래와 같이 바꿀 수 있다.
friends.stream().map(String::toUpperCase);

Passing a Method Reference to a static Method

str.chars().filter(ch -> Character.isDigit);
// 목표로 하는 클래스의 static method 호출을 아래와 같이 바꿀 수 있다.
str.chars().filter(Character::isDigit);

Passing a Method Reference to a Method on Another

str.chars().forEach(ch -> System.out.println(ch));

// lambda expression 에서 다른 instance 의 method 에 인자를
// 전달하여 호출할 수 있다.
str.chars().forEach(System.out::println);

Passing a Method Reference of a Method That Takes Parameters

people.stream().sorted((person1, person2) -> person1.ageDifference(person2));

// lambda expression 에서 첫번째 인자의 method 호출에 
// 다른 인자들을 parameter 로 전달하여 호출할 수 있다.
people.stream().sorted(Person::ageDifference);

Using a Constructor Reference

Supplier<Heavy> supplier = () -> new Heavy();

// Constructor 를 직접 호출하지 않고, constructor-reference 를 사용할 수 있다.
Supplier<Heavy> supplier = Heavy::new;

Dominant Functional Programming Languages

  • Haskell

    • 순수 함수형 프로그래밍 언어로, 강력한 정적 타입 시스템과 지연 평가를 특징으로 하여 수학적 정확성과 고도의 추상화를 지원합니다.

    • 순수 함수형 프로그래밍과 독특한 타입 시스템으로 인해 학습 곡선이 매우 가파르지만, 간결하고 수학적으로 엄밀한 구문을 통해 고도의 추상화와 안전성을 제공합니다.

  • Scala

    • 객체 지향과 함수형 프로그래밍을 통합한 언어로, 높은 수준의 추상화와 JVM 호환성을 통해 강력하고 유연한 소프트웨어 개발을 가능하게 합니다.

    • 함수형과 객체 지향 프로그래밍의 결합으로 인해 학습 곡선이 가파르지만, 유연하고 표현력 있는 구문을 통해 복잡한 시스템을 효과적으로 설계할 수 있게 합니다.

  • Clojure

    • 간결하고 표현력 있는 구문과 불변 데이터 구조를 특징으로 하는 함수형 프로그래밍 언어로, JVM에서 실행되며 동시성 처리를 효과적으로 지원합니다.

    • Lisp 계열의 독특한 구문과 함수형 패러다임으로 인해 학습 곡선이 가파르지만, 간결하고 표현력 있는 구문을 통해 강력한 데이터 처리와 동시성 제어를 가능하게 합니다.

  • Elixir

    • 동시성과 분산 시스템에 강점을 지닌 함수형 프로그래밍 언어로, Virtual Machine 인 BEAM 위에서 실행되며 고성능과 Fault Tolerance 를 제공하는 것을 특징으로 합니다.

    • 생태계를 활용하는 동시성 모델 때문에 학습 곡선이 약간 가파를 수 있지만, 간결하고 가독성이 높은 구문으로 고성능의 분산 시스템을 쉽게 구축할 수 있게 합니다.

  • Elm

    • Compile Time 에 오류를 방지하고 안전한 Front-End Web Application 개발을 지원하는 순수 함수형 언어로, Side Effect 가 없는 상태 관리를 특징으로 합니다.

    • 순수 함수형 프로그래밍과 Side Effect 없는 아키텍처로 인해 학습 곡선이 다소 가파르지만, 직관적이고 엄격한 구문을 통해 안정적이고 오류가 적은 Front-End Application 을 개발할 수 있습니다.

  • F#

    • 함수형 프로그래밍과 객체 지향 프로그래밍을 통합하여 간결하고 효율적인 코드를 작성할 수 있게 해주며, .NET 플랫폼과의 뛰어난 호환성을 특징으로 합니다.

    • 함수형 패러다임에 익숙하지 않다면 학습 곡선이 다소 가파를 수 있지만, 간결하고 직관적인 구문을 통해 복잡한 문제를 쉽게 해결할 수 있도록 돕습니다.

  • Rust

    • 메모리 안전성과 성능을 모두 중시하며, 시스템 프로그래밍에서 데이터 경합과 메모리 오류를 방지하는 안전한 동시성을 특징으로 하는 언어입니다.

    • 메모리 안전성과 소유권 모델의 복잡성으로 인해 학습 곡선이 가파르지만, 명확하고 표현력 있는 구문으로 안전하고 성능 높은 시스템 프로그래밍을 가능하게 합니다.

    • Type Safe, Strong Type Check, Parallel Friendly, Alternative for C/C++

  • OCaml

    • 다양한 패러다임을 지원하는 강력한 기능으로 인해 학습 곡선이 다소 가파르지만, 강력한 타입 시스템과 간결한 구문으로 고성능과 안정성을 갖춘 코드를 작성할 수 있게 합니다.

    • 함수형, 객체 지향, 명령형 프로그래밍 패러다임을 결합하여 강력한 타입 시스템과 빠른 실행 속도를 제공하는 다중 패러다임 언어입니다.

  • Erlang

    • 분산 시스템과 고가용성 소프트웨어 개발을 위해 설계된 언어로, 강력한 동시성 모델과 Fault Tolerance 을 특징으로 합니다.

    • 동시성 및 분산 시스템 개념의 이해가 필요해 학습 곡선이 다소 가파르지만, 간결하고 명령형 프로그래밍과는 다른 독특한 구문으로 고가용성 시스템 개발을 용이하게 합니다.

  • Kotlin

    • 현대적이고 간결한 구문과 완전한 Inter Operability 를 갖춘 JVM 기반 언어로, 안드로이드 개발과 Multi Platform 개발에 최적화되어 있습니다.

    • Java와 유사한 구문 덕분에 학습 곡선이 완만하며, 간결하고 표현력 있는 구문을 통해 코드의 가독성과 생산성을 높입니다.

Functional Programming Languages Recommendation by Learning Curves and Pragmatic and Future vision

업계에서 현재 많이 사용하고 있고, 해당 언어의 임금 수준을 고려하였으며, 미래에도 잘 쓰일지 에 대해서도 염두에 두었습니다. 개인적인 판단이 섞여 있다는 말씀 드립니다. Level 은 어려울 수록 숫자가 커집니다.

  1. Kotlin - Easy

  2. Scala - Difficult (Level 2)

    • 1st Salary
  3. Rust - Difficult (Level 2)

  4. Elixir - Difficult (Level 1)

    • 3rd Salary
  5. Clojure - Difficult (Level 2)

    • 4th Salary
  6. F# - Difficult (Level 1)

    • 5th Salary
  7. Haskell- Difficult (Level 3)

    • 2nd Salary

Haskell, Scala, Clojure, F#, Erlang, and Elixir. You can observe how the demand for these languages has evolved, with Scala and Elixir showing significant growth, while others like Haskell and Clojure have also seen steady increases.

더 읽을 거리

0
Subscribe to my newsletter

Read articles from Gyuhang Shim directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Gyuhang Shim
Gyuhang Shim

Gyuhang Shim Passionate about building robust data platforms, I specialize in large-scale data processing technologies such as Hadoop, Spark, Trino, and Kafka. With a deep interest in the JVM ecosystem, I also have a strong affinity for programming in Scala and Rust. Constantly exploring the intersections of high-performance computing and big data, I aim to innovate and push the boundaries of what's possible in the data engineering world.