Vavr User Guide
Vavr
- Vavr는 함수 제어 구조와 영구적인 데이터 타입을 제공해주는 JAVA 8+를 위한 함수형 라이브러리입니다.
- Vavr는 함수 패턴을 기반으로 한 람다를 활용하여 다양한 새로운 기능들을 제공해줍니다.
- 그중 하나는 JAVA 표준 컬렉션 라이브러리를 대체하기 위한 함수 컬렉션 라이브러리입니다.
1. 함수형 프로그래밍
- vavr에 대해 알아보기 전에 함수형 프로그래밍의 기초에 대해 알아보면서 왜 자바 컬렉션 라이브러리를 대체할 수 있는 vavr를 만들었는지에 대해 알아보겠습니다.
1) Side-Effects(부수 효과)
- 자바는 전형적으로 Side-Effects가 많이 존재합니다.
- 흔히 Side-Effects는 객체나 변수들을 변경시키고 만약 Side-Effects로 인해 의도하지 않은 변경들이 일어나는 경우 이는 시스템에 피해를 줄 수 있습니다..
int divide(int dividend, int divisor){
return a / b;
}
- 이 함수는 divisor가 0일 때 예외를 발생시킵니다.
- 그리고 이런 예외는 프로그램에 영향을 끼칠 수 있으며 정상적인 흐름으로 동작되지 않게 됩니다.
- 아래와 같이 vavr의 Try를 이용하면 예외가 발생하더라도 예외가 던져지지 않으며 개발자가 원하는 흐름으로 제어할 수 있습니다.
Try<Integer> divide(Integer dividend, Integer divisor){
return Try.of(() -> dividend / divisor);
}
2) Referential Transparency(참조 투명성)
- 참조 투명성이란 간단히 말하자면 같은 입력이 들어왔을 때 항상 같은 출력이 나오는 성질이라고 할 수 있습니다.
- 어떤 함수가 외부의 영향을 받지 않고 해당 함수의 파라미터만을 사용한다면 참조 투명성을 가질 수 있을 것입니다.
- 함수의 모든 표현식이 참조 투명성을 가진다면 이러한 함수를 pure 하다고 할 수 있으며 pure 한 함수는 외부의 영향을 받지 않기 때문에 테스트가 쉬워집니다.
Math.random();
// random()은 같은 입력이 들어오더라고 다른 값들이 나오기 때문에 참조 투명성이 없습니다.
Math.max(1, 2);
// max()는 파라미터만을 사용하여 항상 같은 입력에 대해 같은 출력을 반환하기 때문에 참조 투명성을 가진다고 할 수 있습니다.
3) immutable values(불변 값)
- 불변 값들은 본질적으로 Thread-safe 하기 때문에 동기화가 필요 없습니다.
- 불변하므로 equals와 hashCode가 안정적이므로 hashkey를 신뢰할 수 있습니다.
- 복사될 필요가 없으며 타입 변환 시 안정적으로 동작될 수 있습니다.
그러므로 자바에서 참조 투명성을 가지는 함수와 불변 값을 사용하는 것이 좋을 것이고 Vavr는 이러한 조건들을 만족시키기 위해 필요한 Controls, Collections들을 제공합니다.
2. 데이터 구조
- Vavr의 컬렉션 라이브러리는 람다로 구축된 다양한 함수 데이터 구조를 제공합니다.
- 자바의 컬렉션 라이브러리와 동일한 인터페이스는 Iterable만 존재하며 그 이유는 자바 컬렉션 인터페이스들의 메서드가 변화를 예측할 수 없는 void 타입을 반환하는 데이터들을 제공하기 때문입니다.
1) Mutable Data Structures(가변 데이터 구조)
- 자바는 객체지향 언어로 데이터를 숨기기 위해 객체의 상태를 캡슐화하고 해당 상태를 제어할 수 있는 퍼블릭 인터페이스인 메서드들을 제공합니다.
- void clear()와 같은 메서드들은 부수 효과를 발생시키고 변화를 예측할 수 없게 만듭니다.
2) Immutable Data Structures(불변 데이터 구조)
- 불변 데이터 구조는 한번 생성된 후 수정이 불가능합니다.
3) Persistent Data Structures(영구적인 데이터 구조)
- 영구적인 데이터 구조는 변경이 일어나더라도 이전 상태를 보관하기 때문에 사실상 불변하다고 할 수 있습니다.
- 모든 영구적인 데이터들은 변경 및 조회를 할 수 있습니다.
- 작은 변화에도 많은 동작이 일어나야 하기 때문에 메모리와 시간을 효율적으로 관리하기 위해선 필요한 데이터를 최대한 공유해야 합니다.
4) Functional Data Structures(함수형 데이터 구조)
- 함수형 데이터 구조는 불변성, 영구적인 데이터 구조와 참조 투명성을 가지고 있습니다.
- Vavr는 가장 일반적으로 사용되는 함수형 데이터 구조를 특징으로 구성됩니다.
Vavr 사용해보기
- Vavr는 그림과 같이 Tuple, Lambda, Value를 기반으로 만들어져 있습니다.
dependencies {
implementation('io.vavr:vavr:0.9.3')
testImplementation('org.junit.jupiter:junit-jupiter:5.6.0')
testImplementation('org.assertj:assertj-core:3.11.1')
}
test {
useJUnitPlatform()
}
- gradle 기준으로 의존성을 추가해줍니다. 테스트 작성을 위해 테스트 의존성도 함께 추가해줍니다.
1. Tuples(튜플)
- 자바에는 일반적인 튜플의 개념이 존재하지 않습니다.
- 배열, 리스트와 달리 튜플은 다른 타입의 객체들을 가질 수 있으며 해당 값들은 불변합니다.
1) Create a tuple
@Test
void tuple() throws Exception{
Tuple2<String, Integer> hello10 = Tuple.of("Hello", 10);
String hello = hello10._1;
Integer ten = hello10._2;
assertThat(hello).isEqualTo("Hello");
assertThat(ten).isEqualTo(10);
}
- Tuple.of로 튜플을 생성할 수 있고 tuple._1, _2으로 튜플 내부에 있는 객체를 꺼내올 수 있습니다.
2) Map and Transfrom a tuple
@Test
void tuple() throws Exception{
Tuple2<String, Integer> hello10 = Tuple.of("Hello", 10);
// 각 요소별로 매핑하기
Tuple2<String, Integer> helloMap15 =
hello10.map(
s -> s + " Map",
i -> i + 5
);
assertThat(helloMap15._1).isEqualTo("Hello Map");
assertThat(helloMap15._2).isEqualTo(15);
// 한번에 매핑하기
Tuple2<String, Integer> helloMap15V2 =
hello10.map((s, i) -> Tuple.of(s + " Map", i + 5));
assertThat(helloMap15V2._1).isEqualTo("Hello Map");
assertThat(helloMap15V2._2).isEqualTo(15);
// 새로운 타입으로 변환하기
String hello = hello10.apply((s, i) -> s + i);
assertThat(hello).isEqualTo("Hello10");
}
- 각 요소별, 혹은 한 번에 매핑하는 것도 가능합니다.
- 뿐만 아니라 apply를 통해 새로운 타입으로 변환도 가능합니다.
2. Functions
- 자바 Function에서는 두 개의 매개변수를 사용할 수 있는 BiFunction을 제공하지만 Vavr에서는 최대 8개의 매개 변수를 사용할 수 있는 기능을 제공합니다.
- Function0, 1, 2등으로 사용할 수 있고 예외 처리가 필요할 경우는 CheckedFunction1, 2 등을 제공합니다.
1) Composition
- 여러 Function들을 하나의 Function으로 조합시키는 기능이 존재합니다.
@Test
void function() throws Exception{
Function1<Integer, Integer> addOne = i -> i + 1;
Function1<Integer, Integer> multiplyByTwo = i -> i * 2;
// andThen은 호출하는 객체가 먼저 적용된다.
Function1<Integer, Integer> add1AndMultiplyBy2 = addOne.andThen(multiplyByTwo);
Integer answer = add1AndMultiplyBy2.apply(3);
assertThat(answer).isEqualTo(8);
assertThat(add1AndMultiplyBy2.apply(4)).isEqualTo(10);
// compose는 넘겨주는 전달인자부터 적용된다.
Function1<Integer, Integer> add1AndMultiplyBy2V2 = multiplyByTwo.compose(addOne);
Integer answer2 = add1AndMultiplyBy2V2.apply(3);
assertThat(answer2).isEqualTo(8);
assertThat(add1AndMultiplyBy2V2.apply(4)).isEqualTo(10);
}
- andThen, compose 메서드를 이용하면 Function들을 합칠 수 있습니다.
2) Lifting
- Function의 반환 타입을 Option타입으로 감싸여 반환하게 할 수 있습니다.
- 이는 자바 Optional과 같이 유연한 예외, Null처리가 가능해질 것입니다.
@Test
void function2() throws Exception{
Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;
Function2<Integer, Integer, Option<Integer>> safeDivide = Function2.lift(divide);
// lift함수를 이용하여 만들어진 Function의 반환값은 안전하다.
Option<Integer> divide0 = safeDivide.apply(1, 0);
Integer isNull = divide0.getOrNull();
assertThat(isNull).isNull();
}
3) Partial Apply(부분 적용 함수), Currying(커링 함수)
-
Function으로 부분 적용 함수와 커링 함수를 간단하게 만들 수 있습니다.
@Test void function3() throws Exception{ Function4<Integer, Integer, Integer, Integer, Integer> sum = (a, b, c, d) -> a + b + c + d; // 부분 적용 함수 Function2<Integer, Integer, Integer> sum3 = sum.apply(1, 2); assertThat(sum3.apply(3, 4)).isEqualTo(10); // 커링 함수 Function1<Integer, Function1<Integer, Integer>> currying = sum.curried().apply(1).apply(2); assertThat(currying.apply(3).apply(4)).isEqualTo(10); }
-
부분 적용 함수는 별다른 기능 호출 없이 apply를 통해 즉시 만들 수 있습니다.
-
Function의 모든 매개변수를 넘겨주지 않으면 넘겨준 매개변수가 적용된 Function을 반환해 줍니다.
-
curried() 메서드를 이용하면 커링 함수로 만들 수 있습니다.
-
커링 함수는 부분 적용 함수와는 다르게 매번 하나의 매개 변수만 넘길 수 있습니다.
4) Memorization
- 캐싱 역할을 하는 기능도 제공됩니다.
@Test
void function4() throws Exception{
Function0<Double> cachedValue = Function0.of(Math::random).memoized();
Double random1 = cachedValue.apply();
Double random2 = cachedValue.apply();
assertThat(random1).isEqualTo(random2);
}
- memoized 메서드 이용하면 해당 결과는 캐싱됩니다.
- 그러므로 random1, 2가 동일한 값을 가지고 있습니다.
3. Values
- Vavr의 Value들은 불변하므로 메모리에서 공유하여 사용하더라도 Thread-safe 하게 사용할 수 있습니다.
1) Option
- Option은 자바의 Optional과 유사하지만 약간의 차이가 존재합니다.
- Option은 Some(값이 존재) 혹은 None을(값이 없음) 가지게 됩니다.
@Test
void option() throws Exception {
Optional<String> foo = Optional.of("foo");
// Optional은 NULL이 되면 notpresent가 된다.
Optional<String> maybeBar = foo
.map(s -> (String) null)
.map(s -> s.toUpperCase() + "bar");
assertThat(maybeBar.isPresent()).isFalse();
Option<String> foo2 = Option.of("foo");
// Option은 NULL이여도 Some(Null)과 같이 값으로 취급하기때문에 NPE가 발생한다.
assertThatThrownBy(() -> {
Option<String> maybeBar2 = foo2
.map(s -> (String) null)
.map(s -> s.toUpperCase() + "bar");
}).isInstanceOf(NullPointerException.class);
}
- Option의 경우 예외가 발생하는 것을 알 수 있습니다.
- Optional에 비해 쓸모없다고 생각할 수 있지만 null처리는 의식적으로 처리하는 게 좋기 때문에 flatMap을 통해 null처리를 할 수 있습니다.
@Test
void option_flatMap() throws Exception{
Option<String> foo = Option.of("foo");
Option<String> maybeBar = foo.map(s -> (String) null)
.flatMap(s -> Option.of(s))
.map(s -> s.toUpperCase() + "bar");
Option<String> maybeBar2 = foo.flatMap(s -> Option.of((String) null))
.map(s -> s.toUpperCase() + "bar");
assertThat(maybeBar.isEmpty()).isTrue();
assertThat(maybeBar2.isEmpty()).isTrue();
}
- flatMap을 이용하면 NPE를 발생시키지 않게 할 수 있습니다.
2) Try
- Try는 정상적인 반환 값 혹은 예외 발생에 대한 계산을 나타내는 모나드 컨테이너 유형입니다.
@Test
void try_vavr() throws Exception {
String exception_2 = Try.of(this::throwException)
// x는 Throwble로 try에서 던져지는 예외를 가진다.
// x를 Match와 Case를 이용하여 각 예외별로 다른 처리를 진행항 수 잇다.
.recover(x -> Match(x).of(
Case($(instanceOf(Exception_1.class)), this::somethingWithException),
Case($(instanceOf(Exception_2.class)), this::somethingWithException),
Case($(instanceOf(Exception_3.class)), this::somethingWithException)
))
.getOrElse("Else");
assertThat(exception_2).isEqualTo(Exception_2.class.getSimpleName());
}
private String somethingWithException(Exception t) {
return t.getClass().getSimpleName();
}
private String throwException() {
if (true) throw new Exception_2();
return "Str";
}
static class Exception_1 extends RuntimeException { }
static class Exception_2 extends RuntimeException { }
static class Exception_3 extends RuntimeException { }
- Try.of에 잇는 throwException은 Exception_2를 던지기 때문에 String은 somethingWitException 메서드에 의해 Excpetion_2의 이름을 가지는 것을 알 수 있습니다.
3) Lazy
- Lazy는 Supplier와 같이 지연 계산을 하는 기능을 가지고 있습니다.
- 추가적으로 결과를 캐싱하는 성질을 가집니다.
@Test
void lazy() throws Exception{
Lazy<Double> lazy = Lazy.of(Math::random);
// 호출이 한 번도 되지 않았으니 fasle이다.
assertThat(lazy.isEvaluated()).isFalse();
Double first = lazy.get();
// 호출되었으니 true가 된다.
assertThat(lazy.isEvaluated()).isTrue();
Double second = lazy.get();
// 값을 캐싱하기 때문에 동일한 값을 가지고 있다.
assertThat(first).isEqualTo(second);
}
'JAVA' 카테고리의 다른 글
Mockito 공부하기 #2 (0) | 2020.02.24 |
---|---|
Mockito 공부하기 #1 (0) | 2020.02.24 |
모던 자바 1. 동작 파라미터화 코드 전달하기 (0) | 2020.02.21 |
AssertJ 주요 기능 공부 (0) | 2020.02.19 |
이펙티브 자바: 아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라 (0) | 2020.02.17 |