[개요]
- 불변성 (Immutable) 값이나 변수를 적극 활용할 수 있다.
- 함수가 참조 투명성을 지키고, 부작용을 줄일 수 있도록 구현할 수 있다.
- 순수함수 (Pure Function) 로 구현할 수 있다.
- 각 언어로 만들어진 다음 2개 클래스에서 중복된 코드를 줄이고
- 함수형 표현으로 최대한 개선한다.
- 1부터 100까지 숫자를 각 함수에 넣고 동작 결과가 동일해야 한다.
- 전역변수
int number
를 삭제하였다. main
메서드에서 인스턴스 하나를 삭제하고 두 개의 결과값을 받을 수 있도록 바꾸었다.
public static void main(String[] args) {
ClassifierAlpha alpha1 = new ClassifierAlpha();
System.out.println(alpha1.isPerfect(10));
System.out.println(alpha1.isPerfect(6));
}
static public int sum(Set<Integer> factors) { // 약수의 합
return factors.stream()
.mapToInt(Integer::intValue)
.sum();
}
public int factorsSum(int number) {
return IntStream.rangeClosed(1,number)
.filter(i -> isFactor(number,i)).sum(); // 바로 int로 반환하게 하여 불필요한 함수를 제거
}
public class Factor {
protected boolean isFactor(int number, int potentialFactor) {
return number % potentialFactor == 0;
}
protected Set<Integer> factors(int number) {
return IntStream.rangeClosed(1, number)
.filter(i -> isFactor(number, i))
.boxed()
.collect(Collectors.toSet());
}
protected int factorsSum(int number) {
return factors(number).stream()
.mapToInt(i->i)
.sum(); // 다른 함수 재사용 하는 것으로 수정
}
}
- 클로저를 선언해서 매개변수 또는 리턴값으로 전달할 수 있다
- map, filter, reduce 고차 함수를 활용할 수 있다
- 클로저 관련된 다양한 표현을 학습한다
- 앞서 작성한 자연수 분류 ClassifierAlpha, PrimeAlpha 를 이용한다.
- 2-100까지 자연수 중에서 각각 다음의 목록을 출력한다.
- 완전수(perfect)
- 과잉수(Abundant)
- 부족수(Deficient)
- 소수(Prime)
- 정사각수(Squared)
- map, filter, reduce 고차 함수를 활용한다.
- 출력을 위해서는 반드시 클로저(또는 람다)를 선언하고 반복문 대신 reduce를 활용해서 출력해야 한다.
- 자연수 중에서 다른 자연수의 제곱으로 표현되는 정사각수(squared) 판단 함수를 추가한다
- 기존 클래스 메서드들을 함수형 인터페이스로 바꾸어 정의해주었다.
// 변경 전
protected Set<Integer> factors(int number) {
return IntStream.rangeClosed(1, number)
.filter(i -> isFactor(number, i))
.boxed()
.collect(Collectors.toSet());
}
// 변경 후
protected static IntFunction<Set<Integer>> factors =
(number) -> IntStream.rangeClosed(1, number)
.filter(i -> isFactor.test(number, i))
.boxed()
.collect(Collectors.toSet());
import java.util.function.IntPredicate;
public class SquareAlpha {
public static final IntPredicate isSquare =
(number) -> (int)(Math.sqrt(number) * 100) == (int)Math.sqrt(number) * 100;
}
- 제일 간단한 방법으로 구현하고 싶어서 위와 같은 방법으로 구현하였다.
- 정수로 나눠떨어져야지만 메소드 내부 등호가 성립하게된다.
NumberInfo
클래스에서int i
값을 받으면 각 조건을 체크하여 결과를String
으로 반환하는 값을 만들었다.
public static final IntFunction<String> getInfo =
i -> {
List<String> info = new ArrayList<>(); // ArrayList에 각 조건에 맞는 키워드 저장
if (PrimeAlpha.isPrime.test(i)) info.add("prime");
if (ClassifierAlpha.isPerfect.test(i)) info.add("perfect");
if (ClassifierAlpha.isDeficient.test(i)) info.add("deficient");
if (ClassifierAlpha.isAbundant.test(i)) info.add("abundant");
if (SquareAlpha.isSquare.test(i)) info.add("square");
return i + " : " + String.join(", ", info); // 1개의 String으로 만들어 반환
};
- 메인함수에서는
Stream API
에reduce()
를 활용하여 다음과 같이 출력하였다.
String reduce = IntStream.rangeClosed(2, 100) // 2~100까지 입력
.mapToObj(NumberInfo.getInfo::apply) // 메소드 참조
.reduce("", (a, b) -> a + b +"\n"); // 결과값을 누적하여 1개의 String으로 최종 연산
System.out.println(reduce);
- 끝 🎉🎉
일반적으로 이야기하는 프로그래밍에서 순수 함수
의 특징들은 이러한 수학적 함수의 성질에서 기원한다.
- 인자
input
과 결과값output
이 있다. - 함수의 인자로 사용되는 값 하나에 대응하는 함수의 결과값이 유일하게 존재한다.
- 불변성 : 함수 외부의 상태를 변경하지 않는다.
- 이를
부수효과side effect 가 없다
라고도 한다. - 참조투명성 : 외부 상태를 변경하지도 않고 외부의 상태로부터 영향 받지 않는다.
외부에서 받는 효과로 다음과 같은 예시 상황들이 있다.
- 자료구조를 고치거나 필드값에 할당
- 자료구조를 제자리에서 수정
- 예외 발생, 오류로 실행 중단
- 파일 읽기/쓰기 등의 I/O 동작 수행 등...
// 순수함수의 예
fun main(args: Array<String>) {
println(pureFunction("FP") // Hello FP
println(pureFunction("FP") // Hello FP
println(pureFunction("FP") // Hello FP
}
fun pureFunction (iniput: String): String {
return "Hello " + input
}
- 몇 번을 호출하든 인자가 같으면 출력값도 같다.
// 비 순수함수의 예1
fun main(args: Array<String>) {
println(nonPureFunction("FP") // Hello FP
println(nonPureFunction("FP") // Hello Hello FP
println(nonPureFunction("FP") // Hello Hello Hello FP
}
val strBuilder: StringBuilder = StringBuilder("Hello ")
fun nonPureFunction(input: String): String {
return strBuilder.append(input).toString()
}
- 출력값이 매번 다르다.
- 동시성을 생각했을 때 (멀티스레드) 굉장히 프로그래밍이 복잡해진다.
- 디버깅, 에러 확인이 힘들다.
- 테스트 코드 작성이 힘들다.
// 비 순수함수의 예2
var x =1
fun nonPureFunction2(input: Int): Int{
return input+x // 외부 변수 참조
}
- 외부 변수를 참조하고 있다.
- 외부 변수가 언제 바뀔지 모르므로 값이 변경될 가능성이 있다.
- 순수함수가 아닌 것이 나쁜 코드라는 것은 아니라 단지 함수형 프로그래밍이라는 패러다임일 뿐이다.
- 모듈화 수준이 높으면 재사용성이 높아진다.
- 궁극적으로 평가 시점이 무관하다는 특성으로 효율적인 로직을 구성하는 것이 함수형 프로그래밍의 패러다임이다.
- Side effect가 없으므로 병렬처리 (Thread)의 안정성을 확보할 수 있다. mutex 동기화를 고민하지 않아도 된다는 장점이 있다. Mutex 이해를 위한 참조링크
- 수학 세계에서는 '무언가를 저장하고 변경하고 불러온다'라는 개념이 없기 때문에 함수는 함수 자체로 독립적으로 존재한다.
- 프로그래밍을 하며
불변성
을 유지하기 위해서 꽤나 신경써 주어야 한다. 그래서 몇 가지 규칙들로 이를 유지하고자 한다.- 프로그램에서
변이Mutation
이 발생하는 근본 원인을 파악하고 대응해보자.
- 프로그램에서
- 상태를 변경하지 않는 것.
- 단순히 프로그램 변수를 변경하거나 재할당 하는 것을 의미하는 것만이 아니라 메모리에 저장된 값을 변경하는 모든 행위를 의미한다.
값에 의한 호출
과참조에 의한 호출
- 불변성을 지키고 싶다면 메모리 상태를 항상 염두에 두고 코드를 작성해야 한다.
이렇게 신경쓸 것들이 많은데, 왜 불변성을 지키고자 할까?
- 무분별한 상태의 변경을 막을 수 있다.
- 무분별한 상태 변경의 대표적인 예 : 전역 변수의 남용
- 상태의 변경을 추적하기 쉽다.
- 모든 프로그램에 대해 어떤 표현식expression e를 모두 그 표현식의 결과로 치환해도 프로그램에 아무 영향이 없다면 그 표현식 e는 참조에 투명하다(referentially transparent)고 할 수 있다.
- == 함수 외부의 영향을 받지 않는 것.
- 부수 효과를 없애는 것(외부 세계를 변경하지 않는 것)에 더해서 외부로부터 영향받지 않아야 한다.
- 함수의 결과는 입력 파라미터에만 의존하고 함수 외부 세계인 입력 콘솔, 파일, 원격, URL, 데이터베이스, 파일시스템 등에서 데이터를 읽지 않는다.
- 자기 충족적 self-contained
- 함수 외부에 의존하는 코드가 없고, 함수 사용자 입장에서 유효한 매개변수만 전달하면된다.
- 결정론적 deterministic
- 동일한 매개변수에 대해 항상 동일한 결과가 나온다.
- 예외 Exception 을 던지지 않는다.
- 에러들은 버그로 취급한다.
- 다른 코드가 예기치않게 실패하는 조건을 만들지 않는다.
- 매개 변수의 값을 변경하거나 함수 외부 데이터를 변경하지 않는다.
- 데이터베이스, 파일 시스템, 네트워크 등의 외부 기기로 인해 동작이 멈추지 않는다.
- 어떤 프로그래밍 언어의 함수 구현에서 함수를 인자로 넘길 수 있거나 반환할 수 있을 때, 함수를 일급객체로 취급한다고 한다.
- 함수를 인자로 받거나 결과로 반환하는 함수를 고차함수라고 한다.
- <-> 일차 함수
- 자바는 함수형 인터페이스를 사용하므로 엄밀히 말하면 함수형 언어는 아니지만, 원칙은 같다.
package wiki.namu.example;
import java.util.function.Function;
public class Main {
public static final Function<Integer, Integer> INCREASE = value -> value + 1;
public static void main(String[] args) {
System.out.println(apply(INCREASE, 9)); // 10
}
public static int apply(Function<Integer, Integer> fx, int value) {
return fx(value);
}
}
“A closure is the combination of a function and the lexical environment within which that function was declared.” 클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이다.
function outerFunc() {
var x = 10;
var innerFunc = function () { console.log(x); };
innerFunc();
}
outerFunc(); // 10
public class La {
public interface Calculator {
public int square(int number);
}
public static void main(String[] args) {
Calculator closure1 = (int n) -> {return n * n; };
Calculator closure2 = (n) -> {return n * n; };
Calculator closure3 = n -> n * n;
System.out.println(closure1.square(2));
}
}
- 자바는 람다를 사용하며, 사용할 때 반드시 인터페이스를 선언해야만 한다. 람다식 하단 참조
- 익명함수 anonymous function 이라고도 한다.
- 프로그래밍 언어에서 사용되는 '람다 함수'도 같은 의미이다.
- 수학에서 사용하는 함수(메소드)를 보다 단순하게 표현하는 방법이다.
- 함수를 변수처럼 사용할 수 있다. (일급 시민으로 취급한다.)
- Java 8부터 기능이 포함되어 있다.
- 함수(메서드)를 간단한 ‘식’으로 표현하는 방법이다.
(a, b) -> a>b ? a: b
- 익명 함수 anonymous function
- 함수와 메서드의 차이점
- 근본적으로 동일하지만,
- 함수 : 일반적인 용어. 클래스에 독립적
- 메소드 : 객체지향개념의 용어이다. 클래스에 종속적이다.
- 근본적으로 동일하지만,
- 호출해서 사용하는 방식이 아닌, 구현부에서 바로 함수처리를 하여 내부에서 직접 기능을 처리하는 방식
- 반환 타임과 이름을 지운다.
()
와{}
사이에 화살표로 연결한다. - 반환값이 있는 경우 식이나 값만 적고
return
,;
생략 가능 - 매개변수의 타입이 추론 가능하면 대부분 생략 가능하다.
// 작성실습
int max(int a, int b) {
return a>b? a:b;
}
(a,b) -> a>b ? a : b
- 람다 사용시 반드시 함수형인터페이스를 먼저 정의해야 한다.
- Java에서 자주 사용되는 함수형 인터페이스
java.util.function
- 표 외에도 더 많다. 공식문서 참고자료
Function<T,R> | T -> R |
---|---|
BiFunction<T,U,R> | T,U -> R |
Consumer | T -> () |
Supplier | () -> T |
Predicate | T -> boolean |
UnaryOperator | Function<T,T> 와 같음. T -> T |
BinaryOperator | BiFunction<T,T,T>와 같음. T, T -> T |
- 매개변수가 1개인 경우, 타입 없다면 생략 가능
- 블록 안에 문장이 하나라면
{}
생략 가능. 끝에;
안 붙여도 된다.
- 익명 함수
- 특정 클래스에 종속되지 않는 함수이다.
- 익명 객체
Object
이다.- 자바에서는 메소드만 존재할 수 없으니까
- 이를 객체로 다루기 위해선 함수형 인터페이스
- 전달이 가능하다.
- 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
- 간결해져 가독성이 향상된다.
- 멀티 스레드 환경에서 용이하다. (함수형 프로그래밍의 특징)
- 람다로 인한 무명함수는 재사용이 불가능하다. (항상 새 인스턴스로 할당해야 한다.)
- 디버깅이 좀 힘들다.
- 무분별한 람다 사용은 코드 가독성을 해친다.
- 재귀로 만들기 부적합하다.