erlang : 여러 Case / If 문이 내포되었을 때 복잡도 감소?

    요약

    • 중첩 Case 문을 사용하는 것은 코드를 읽기 어렵게 만듦. 
    • 중첩 Case 문의 컨텍스트를 유지할 필요가 없다면, Case문에 Case를 중첩하지 말고 메서드로 빼서 Flat한 Case를 만드는 것이 좋을 것으로 판단됨. 

    함수로 풀어내기

    이 코드는 리트코드 72번 문제를 풀면서 작성한 코드다. 

    sol(_, _, I1, I2, DP) when I1 == 0 orelse I2 == 0->
      {abs(I2-I1), DP};
    sol(W1, W2, I1, I2, DP) ->
      {NewT1, NewDP} = case get(I1, I2, DP) of
        none ->
          case maps:get(I1, W1) =:= maps:get(I2, W2) of
            true ->
              sol(W1, W2, I1-1, I2-1, DP);
            _ ->
              {T1, NewDp1} = sol(W1, W2, I1, I2-1, DP),
              {T2, NewDp2} = sol(W1, W2, I1-1, I2, NewDp1),
              {T3, NewDP3} = sol(W1, W2, I1-1, I2-1, NewDp2),
              NewT = lists:min([T1, T2, T3]) + 1,
              {NewT, NewDP3}
          end;
        V -> {V, DP}
      end,
      UpdatedDp = update(I1, I2, NewT1, NewDP),
      {NewT1, UpdatedDp}.

    위 코드에서 내가 생각하는 문제점은 중첩 case 문을 사용했다는 것이다. 어떻게 읽기 어려워질까? 

    첫번째 case 문에서 none일 때, 수행해야하는 코드 블락이 너무 크다. 이것 때문에 우선 첫번째 case 문에서 none이 아닐 때 어떤 코드가 수행되어야 하는지에 대한 문맥을 기억에서 유지하는 것이 어렵다. 실행되어야 하는 코드 블락이 커지면 커질수록 더욱 어려워진다. 예를 들면 V -> {V, DP} 인 것은 알겠어, 그런데 어떤 코드가 실행되었을 때 V라는거지? 를 생각하며 case get(I1, I2, DP) of  코드를 다시 한번 살펴봐야한다. 

    이 문제는 첫번째 case 문에서 각 경우에 실행되어야 하는 코드를 줄여서 해결할 수 있다.

    두번째 case 문도 여전히 어렵다. 왜냐하면 첫번째 case 문에 none이라는 조건이 매칭되었다는 컨텍스트를 코드를 읽는 내내 유지해야하기 때문이다. 특히 중첩 Case문 안에서 실행되고 있기 때문에 반드시 컨텍스트를 유지해야만 한다. 만약 컨텍스트 유지가 필요없다면 오히려 Case 문 안에서 Case를 한번 더 실행하기 보다는 함수로 분리하는 것이 더 나은 것 같다. 

    sol(_, _, I1, I2, DP) when I1 == 0; I2 == 0->
      {abs(I2-I1), DP};
    sol(W1, W2, I1, I2, DP) ->
      {NewT1, NewDP} = case get(I1, I2, DP) of
        none -> handle_none_case(W1, W2, I1, I2, DP);
        V -> {V, DP}
      end,
      UpdatedDp = update(I1, I2, NewT1, NewDP),
      {NewT1, UpdatedDp}.
    
    handle_none_case(W1, W2, I1, I2, DP) ->
      case maps:get(I1, W1) =:= maps:get(I2, W2) of
        true -> sol(W1, W2, I1-1, I2-1, DP);
        _ -> handle_diff_case(W1, W2, I1, I2, DP)
      end.
    
    handle_diff_case(W1, W2, I1, I2, DP) ->
      {T1, NewDp1} = sol(W1, W2, I1, I2-1, DP),
      {T2, NewDp2} = sol(W1, W2, I1-1, I2, NewDp1),
      {T3, NewDP3} = sol(W1, W2, I1-1, I2-1, NewDp2),
      NewT = lists:min([T1, T2, T3]) + 1,
      {NewT, NewDP3}.

    결론적으로는 다음과 같이 분리할 수 있다. 사실 '조삼모사' 처럼 보일 수 있지만, 우선은 이렇게 분리하면서 중첩 Case 문을 제거할 수 있어서 결론적으로는 읽기 더 편해진 것 같다. 

     


    패턴 매칭으로 풀어내기

    sol(_, _, I1, I2, DP) when I1 == 0 orelse I2 == 0->
      {abs(I2-I1), DP};
    sol(W1, W2, I1, I2, DP) ->
      {NewT1, NewDP} = case get(I1, I2, DP) of
        none ->
          case maps:get(I1, W1) =:= maps:get(I2, W2) of
            true ->
              sol(W1, W2, I1-1, I2-1, DP);
            _ ->
              {T1, NewDp1} = sol(W1, W2, I1, I2-1, DP),
              {T2, NewDp2} = sol(W1, W2, I1-1, I2, NewDp1),
              {T3, NewDP3} = sol(W1, W2, I1-1, I2-1, NewDp2),
              NewT = lists:min([T1, T2, T3]) + 1,
              {NewT, NewDP3}
          end;
        V -> {V, DP}
      end,
      UpdatedDp = update(I1, I2, NewT1, NewDP),
      {NewT1, UpdatedDp}.

    기본 코드는 이런 상태다. 여기서 문제가 되는 부분은 case를 2번 써서 Depth가 2번 되었다는 것이다. 이 부분을 줄이기 위해 패턴매칭을 사용해 볼 수 있다. 

    sol(W1, W2, I1, I2, DP) ->
      {NewT1, NewDP} = case validate(I1, I2, W1, W2, DP) of
                         {none, true}  -> sol(W1, W2, I1-1, I2-1, DP);
                         {none, false} -> 
                            {T1, NewDp1} = sol(W1, W2, I1, I2-1, DP),
                            {T2, NewDp2} = sol(W1, W2, I1-1, I2, NewDp1),
                            {T3, NewDP3} = sol(W1, W2, I1-1, I2-1, NewDp2),
                            NewT = lists:min([T1, T2, T3]) + 1,
                            {NewT, NewDP3}
                         {V, _} -> {V, DP}
                       end,
      UpdatedDp = update(I1, I2, NewT1, NewDP),
      {NewT1, UpdatedDp}.
      
      
    validate(I1, I2, W1, W2, DP) ->
      First = get(I1, I2, DP),
      Second = maps:get(I1, W1) =:= maps:get(I2, W2),
      {First, Second}.

    Case 조건문이 중첩되는 것은 일반적인 언어에서는 If 조건문이 2-Depth를 가지게 되는 것을 의미한다. 이 2-Depth를 줄이기 위해 2개의 조건 결과를 튜플로 묶어주는 함수를 하나 추가하고, 그 함수가 Case of 문에 오도록 하는 것이다. 

    이렇게 작성하면 패턴 매칭을 이용해 Case 문의 Depth를 줄이는데 사용할 수 있다. 그러나 이 방법은 3-Depth 이상을 사용하는 것은 어려울 수 있다. 왜냐하면 최대 8가지 경우의 수를 하나의 함수 블락에 표현해야되기 때문이다. 

     

     

     

     

    댓글

    Designed by JB FACTORY