2018년 8월 19일 일요일

[java]NULL에게서 도망가기 위한 여정 - 01

출처 : 하스켈로 배우는 함수형 프로그래밍

https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare

NullPointerException은 가장 많이 보이고 짜증나는 예외이다.
대부분의 언어에서 null은 아주 흔하게 존재한다.
이 녀석은 대부분 에러를 낼 때만 만나게 되는데 우리는 이 null이 없이는 코딩을 할 수 없을 것 같은 느낌을 가지고 있다.
그 만큼 null은 우리의 코딩방식을 반영한다.
null이라는 건 빈 박스를 취하는 것이다. 무엇을 넣을지는 모르겠지만 일단 취하는 것이다.
그 박스를 제공하기 위해 컴퓨터는 빈 공간을 제공해줄 것이다.

만약 빈 박스에 내용으로 뭔가 하려 한다면? 자바에서는 NullPointerException이 날 것이다.

시대가 조금씩 바뀌고 있다. NULL을 모두 체크해야 하는 것으로는 한계가 있다.
요즘에는 Optional이라는 개념이 많이 유입되고 있는 것 같다.
자바의 경우에도 java.util.Optional 이라는 녀석이 생겼다. 그래서 Optional에서 값이 있는지 없는지를 체크할 수 있도록 한다.
이제 if/else문에 대한 처리를 Optional 내부에서 처리를 할 수 있기 때문에 코드가 복잡할 필요가 없어진다.

package org.test;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Optional;
import java.util.function.BiFunction;

public class TestOptional {
 // 공백으로 분할하기, 세 개로 분할할 수 없으면 무효!
 private static Optional words(String expr) {
  String[] res = expr.split(" ");
  if (3 == res.length) {
   return Optional.of(res);
  }
  else {
   return Optional.empty();
  }
 }
 
 // 문자열을 정수로 변환, 변활할 수 없으면 무효
 private static Optional toNum(String s) {
  try {
   return Optional.of(Integer.parseInt(s));
  } catch (NumberFormatException ex) {
   return Optional.empty();
  }
 }
 
 private static Optional add(Integer a, Integer b) {
  return Optional.of(a + b);
 }
 private static Optional sub(Integer a, Integer b) {
  return Optional.of(a - b);
 }
 private static Optional mul(Integer a, Integer b) {
  return Optional.of(a * b);
 }
 private static Optional div(Integer a, Integer b) {
  if (a != b) {
   return Optional.of(a / b);
  }
  else  {
   return Optional.empty();
  }
 }
 
 // +-*/ 외 문자 무효
 private static Optional>> toBinOp(String s) {
  switch(s) {
   case "+" : return Optional.of(TestOptional::add);
   case "-" : return Optional.of(TestOptional::sub);
   case "*" : return Optional.of(TestOptional::mul);
   case "/" : return Optional.of(TestOptional::div);
   default : return Optional.empty();
  }
 }

 public static void main(String[] args) throws IOException {
  
  String expr = new BufferedReader(new InputStreamReader(System.in)).readLine();
  
  // 마치 haskell의 Monad같은 모습.
  System.out.println(
   words(expr)
    .flatMap(ss -> toNum(ss[0])
     .flatMap(a -> toBinOp(ss[1])
      .flatMap(op -> toNum(ss[2])
       .flatMap(b -> op.apply(a, b)))))
    .map(n -> "" + n)
    .orElseGet(() -> "invalid"));   
  }
 }
}

여기서 flatMap이 뭔가 하면 Optional을 받아서 Optional을 리턴하는 녀석인데, Optional의 내용이 empty()면 다음 flatMap으로 전파되지 않는다.
예를들어보자.
Optional str = Optional.ofNullable("AB");
//Optional str = Optional.ofNullable("ABC");
//Optional str = Optional.ofNullable("ABCD");
str.flatMap(ss -> {
 if (ss.length() > 3) {System.out.println("FlatMap1 fail");return Optional.empty();}
 else {System.out.println("flatMap1 pass");  return Optional.of(ss.toLowerCase());}
}).flatMap(ss2 -> {
 if (ss2.length() > 4) {System.out.println("FlatMap2 fail"); return Optional.empty();}
 else {System.out.println("FlatMap2 pass");return Optional.of(ss2.toUpperCase() + "@#$");}
}).ifPresent(System.out::println);
저 위에 있는 것들로 테스트를 해보면 empty()를 뱉는 순간 다음 내용으로 if-else문으로 이어지지 않음을 알 수 있다.

여기서 알아야 할 점은
NULL 체크를 flatMap에서 전부한다는 것이다. 그런데 온통 flatMap이 있으니!
NULL 체크를 모든 곳에서 하는 것이다.
왜 flatMap이 계속 함수 내부로 형성되는지 의아할 것이다.
각 토큰을 Optional로 리턴한 OPtional 값을 하나하나 검사하려고 flatMap을 사용하는 것이다.
그리고 첫번째 flatMap의 값 ss를 함께 사용하려는 의도도 있을 것이다.

게다가 마지막으로 flatMap은 인스턴스 메소드다. 무슨 말이냐면 객체에서 불러내야 사용할 수 있는 것이다.
Optional 객체가 있을 때, 그 객체에서 flatMap 메소드를 호출해야 가능하다. 그러므로 Optional값이 하나 나오면
그때 검사하고 다른 Optional값이 나오면 또 검사할 flatMap을 그때그때(전부) 호출한다.

이제 이 내용을 하스켈로 보자.
http://www.whynam.com/2018/08/haskell-null-02.html

댓글 없음:

댓글 쓰기