Skip to content

Instantly share code, notes, and snippets.

@new-pow
Last active February 1, 2023 04:42
Show Gist options
  • Save new-pow/0db33048036b267f54db4ad712f6503b to your computer and use it in GitHub Desktop.
Save new-pow/0db33048036b267f54db4ad712f6503b to your computer and use it in GitHub Desktop.
IIRIN - CS08

정수 분류기

[개요]


👩🏻‍💻 구현 미션1. 순수함수 만들기

학습목표

  • 불변성 (Immutable) 값이나 변수를 적극 활용할 수 있다.
  • 함수가 참조 투명성을 지키고, 부작용을 줄일 수 있도록 구현할 수 있다.
  • 순수함수 (Pure Function) 로 구현할 수 있다.

기능 요구사항

  • 각 언어로 만들어진 다음 2개 클래스에서 중복된 코드를 줄이고
  • 함수형 표현으로 최대한 개선한다.
  • 1부터 100까지 숫자를 각 함수에 넣고 동작 결과가 동일해야 한다.

구현과정 ClassifierAlpha, PrimeAlpha

0. 코드 분석

1. 전역변수부터 삭제 후 매개변수로 입력받는 구조로 변경

  • 전역변수 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));
}

2. Iteratorstream API로 변경

static public int sum(Set<Integer> factors) {   // 약수의 합
    return factors.stream()
            .mapToInt(Integer::intValue)
            .sum();
}

3. 지정된 범위의 연속된 정수 stream API로 변경

public int factorsSum(int number) {
    return  IntStream.rangeClosed(1,number)
            .filter(i -> isFactor(number,i)).sum(); // 바로 int로 반환하게 하여 불필요한 함수를 제거
}

4. 공통함수 부모 클래스로 분리

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();  // 다른 함수 재사용 하는 것으로 수정
    }
}

👩🏻‍💻 구현 미션2. 고차함수 활용하기

학습 목표

  • 클로저를 선언해서 매개변수 또는 리턴값으로 전달할 수 있다
  • map, filter, reduce 고차 함수를 활용할 수 있다
  • 클로저 관련된 다양한 표현을 학습한다

기능 요구사항

  • 앞서 작성한 자연수 분류 ClassifierAlpha, PrimeAlpha 를 이용한다.
  • 2-100까지 자연수 중에서 각각 다음의 목록을 출력한다.
    • 완전수(perfect)
    • 과잉수(Abundant)
    • 부족수(Deficient)
    • 소수(Prime)
    • 정사각수(Squared)

프로그래밍 요구사항

  • map, filter, reduce 고차 함수를 활용한다.
  • 출력을 위해서는 반드시 클로저(또는 람다)를 선언하고 반복문 대신 reduce를 활용해서 출력해야 한다.
  • 자연수 중에서 다른 자연수의 제곱으로 표현되는 정사각수(squared) 판단 함수를 추가한다

구현 과정

1. 함수형 인터페이스 선언

  • 기존 클래스 메서드들을 함수형 인터페이스로 바꾸어 정의해주었다.
// 변경 전
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());

2. 정사각수 판별 구현

import java.util.function.IntPredicate;

public class SquareAlpha {
    public static final IntPredicate isSquare =
            (number) -> (int)(Math.sqrt(number) * 100) == (int)Math.sqrt(number) * 100;
}
  • 제일 간단한 방법으로 구현하고 싶어서 위와 같은 방법으로 구현하였다.
    • 정수로 나눠떨어져야지만 메소드 내부 등호가 성립하게된다.

3. 출력 구현

  • 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 APIreduce()를 활용하여 다음과 같이 출력하였다.
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 가 없다 라고도 한다.
  • 참조투명성 : 외부 상태를 변경하지도 않고 외부의 상태로부터 영향 받지 않는다.

부수효과 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이 발생하는 근본 원인을 파악하고 대응해보자.

불변성과 상태

  • 상태를 변경하지 않는 것.
  • 단순히 프로그램 변수를 변경하거나 재할당 하는 것을 의미하는 것만이 아니라 메모리에 저장된 값을 변경하는 모든 행위를 의미한다.
    • 값에 의한 호출참조에 의한 호출
    • 불변성을 지키고 싶다면 메모리 상태를 항상 염두에 두고 코드를 작성해야 한다.

불변성을 지키는 것의 장점

이렇게 신경쓸 것들이 많은데, 왜 불변성을 지키고자 할까?

  1. 무분별한 상태의 변경을 막을 수 있다.
    • 무분별한 상태 변경의 대표적인 예 : 전역 변수의 남용
  2. 상태의 변경을 추적하기 쉽다.

참조링크

참조투명성

  • 모든 프로그램에 대해 어떤 표현식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부터 기능이 포함되어 있다.

람다식 Lambda Expression

  • 함수(메서드)를 간단한 ‘식’으로 표현하는 방법이다.
(a, b) -> a>b ? a: b
  • 익명 함수 anonymous function
  • 함수와 메서드의 차이점
    • 근본적으로 동일하지만,
      • 함수 : 일반적인 용어. 클래스에 독립적
      • 메소드 : 객체지향개념의 용어이다. 클래스에 종속적이다.

람다식 사용법

  • 호출해서 사용하는 방식이 아닌, 구현부에서 바로 함수처리를 하여 내부에서 직접 기능을 처리하는 방식
  1. 반환 타임과 이름을 지운다. (){} 사이에 화살표로 연결한다.
  2. 반환값이 있는 경우 식이나 값만 적고 return, ; 생략 가능
  3. 매개변수의 타입이 추론 가능하면 대부분 생략 가능하다.
// 작성실습
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이다.
    • 자바에서는 메소드만 존재할 수 없으니까
    • 이를 객체로 다루기 위해선 함수형 인터페이스
  • 전달이 가능하다.
    • 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
  • 간결해져 가독성이 향상된다.
  • 멀티 스레드 환경에서 용이하다. (함수형 프로그래밍의 특징)

람다의 단점

  • 람다로 인한 무명함수는 재사용이 불가능하다. (항상 새 인스턴스로 할당해야 한다.)
  • 디버깅이 좀 힘들다.
  • 무분별한 람다 사용은 코드 가독성을 해친다.
  • 재귀로 만들기 부적합하다.
import java.util.function.IntPredicate;
public class ClassifierAlpha extends Factor{
public static final IntPredicate isPerfect = (int number) -> factorsSum.apply(number) - number == number;
public static final IntPredicate isAbundant = (int number) -> factorsSum.apply(number) - number > number;
public static final IntPredicate isDeficient = (int number) -> factorsSum.apply(number) - number < number;
public static void main(String[] args) {
System.out.println(ClassifierAlpha.isPerfect.test(10));
System.out.println(ClassifierAlpha.isPerfect.test(6));
}
}
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.IntFunction;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class Factor {
protected static final BiPredicate<Integer, Integer> isFactor =
(number, potentialFactor) -> number % potentialFactor == 0;
protected static final IntFunction<Set<Integer>> factors =
(number) -> IntStream.rangeClosed(1, number)
.filter(i -> isFactor.test(number, i))
.boxed()
.collect(Collectors.toSet());
protected static final IntFunction<Integer> factorsSum =
(int number) -> factors.apply(number).stream()
.mapToInt(i->i)
.sum();
}
import org.junit.jupiter.api.Test;
import java.util.function.IntFunction;
import java.util.stream.IntStream;
class missionTest {
@Test
void factorsTest() {
int number = 10;
IntStream.rangeClosed(1, number)
.filter(i -> number%i==0)
.forEach(System.out::println);
}
@Test
void classifierAlpha() {
for (int i=1; i<101; i++) {
System.out.println(i + " classifier: "+ ClassifierAlpha.isPerfect.test(i));
}
}
@Test
void primeAlphaTest() {
for (int i=1; i<101; i++) {
System.out.println(i + " prime: "+ PrimeAlpha.isPrime.test(i));
}
}
@Test
void squareAlphaTest() {
for (int i=2; i<101; i++) {
System.out.println(i + " square: "+SquareAlpha.isSquare.test(i));
}
}
@Test
void mission2Test() { // 리팩토링 전 함수
StringBuilder sb;
for (int i=2; i<101; i++) {
System.out.println(i + " :"+ (PrimeAlpha.isPrime.test(i) ? " prime " : "")
+ (ClassifierAlpha.isPerfect.test(i)? " perfect " : "")
+ (ClassifierAlpha.isDeficient.test(i) ? " deficient " : "")
+ (ClassifierAlpha.isAbundant.test(i) ? " abundant " : ""));
}
}
@Test
void mission2TestReduce() {
String reduce = IntStream.rangeClosed(2, 100)
.mapToObj(NumberInfo.getInfo::apply)
.reduce("", (a, b) -> a + b +"\n");
System.out.println(reduce);
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.function.IntFunction;
public class NumberInfo {
public static final IntFunction<String> getInfo = // 메서드에 final 붙으면 overriding이 안됨. 함수가 바뀌지 않음.
i -> {
List<String> info = new 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);
};
}
import java.util.HashSet;
import java.util.Set;
import java.util.function.IntPredicate;
public class PrimeAlpha extends Factor{
public static final IntPredicate isPrime = (int number) -> { // 이거 어떻게 고치지....
Set<Integer> primeSet = new HashSet<>(){ {add(1); add(number);} };
return number > 1 && factors.apply(number).equals(primeSet);
};
public static void main(String[] args) {
System.out.println(PrimeAlpha.isPrime.test(10));
System.out.println(PrimeAlpha.isPrime.test(7));
}
}
import java.util.function.IntPredicate;
public class SquareAlpha {
public static final IntPredicate isSquare =
(number) -> (int)(Math.sqrt(number) * 100) == (int)Math.sqrt(number) * 100;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment