본문 바로가기

프로그래밍 기술 노트/Functional Study

[Monad] 시퀀스를 여행하는 모나드를 위한 안내서

제목이 저런 이유는 Sequence / Traverse  에 관한 내용이기 때문..

 

Sequence

개발을 하다가 보면 List 같은 형태의 Option/Result 가 필요한 경우가 생기게 된다.

실무에서 자주쓰는 대표적인 예시중에 다음 같은것이 있다.

N 개의  URI 에 응답을 조합한다, N개중 1개라도 응답이 오지않으면 Fail 이다.

 

MSA 에서 API Composition 할때나, FE 에서 Server 에 여러번 요청을 할때 등등.. 상당히 많이 쓰이는 로직이다.

간단하게 예시를 들어서 다음과 같다고 해보자, 편의를 위하여 가장 기본적인 Type만 사용하였다. 

  • 내 블로그에서 001~003 까지 결과를 알려주는 함수 MyBlog 를 짠다.
  • 요청해야할 URI : ["see-ro-e.tistory.com/001", "see-ro-e.tistory.com/002", "see-ro-e.tistory.com/003"]
  • MyBlog() 에서 사용할, URI 의 응답을 받는 함수 : fetchWebPage(uri: String) -> Option[String]
  • 즉 MyBlog() 의 시그니쳐: MyBlog(uris: List[String]) -> Option[List[String]]

우리는 이미 Map 이라는 아주 좋은 함수를 알고있으므로, 요청 uris 에 대한 모든 응답을 받을 수 있다,

uris.map(fetchWebPage) // scala

// uris |> List.map fetchWebPage // Fsharp
// uris.map { fetchWebPage(it) } // kotlin
// (->> uris (map fectchWebPage) ) // clojure

 

모든 URI 를 순회하면서 응답을 요청하는것이므로, map 을 쓰면 깔끔해보인다.

근데 여기서 문제가 발생한다.

fetchWebPage 는 Option을 리턴하는 함수기 때문에, 응답이 Option[String] 이다.

그럼 map 하고 난 결과물은? List[Option[String]] 이다.

우리가 원하는건 Option[List[String]] 이다.

즉 Option <-> List 의 감싸는 순서가 Swap 되었다.

 

어떻게 할수 있을까?

  • List[Option[String]] 의 모든 요소가 유효한지 아닌지 검사해야한다, (즉 List 의 각 요소를 Some / None 인지 검증) 
  • 이 검사하는 함수를 임시로 AllCheck 라고 생각해보자
  • 하나라도 유효하지 않으면, 이 결과는 유효하지 않다. 즉 최종 결과물이 None 이다 -> AllCheck 의 최종결과물 타입이 Option 이어야함
  • 모두 유효하면 각 List 의요소의 Option 은 필요가 없다 (=> 모두 유효하니까) 즉 List[String] 이 얻어지는데, 위에 처럼 AllCheck 의 최종 결과물은 Option 타입이어야 하므로, Option[List[String]] 이 최종 결과물이 된다.

 

이런 경우처럼 Layer 를 Swap 하는 경우는 상당히 빈번하게 발생한다.

  • 방금 예시처럼 HTTP 상이든, 아니면 DB 에서 조회를 하던, 여러 데이터를 가져오는 경우 실패하는 경우가 발생한다.
  • 비동기 데이터를 한번에 요청해서 모든 결과가 와야 다음 작업을 할수있다. 예를 들어 JS 같은 언어의 Promise (비동기)를 사용하다보면 모든 Promise의 결과를 모으는 함수가 있다. Promise.all 이다.

 

이런 AllCheck 를 좀더 일반적인 명칭으로 확장한 함수가 바로바로

sequence

되시겠다.

Seqence 는 Layer 를 Swap 시킨다,

 

왜 이름이 sequecne 냐고 하면 나도 잘모른다. 카테고리 이론이 그런가보지 뭐...

대충 비결정형 (List 같은 Sequence) 를 Sequencing (순서화) 하기 때문에 그런거 같음..

 

즉 우리가 처음에 짠 코드를 sequence 함수를 이용하도록 하면 다음과 같다. (구현되어있다고 가정)

uris.map(fetchWebPage).sequence() // scala

// uris.map { fetchWebPage(it) }.sequence() // kotlin
// uris |> List.map fetchWebPage |> List.sequence // Fsharp
// (->> uris (map fectchWebPage) sequecne) // clojure

 

 

Traverse

위처럼 map 하고 sequence 하는걸 하나로합쳐서 traverse 하고 한다.  (와 코드가 깔끔해졌다)

uris.traverse(fetchWebPage) // scala

// uris.traverse { fetchWebPage(it) } // kotlin
// uris |> List.traverse fetchWebPage // Fsharp
// (->> uris (traverse fectchWebPage)) // clojure

 

즉 Map 과 Traverse 는 전부 순회라는점에서는 같지만

  • List/Array 같은 Sequence 에서 각 Item에 대한 순회 결과가 필요하면 map
  • List/Array 같은 Sequence 에서 전체 Sequence 에 대한 순회 결과가 필요하면 traverse 이다

 

Traverse 는 Sequence 의 모든 정보를 여행(??) 해서 최종 결과 Layer 를 씌운다.

 

A vs M

이를 구현하는데 applicative 스럽게 구현 하느냐 monadic 하게 구현하느냐 에 따라 또 갈리게된다.

이는  Applicative 와 Monad 의 근본적인 차이에 기반하는데, 이로인하여 보통 traverseA / traverseM 이 구분되어있는 경우가 많다.

Applicative 와 Monad  근본적 차이점은 [나도 수학적으로는 모르지만] 

  • Monad 는 bind (= flatmap) 의 chain 이다. 즉 실패 Case 가 발생하는 순간 끝이다 (나머지 요소는 skip)
  • Applicative 는 apply 이다, 꺼내서 (각 요소의 실패,성공 Case와 연관성 없이) 적용시킨다.

모두 성공인 경우는 A 나 M 이나 같은 결과이고, Fail Case가 중요하다,

 

다시 맨처음 예시로 돌아가서, 몇가지를 좀 수정해보자

  • 내 블로그에서 001~003 까지 결과를 알려주는 함수 MyBlog 를 짠다.
  • 요청해야할 URI : ["see-ro-e.tistory.com/001", "THIS IS NOT URL1", "THIS IS NOT URL2"]
    "THIS IS NOT URL1", "THIS IS NOT URL2" 은 실패하는 Case 이다.
  • MyBlog() 에서 사용할, URI 의 응답을 받는 함수 : fetchWebPage(uri: String) -> Result[String]
    실패의 원인을 알기 위하여 Option 에서 Result 로 변경하였다.
  • 즉 MyBlog() 의 시그니쳐: MyBlog(uris: List[String]) -> Result[List[String]]

이런경우 A / M 의 결과는 다음과 같다.

  • Monadic 구현인 TraverseM의 결과는 다음과 같다.
    -> Result.Failure ["THIS IS NOT URL1 is not URL"]
  • Applicative 구현인 TraverseA의 결과는 다음과 같다.
    -> Result.Failure ["THIS IS NOT URL1 is not URL", "THIS IS NOT URL2 is not URL"] 

구현에 따라 다르겠지만, 중요한점은, 실패를 만나는 즉시 Fail 이 되고 나머지 결과는 모르느냐 (M), 모든결과에 대한 Fail 을 아느냐 (A) 의 차이다.

 

Option is Sequence? Option is traversable?

가끔 보다 보면 Option 에 seqeuce 나 traverse 함수가 있는 경우가 있다.

특히 HKT 가 지원되는 언어의 library 에서 그러는 경우가많은데

Option 은 개뿔 List 나 Array 같은게 아닌데 왜 sequence / traverse 가 있는거지? 하는 생각이 들때가 있다
정확하게는 함수 이름이 이러면 안되는거 아닌가 하는.. (나만그런가?)

이유는 정말 심플하게 

Option 도 최대 길이가 1인 List 라고 볼수있기 때문인듯하다... (None : 길이 0, Some : 길이 1)

 

참고 자료

Traverse (typelevel.org)

scala - How to understand traverse, traverseU and traverseM - Stack Overflow

Understanding traverse and sequence | F# for fun and profit (fsharpforfunandprofit.com)

 

728x90