순수함수 (Pure Function)

함수는 주어진 입력으로 계산하는 것 이외에 프로그램의 실행에 영향을 미치지 않아야 하며, 이를 부수 효과(side effect)가 없어야 한다고 한다. 이러한 함수를 순수 함수라고 한다. 이론은 쉬우나 구현은 조금은 다른것 같다.

  • 순수 함수는 참조 투명성을 보장. 입력값이 같다면 결과값도 항상 같다.
  • 스레드가 다루는 데이터 불변이고 사용하는 함수가 순수하다면, 스레드가 아무리 많더라도 문제가 되지 않음
  • 이런 이유로 함수형 프로그래밍은 멀티스레딩에 강하다.

입력에 따라 출력이 달라진다.

  • 인자가 같다면 순수 함수의 결과는 일정해야 한다. (참조투명성)
  • 함수의 결과는 오직 입력된 파라미터에만 의존해야한다. 함수 외부의 어떤 값에도 의존하지 않는다.
    • 전역변수, 파일이나 네트워크로 부터 읽지 않아야 한다.
package chapter3;

import java.util.ArrayList;
import java.util.List;

public class Customer {
    // 모든 고객 정보리스트
    static public ArrayList<Customer> allCustomers = new ArrayList<Customer>();
    public Integer id = 0;
    public String name = "";
    public String address = "";
    public String state = "";
    public String primaryContact = "";
    public String domain = "";
    public Boolean enabled = true;
    public Contract contract;

    public Customer() {
    }

    public interface Function1<A1, B> {
        public B call(A1 in1);
    }

    /**
     * 초기화
     */
    static {
        for (int i = 0; i < 10; i++) {
            Customer customer = new Customer();
            customer.name = "name : " + i;
            customer.address = "address : " + i;
            customer.state = "state : " + i;
            customer.primaryContact = "primaryContact : " + i;
            customer.domain = "domain : " + i;
            customer.enabled = i % 2 == 0;
            allCustomers.add(customer);
        }
    }

    public Customer setCustomerId(Integer customer_id) {
        this.id = customer_id;
        return this;
    }

    public Customer setName(String name) {
        this.name = name;
        return this;
    }

    public Customer setState(String state) {
        this.state = state;
        return this;
    }

    public Customer setDomain(String domain) {
        this.domain = domain;
        return this;
    }

    public Customer setEnabled(Boolean enabled) {
        this.enabled = enabled;
        return this;
    }

    public Customer setContract(Contract contract) {
        this.contract = contract;
        return this;
    }

    /**
     * 고객정보를 가지고오는 정보 클래스
     */
    public static List<String> getEnabledCustomerAddresses() {
        return getEnabledCustomerField(new Function1<Customer, String>() {
            public String call(Customer customer) {
                return customer.address;
            }
        });
    }

    public static List<String> getEnabledCustomerNames() {
        return getEnabledCustomerField(new Function1<Customer, String>() {
            public String call(Customer customer) {
                return customer.name;
            }
        });
    }

    public static List<String> getEnabledCustomerStates() {
        return getEnabledCustomerField(new Function1<Customer, String>() {
            public String call(Customer customer) {
                return customer.state;
            }
        });
    }

    public static List<String> getEnabledCustomerPrimaryContacts() {
        return getEnabledCustomerField(new Function1<Customer, String>() {
            public String call(Customer customer) {
                return customer.primaryContact;
            }
        });
    }

    public static List<String> getEnabledCustomerDomains() {
        return getEnabledCustomerField(new Function1<Customer, String>() {
            public String call(Customer customer) {
                return customer.domain;
            }
        });
    }

    public static <B> List<B> getEnabledCustomerField(Function1<Customer, B> func) {
        ArrayList<B> outList = new ArrayList<B>();
        for (Customer customer : Customer.allCustomers) {
            if (customer.enabled) {
                outList.add(func.call(customer));
            }
        }
        return outList;
    }

    /**
     * id에 해당하는 고객이 존재하면 반환하고, 그렇지 않으면 null을 반환하는 간단한 메소드
     *
     * @param customer_id
     * @return
     */
    public static Customer getCustomerById(Integer customer_id) {
        for (Customer customer : Customer.allCustomers) {
            if (customer.id == customer_id) {
                return customer;
            }
        }
        // 이걸 대체 무슨 의미로 받아들여야 할까요?
        //  - 에러인가? 못찾았다는 것인가?
        return null;
    }

    /**
     * 프로그램 오류는 아닌데도 호출자에게 ‘고객을 찾지 못했으니 예외를 던지는 거 야’라고 말하는 건 좀 이상하므로 위의 getCustomerById 보완
     *
     * @param customer_id
     * @return
     */
    public static ArrayList<Customer> getCustomerListById(Integer customer_id) {
        ArrayList<Customer> outList = new ArrayList<Customer>();
        for (Customer customer : Customer.allCustomers) {
            if (customer.id == customer_id) {
                outList.add(customer);
            }
        }
        return outList;
    }

    /**
     * 일급함수이지만 순수함수는 아니다. 참조된 allCustomers값에 의해서 변경될 소지가 있기 때문이다
     *
     * @param func
     * @return
     */
    public static ArrayList<Customer> filter(Function1<Customer, Boolean> func) {
        ArrayList<Customer> outList = new ArrayList<Customer>();
        for (Customer customer : Customer.allCustomers) {
            if (func.call(customer)) {
                outList.add(customer);
            }
        }
        return outList;
    }

    /**
     * filter 메소드를 이용해서 getCustomerListById 개선한 메소드
     * 함수를 더 작은 순수 함수들로 나누어 작성하면 코드의 전반적인 기능을 잘 이해하고 파악하는 데 도움이 될 수 있음
     *
     * @param customer_id
     * @return
     */
    public static ArrayList<Customer> getCustomerListFilterById(final Integer customer_id) {
        return Customer.filter(new Function1<Customer, Boolean>() {
            public Boolean call(Customer customer) {
                return customer.id == customer_id;
            }
        });
    }

    /**
     *
     * @param function1
     * @param function2
     * @param <B>
     * @return
     */
    public static <B> List<B> getField(Function1<Customer, Boolean> function1, Function1<Customer, B> function2) {
        ArrayList<B> outList = new ArrayList<B>();
        for (Customer customer : Customer.filter(function1)) {
            outList.add(function2.call(customer));
        }
        return outList;
    }

    // 테스트
    public static void main(String[] args) {
        List<String> addresses = getEnabledCustomerAddresses();
        for (String address : addresses) {
            System.out.print(address + " ");
        }
        System.out.println();
        List<String> names = getField(
                new Function1<Customer, Boolean>() {
                    @Override
                    public Boolean call(Customer in1) {
                        return in1.enabled;
                    }
                },
                new Function1<Customer, String>() {
                    @Override
                    public String call(Customer in1) {
                        return in1.name;
                    }
                });
        for (String name : names) {
            System.out.print(name + " ");
        }
    }


}

함수를 순수하게

/**
 * 순수함수 컨셉으로 작업한 클래스
 */
public class FunctionalConcepts {

    private FunctionalConcepts() {
    }

    /**
     * @param inList
     * @param customer_id
     * @return
     */
    public static ArrayList<Customer> getCustomerById(ArrayList<Customer> inList, final Integer customer_id) {
        return filter(inList, new Function1<Customer,
                Boolean>() {
            public Boolean call(Customer customer) {
                return customer.id == customer_id;
            }
        });
    }

    /**
     * @param customer_id
     */
    public static void setContractDisabledForCustomer(ArrayList<Customer> inList, Integer customer_id) {
        for (Customer customer : inList) {
            if (customer.id == customer_id) {
                customer.contract.enabled = false;
            }
        }
    }

    /**
     * @param customer_id
     */
    public static void setContractEnabledForCustomer(ArrayList<Customer> inList, Integer customer_id) {
        for (Customer customer : getCustomerById(inList, customer_id)) {
            customer.contract.enabled = true;
        }
    }

    /**
     *
     * @param inList
     * @param customer_id
     * @param status
     */
    public static void setContractForCustomer(ArrayList<Customer> inList, Integer customer_id, final Boolean status) {
        foreach(getCustomerById(inList, customer_id),
                new Foreach1<Customer>() {
                    public void call(Customer customer) {
                        customer.contract.enabled = status;
                    }
                });
    }

    /**
     *
     * @param customer_id
     * @param status
     * @return
     */
    public static List<Contract> setContractForCustomer(Integer customer_id, final Boolean status) {
        return map(getCustomerById(Customer.allCustomers, customer_id), new Function1<Customer, Contract>() {
                    public Contract call(Customer customer) {
                        return customer.contract.setEnabled(status);
                    }
                }
        );
    }

    /**
     * @param inList
     * @param func
     * @param <A1>
     * @param <B>
     * @return
     */
    public static <A1, B> List<B> map(List<A1> inList, Function1<A1, B> func) {
        ArrayList<B> outList = new ArrayList<B>();
        for (A1 obj : inList) {
            outList.add(func.call(obj));
        }
        return outList;
    }

    /**
     * @param inList
     * @param func
     * @param <A>
     */
    public static <A> void foreach(ArrayList<A> inList, Foreach1<A> func) {
        for (A obj : inList) {
            func.call(obj);
        }
    }

    /**
     * @param inList
     * @param func
     * @param <A>
     * @return
     */
    public static <A> ArrayList<A> filter(ArrayList<A> inList, Function1<A, Boolean> func) {
        ArrayList<A> outList = new ArrayList<A>();
        for (A obj : inList) {
            if (func.call(obj)) {
                outList.add(obj);
            }
        }
        return outList;
    }

    /**
     * @param inList
     * @param function1
     * @param function2
     * @param <B>
     * @return
     */
    public static <B> List<B> getField(ArrayList<Customer> inList,
                                       Function1<Customer, Boolean> function1,
                                       Function1<Customer, B> function2) {
        ArrayList<B> outList = new ArrayList<B>();
        for (Customer customer : filter(inList, function1)) {
            outList.add(function2.call(customer));
        }
        return outList;
    }

}

부수효과 (인자로 전달된 객체 필드를 건드는 일)

  • 함수의 외부의 변수를 변경하거나 파일이나 네트워크로 데이터를 내보내지 않는다.
  • 예외도 부수효과이다.
  • 부수효과를 제거해야 순수함수요건이 완성됨
  • 순수 함수에 가까울수록 문제를찾기가 수월하게 됨. 쉬운 테스트 가능

정리하기

  • 고차 함수로 코드 중복을 줄이고 순수 함수를 만들면 테스트하기 좋아지고 함수의 이해도가 높아진다.
  • 이책에서는 문법이 자바와 유사하고 자바로 코딩할 수 있다는 점을 들어 그루비를 대체 언어로 선정. ( filter, map에대응되는 findAll, collect 고차함수를 내장하였음 )

그루비란

그루비 문법에 맞게 리팩토링

// 널 체크가 필요없는 안전한 방법
def getCustomerById(Integer customerId) { 
   Customer.allCustomers
   .findAll({ customer -> customer.id == customerId })
}

// id와 동일한 고객 리스트를 가지고와서 그 고객상태를 false 변경하고 계약내용을 확인하는 함수 
def setContractForCustomer(Integer customerId) { 
   Customer.allCustomers.findAll({ 
       customer -> customer.id == customerId }).collect({ 
       customer -> customer.contract.setEnabled(false) }).each({ 
       contract -> println contract })
}
import java.util.ArrayList;
import java.util.List;

public class Customer {

    static public ArrayList<Customer> allCustomers = new ArrayList<Customer>();
    public Integer id = 0;
    public String name = "";
    public String address = "";
    public String state = "";
    public String primaryContact = "";
    public String domain = "";
    public Boolean enabled = true;
    public Contract contract;

    public Customer() {}


    public Customer setCustomerId(Integer customer_id) {
        this.customer_id = customer_id;
        return this;
    }

    public Customer setName(String name) {
        this.name = name;
        return this;
    }

    public Customer setState(String state) {
        this.state = state;
        return this;
    }

    public Customer setDomain(String domain) {
        this.domain = domain;
        return this;
    }

    public Customer setEnabled(Boolean enabled) {
        this.enabled = enabled;
        return this;
    }

    public Customer setContract(Contract contract) {
        this.contract = contract;
        return this;
    }

    static def EnabledCustomer = { customer -> customer.enabled == true }
    static def DisabledCustomer = { customer -> customer.enabled == false }

    public static List<String> getDisabledCustomerNames() {
        Customer.allCustomers.findAll(DisabledCustomer).collect({ cutomer ->
            cutomer.name
        })
    }

    public static List<String> getEnabledCustomerStates() {
        Customer.allCustomers.findAll(EnabledCustomer).collect({ cutomer ->
            cutomer.state
        })
    }

    public static List<String> getEnabledCustomerDomains() {
        Customer.allCustomers.findAll(EnabledCustomer).collect({ cutomer ->
            cutomer.domain
        })
    }

    public static List<String> getEnabledCustomerSomeoneEmail(String someone) {
        Customer.allCustomers.findAll(EnabledCustomer).collect({ cutomer ->
            someone + "@" + cutomer.domain
        })
    }

    public static ArrayList<Customer> getCustomerById(ArrayList<Customer> inList, final Integer customer_id) {
        inList.findAll({ customer -> customer.customer_id == customer_id })
    }
}

results matching ""

    No results matching ""