Java

[모던 자바] 함수형 프로그래밍의 패턴 매칭(pattern matching)

Beekei 2022. 4. 4. 16:50
반응형

패턴 매칭

일반적으로 함수형 프로그래밍을 구분하는 중요한 특징으로 구조적인 패턴 매칭(pattern matching)을 들 수 있다.

예를 들어 자동차 연료를 주입하는 로직이 있다고 가정했을때 가솔린, 디젤, 전기 차량에 따라 다른 연료를 주입해야 한다. 이럴때 자바에서는 if-then-else나 switch문을 사용해서 구현하곤 한다.

public class Car {
}

public class GasolineCar extends Car {
}

public class DieselCar extends Car {
}

public class ElectricCar extends Car {
}

// 연료 주입
public Car fuel(Car car) {
    if (car instanceof GasolineCar) {
        System.out.println("휘발유 주유");
    } else if (car instanceof DieselCar) {
        System.out.println("경유 주유");
    } else {
        System.out.println("전기 충전");
    }
    return car;
}

 

자료형이 복잡해지면서 이러한 작업을 처리하는데 필요한 코드의 양도 증가했다.

패턴 매칭을 사용하면 이러한 불필요한 잡동사니 코드를 줄일 수 있다.


방문자 디자인 패턴

자바에서는 방문자 디자인 패턴(visitor design pattern)으로 자료형을 언랩할 수 있다.

특히 특정 데이터 형식을 방문하는 알고리즘을 캡슐화하는 클래스를 따로 만들 수 있다.

 

방문자 클래스는 지정된 데이터 형식의 인스턴스를 인수로 받는다. 그리고 인스턴스의 모든 멤버에 접근한다. 

public class GasolineCar extends Car {
    public Car accept(FuelVisitor v) {
        return v.visit(this);
    }
}

public class DieselCar extends Car {
    public Car accept(FuelVisitor v) {
        return v.visit(this);
    }
}

public class ElectricCar extends Car {
    public Car accept(FuelVisitor v) {
        return v.visit(this);
    }
}

public class FuelVisitor {
    public Car visit(GasolineCar car) {
        System.out.println("휘발유 주유");
        return car;
    }

    public Car visit(DieselCar car) {
        System.out.println("경유 주유");
        return car;
    }

    public Car visit(ElectricCar car) {
        System.out.println("전기 충전");
        return car;
    }
}

스칼라의 패턴 매칭 흉내 내기

패턴 매칭이라는 좀 더 단순한 해결 방법도 있다.

하지만 자바는 패턴 매칭을 지원하지 않고 스칼라 프로그래밍 언어에서 지원한다.

스칼라 패턴 매칭

이러한 패턴매칭을 자바로 흉내낼 수 있는데 자바8의 람다를 이용할 수 있다. 람다는 단일 수준의 패턴 매칭만 지원된다.

 

먼저 규칙을 정해보면, 람다를 이용하여 if-then-else가 없어야 한다. 삼항 연산자로 if-then-else를 대신할 수 있다.

물론 if-then-else를 사용하는 것이 코드의 명확성을 더 높힐 수 있지만, if-then-else나 switch가 패턴 매칭에는 도움이 되질 않으며 람다를 이용하면 단일 수준의 패턴 매칭을 간단하게 표현할 수 있으므로 여러 개의 if-then-else 구분이 연결되는 상황을 깔끔하게 정리할 수 있다.

public <T> T patternMatchFuel(Car car,
                              Function<GasolineCar, T> gasolineCarFuel,
                              Function<DieselCar, T> dieselCarFuel,
                              Function<ElectricCar, T> electricCarFuel,
                              Supplier<T> defaultFuel) {
    return
        (car instanceof GasolineCar) ?
            gasolineCarFuel.apply((GasolineCar) car) :

        (car instanceof DieselCar) ?
            dieselCarFuel.apply((DieselCar) car) :

        (car instanceof  ElectricCar) ?
            electricCarFuel.apply((ElectricCar) car) : 

        defaultFuel.get();
}

가솔린, 디젤, 전기차 별로 실행할 로직을 Function 함수형 인터페이스를 통해 구현한다.

만약 인수가 더 많다면 BiFunction, TriFunction 함수형 인터페이스를 사용해서 구현할 수 있다.

그럼 이제 위 코드를 실행해보자.

Car electricCar = new ElectricCar();
patternMatchFuel(
    electricCar,
    (car) -> { System.out.println("휘발유 주유"); return car; },
    (car) -> { System.out.println("경유 주유"); return car; },
    (car) -> { System.out.println("전기 충전"); return car; },
    () -> { System.out.println("알수없는 유형의 자동차입니다."); return null; }
);
// 출력 결과 : 전기 충전

위 예제 코드를 메서드로 감싸 안쪽에서 처리해 더욱 단순화할 수도 있다.

public Car simplyPatternMatchFuel(Car targetCar) {
    Function<Car, Car> gasolineCarFuel = (car) -> { System.out.println("휘발유 주유"); return car;};
    Function<Car, Car> dieselCarFuel = (car) -> { System.out.println("경유 주유"); return car;};
    Function<Car, Car> electricCarFuel = (car) -> { System.out.println("전기 충전"); return car;};
    Supplier<Car> defaultFuel = () -> { System.out.println("알수없는 유형의 자동차입니다."); return null; };
    return patternMatchFuel(targetCar, gasolineCarFuel, dieselCarFuel, electricCarFuel, defaultFuel);
}

Car electricCar = new ElectricCar();
simplyPatternMatchFuel(electricCar);

 

반응형