Today I Learned(23.03.12)

MilkyMilky·2023년 3월 12일
0

Today I Learned (23.03.12)

으아아… 오늘 공부는 정말 힘들었다..

  • Enum 모듈에서 지원하는 라이브러리 몇가지

list = [1,2,3,4,5]

all?: 컬렉션의 모든 요소들이 부합하는지에 대한 명제

iex> Enum.all?(list,&(&1 < 4))
false

each: 컬렉션의 각 요소에 함수를 적용하는 메소드

iex> Enum.each(list, &(IO.puts(&1))
1
2
3
4
5
:ok

filter: 컬렉션의 조건으로 값을 선택하는 메소드

iex> Enum.filter(list,&(&1 > 2))
[3,4,5]

split: n개의 수만큼 가진 리스트로 나누기

iex> Enum.split(list,3)
{[1,2,3], [4,5]}

take: n개의 수만큼 가진 리스트를 반환

iex> Enum.take(list,3)
[1,2,3]

flatten: 리스트의 모든 요소들을 1차원 배열로 바꾸어 반환

iex> Enum.flatten([1,[2,3,[4,5]],[[[6]]])
[1,2,3,4,5,6]

오늘 공부 한 것은 위에 있는 함수들을 전부 스스로 구현해보는 것이였다…

  1. all

내가 구현한 코드

def all(list, func, result \\ true)

  def all(_, _, result) when result == false do
    false
  end

  def all([], _, _), do: true

  def all([head | tail], fun, _) do
    result = fun.(head)
    all(tail, fun, result)
  end

all 함수는 크게 어렵지 않았다. 리스트의 각 요소에 함수를 적용하고, 만일 단 하나라도 false가 나오면 그대로 false를 반환한다.

만일 리스트가 끝까지 모두 true를 반환하면 그대로 true를 반환한다.

  1. each
def each([], _), do: :ok

def each([head | tail], func) do
    func.(head)
    each(tail, func)
end

each도 그렇게 어렵지 않았다. 그저 리스트에 각 요소에 함수를 적용하고, 리스트의 끝까지 갔을 경우에는 atom인 :ok를 반환하여 마무리 한다.

  1. filter
def filter(list, func, result \\ [])

def filter([], _, result) do
    reverse(result)
end

def filter([head | tail], func, result) do
    is_ok = func.(head)

    if is_ok do
      # easy way
      # filter(tail, func, result ++ [head])

      # hard way
      filter(tail, func, [head | result])
    else
      filter(tail, func, result)
    end
end

Filter 함수는 func함수에 부합하는 요소들을 result에 넣어서 반환한다. 여기서 문제는, 결과값이 거꾸로 나올수 있다는 것이다.

엘릭서는 리스트에 값을 넣는 방식이 [ element | tail] 이기 때문에 재귀를 순환할수록 가장 먼저 들어간 값이 제일 끝에 배치될 수 밖에 없다.

이 문제를 해결하는 쉬운 방법이 있다. 가장 쉬운 방법은 계속 값이 들어가는 result와 head를 ++ 연산자를 통해 concat 하는 것이다. 하지만 이 방법은 성능 측면으로 봤을때 느리다고 엘릭서 공식 문서에 설명되어 있다.

또 한가지 방법은 마지막에 나온 리스트를 뒤집어 주는 것이다. 하지만 이번 구현의 목표는 라이브러리를 일절 사용하지 않는 것이므로 reverse 함수도 직접 구현해야 한다. 그래서 어려운 방법도 시도해보았다…

  1. reverse
def reverse(list, result \\ [])

def reverse([], result), do: result

def reverse([head | tail], result) do
    reverse(tail, [head | result])
end

Reverse 함수도 크게 어렵지 않았다. 가장 먼저 들어간 값이 리스트의 앞단에 올 수 있도록 head와 result를 결합 시켰다. 그리고 tail을 재귀 순환 한다.

  1. split /take
    split과 take는 거의 같은 함수이므로 같이 살펴보자.

split

def split(list, num, split_list \\ [], remain \\ [])

def split(_list, 0, split_list, remain) do
    {reverse(split_list), remain}
end

def split([head | tail], num, split_list, _remain) do
   split(tail, num - 1, [head | split_list], tail)
end

take

def take(list, num, result \\ [])

def take(_list, 0, result), do: reverse(result)

def take([head | tail], num, result) do
    take(tail, num - 1, [head | result])
end

Num을 파라미터로 받고 num이 0이 될때까지 재귀 순환을 반복한다. split과 take의 차이는 단지 남은 부분을 반환하냐 안하는냐의 차이이다.

  1. flatten

이 함수가 가장 어려웠고 그지 같았다… 일단 살펴보자.

def flatten(list, result \\ [])

def flatten([], result) do
    reverse(result)
end

def flatten([head | tail], result) when is_list(head) do
    if length(tail) > 0 do
      flatten(head, result) ++ flatten(tail, [])
    else
      flatten(head, result)
   end
end

def flatten([head | tail], result) when not is_list(head) do
    flatten(tail, [head | result])
end

flatten을 구현할때 가장 큰 부분으로 생각한 것은, 리스트의 각 요소가 중첩된 리스트인지 아니면 값인지를 살펴보는 것이였다.

요소가 중첩된 리스트라면 재귀순환을 반복하고, 아니라면 result에 값을 넣는다.

중첩된 리스트에서 중요했던 부분은 tail 부분을 어떻게 재귀순환 할 것인가였다. 이 부분이 가장 생각하기 까다로웠는데, 예시를 들어보겠다.

[[1],[2,3,4]] 이런 리스트가 있다고 생각해보자.

처음 function clause가 맞는 부분은 [1]을 살펴볼때이다. 이때 리스트의 head와 tail은 각각 [1][2,3,4]이다.

이떄 tail 부분또한 flatten을 적용해야 하는 리스트이다. 따라서 나는 tail이 존재할때 역시 tail에 flatten을 적용시켰다. 다만 result가 섞이면 안되므로 tail에서 flatten을 호출할때는 result 파라미터를 비어두었다.

그리고 만일 대상 리스트가 비었다면, 이전에 만든 reverse 함수를 이용하여 최종 결과물을 반환해준다.

글로 적고 보니 얼마 안되지만 생각하는데 정말 시간이 오래 걸렸다… 오늘의 공부는 여기까지!

profile
BE Developer

0개의 댓글