제목이 저런 이유는 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
되시겠다.
왜 이름이 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 이다
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)
참고 자료
scala - How to understand traverse, traverseU and traverseM - Stack Overflow
Understanding traverse and sequence | F# for fun and profit (fsharpforfunandprofit.com)
'프로그래밍 기술 노트 > Functional Study' 카테고리의 다른 글
[liftIO] 빠르게 올리는 함수형 컨퍼런스 liftIO 2022 후기 (0) | 2022.12.03 |
---|---|
Delimited Continuations 가 대체 뭔데? (1) | 2022.10.14 |
[HKT] Value / Type / Kind 와 Higher Kinded Type (Feat. 고차함수) (1) | 2022.04.30 |
[Expression Problem] 객체 대수 (Object algebras) 와 Tagless Final (0) | 2022.04.28 |
[Expression Problem] Mutiple Dispatch (Feat Visitor / Multi Method) (0) | 2022.04.27 |