The Minimum Viable Pull-request (KOR)


This article is a korean-translated version of the original post, ‘The Minimum Viable Pull-request‘ by Jean-Michel Fayard.

이 글은 Jean-Michel Fayard가 투고한 The Minimum Viable Pull-request의 한국어 번역본입니다. 의역이 많습니다. 오탈자는 댓글로제보 부탁드립니다.


오픈 소스 프로젝트에 기여하는 것이 복잡할 필요가 전혀 없습니다.

여러분은 우리들 중 많은 사람이 하루에 적은 수의 오픈 소스 기여를 하고, 몇몇 행복한 사람들은 같은 하루에도 10개나 100개 이상의 기여를 하고 있는 것을 알아채셨나요?

그런 사람들이 구직자들이 말하는 ‘Rock-Star Ninja Developer’ 일까요, 아니면 어떻게 기여를 효과적으로 할 수 있는지 알고 있는 것 뿐일까요?

문제점

새 오픈 소스 프로젝트에 기여할 생각이 있으신가요? 오픈 소스니 마음대로 해도 되겠죠? 당신이 해야 할 것은 프로젝트를 포크(fork) 하고 기능을 추가하고 제안하고 모든 것이 끝.

끝? 절대 아니죠!

혼자서 일하는 악덕 업자의 접근방법이 좌절감에 대한 레시피입니다!

물론, 아래의 사항에 해당할 수도 있죠.

  • 프로젝트가 더 이상 관리되지 않거나,
  • 오픈소스 관리자가 해당 방법 대신 다른 방법에 관심을 가지거나,
  • 같은 것을 구현하는 데에 5배나 더 쉬운 방법이 있거나,
  • 이미 있는 기능을 구현했거나,
  • CONTRIBUTING.md 에 있는 내용을 따르지 않았거나,
  • 모든 테스트 케이스가 정확히 작동하는지 CI 체크를 살펴보지 않거나, (물론, 이 때에 빌드는 실패하게 됩니다)
  • 프로젝트의 네이밍이나 코드 컨벤션을 따르지 않거나,
  • 몇 주가 지나 포크한 것과 마스터에 엄청난 차이가 발생했거나,
  • … 또는 당신의 일화 …

저는 최근에 다수의 오픈 소스 프로젝트에 기여했습니다. 처음에는 다소 떨렸지만, 실전을 통해 점점 좋아졌습니다. 제가 당신과 공유하고 싶은 주요 내용은 실현 가능한 최소의 Pull-Request(Minimum Viable Pull-request) 입니다.

WIP Pull-request

여러분이 모를 수 있는, WIP Pull-request는 개발팀 내부의 의사소통을 개선하기 위한 확립된 관행 및 협약입니다.

  • 준비되었을 때가 아닌 기능이나 오류 수정을 시작할 때 Pull Request를 요청합니다.
  • Pull-Request의 이름을 WIP: XXX 라고 정합니다. 이 뜻은 여러분이 이 작업을 진행중이고, 조기 피드백에 답할 준비가 되었음을 말합니다.
  • 작업이 거의 끝나면, WIP 접두어를 제외하고, 파이널 리뷰가 필요함을 알립니다

MVP + WIP = Minimum Viable Pull-request

여러분의 첫 오픈 소스 프로젝트 기여를 어렵게 만드는 요소가 있습니다. 그 것이 바로, 해당 프로젝트에 대해 잘 모른다는 점입니다. 기여한 사람들을 모르고, 코드 기반을 모르고, 규칙이나 문맥 까지도 모릅니다. 그리고 기여자는 여러분을 모릅니다. 물론, 여러분이 탐사 모드(exploration mode)에 있는 것도 한 몫을 합니다.

충고: MVP로서 당신의 (WIP) pull request를 생각하세요.

MVP(Minimum Viabl Product) 컨셉은 린 스타트업 방법론(Lean Startup)에서 언급되는 것으로, 다수의 불분명한 것이 있는 미지의 환경에서 학습을 가속하는 것입니다. 이제 어떻게 이 방법론에서 영감을 얻을지에 대해 알아봅시다.

규칙 1: 당신의 아이디어를 확실하게 전달하기 위해 필요한 작업만 진행하세요.

규칙 2: 기여자와의 대화에 최대한 빠르게 집중하세요.

(편리하게도, MVP는 Minimum Viable Pull-request의 약자이기도 합니다! 그래서 여러분은 새로운 약자를 배울 필요가 없습니다.)

Minumum Viable

여기서 주의해야 될 점은 훌륭한 개발자들은 미덕으로 게을러지는 경향이 있고, 최소(이봐, 작업을 덜 하는 것이 좋은 것이라고!) 에 집중한 나머지 실현 가능한 부분을 건너뛰기도 합니다.

그렇지 않습니다! 여러분의 일은 기여자가 여러분을 쉽게 안내할 수 있도록 만드는 것입니다. 물론, 이 것에 코드를 적게 작성하는 것 또한 포함되어 있지만, 평소보다 더 많은 대화를 하는 것이 포함됩니다.

  • 여러분의 사용처(Use-case) 를 설명하고, 변화를 원하는지 설명하세요.
  • 원하는 기능을 어떻게 구현할 지에 조언을 구하세요.
  • CONTRIBUTING.md 파일을 정독하세요.
  • 프로젝트가 어떻게 작동하는지에 대해 다른 사람의 pull-request를 살펴보세요. CI가 어떻게 구성되었는지도 살펴보세요.
  • 가장 중요한 것은, 작업을 하는 데에 시간을 보내기 전에 변화에 대한 권한을 승인 받으세요.
  • 이 모든 것을 해냈다면 추가 점수가 있습니다 😛

How?

방법은 없습니다, 단지 pull-request를 시작하기 전에 이슈를 여는 것입니다.

제가 과거에 사용한 전략은 코드 베이스에 불합격되는 테스트를 작성하는 것입니다. (역주: 해당 PR의 첫 커밋인 854f32c 에 일부러 CI에 실패한 커밋을 작업했습니다)

비슷한 아이디어로는 구현하려는 변경사항으로 프로젝트의 문서를 변경하는 것이 있습니다.

아니면 제가 이 글을 쓰게 된 경험을 생각해보세요. 저는 종속성 관리로 인해 겪었던 문제점을 해결하는 Gradle 플러그인을 막 출시했습니다.

저는 갑자기 많은 프로젝트에 잠재적으로 기여할 수 있었습니다. 잠재적으로요. 하지만 많은 프로젝트는 그들이 사용하고 있는 솔루션으로도 괜찮습니다. 어느 것을 찾을 수 있을까요? Pull-request를 열고 플러그인을 적용한 다음, 제 솔루션이 어떻게 영향을 미칠 수 있는지 보여줄 수 있을 만큼만 업데이트 했습니다.

이 MVP 접근 방법은 저에게 큰 도움이 되었습니다. 저는 관심이 있는 사람과 없는 사람을 빠르게 구별하는 방법을 배웠습니다. 그들이 관심을 가지지 않을 때 많은 시간을 헛되게 투자하지 않아도 되어 관리자에게 조용히 화를 내지 않았습니다. 그들이 관심을 가질 때 제 시간과 노력을 기여에 투자하는 것이 훨씬 더 좋은 결과를 가져오고, 헛되지 않았을 것입니다.


이 글을 보고 어떤 점을 배웠나요? 감사를 표현하기 위해 :clap: 를 누르고 다른 사람이 이 글을 찾을 수 있게 도와주세요.

저와 나누고 싶은 이야기가 있으신가요? 댓글에 달아주시면 매우 기쁩니다!

[번역] Kotlin Functional Programming II: Monad Transformers


이 글은 Jorge Castillo 가 Medium에 올린 Kotlin Functional Programming II: Monad Transformers 의 번역 글입니다. 다소 오역이 많을 수 있습니다.

This post is Korean translated post of Kotlin Functional Programming II: Monad Transformers, author is Jorge Castillo. Thanks for great post.


 

감동이지 않나요? 분명 Monad 스택을 하나의 타입으로 합성할 때 얻는 기분과 같을 것입니다. 그러니까, 왜 안하겠어요?

만약 이 시리즈의 첫번째 글을 아직 읽지 않았다면 먼저 읽어주세요. 이 글을 이해하는 데에 큰 도움이 될 것입니다. 또한 이 글은 저 접근법에 대한 추가적인 반복이 될 것입니다.


문제점

함수형 패러다임 세계에서 자주 언급되는 말이 있습니다. : Monad 는 합성할 수 없습니다. 적어도 우아하지는 않은 것 같네요. 단지 호출 체인에 모든 같은 Monad 를  올렸을 때 flatMapflatten을 사용해 스택을 합성할 수 있지만 가능성이 매우 낮습니다.

일반적인 상황에서, 이 함수는 다른 레벨의 다른 Monad를 반환할 것이고, 스택은 심하게 중첩될 것입니다.

스택에 많은 중첩 레벨을 얻을 때 마치 그것은 피라미드처럼 보이기 시작합니다. 다음 3개의 레벨의 중첩된 Monad가 있는 코드를 보세요. 여기서는 가장 깊은 수준의 엑세스 권한을 얻고자 합니다.

이 접근법은 기하급수적으로 무서운 상황에 이끌어낼 수 있습니다.

Monad comprehensions

지난 코드를 보면 모든 레벨에 Either를 사용하고 있습니다. 단일된 유형이니, 우리는 Monad Comprehension 라는 멋진 것을 적용할 수 있습니다!

만약 여러분이 Haskell 에 대해 알고 있다면, 아마 “do notation” 라는 것을 들어보셨을 것 입니다. Monad Comprehension는 순차적으로 정렬된 순서대로 순차적 코드를 작성할 수 있는 방법입니다.문법을 위해 결과를 ‘산출’할 수도 있습니다.

KΛTEGORYbinding라고 불리는 Monad Context의 밑에 있는 기능을 제공합니다. Monad 인스턴스를 사용할 수 있는 모든 데이터 유형에 대해 Monad Comprehensions 를 제공합니다.

그래서 binding를 이용하여 지난 코드를 작성해봅시다.

Bindings 는 Monad Context에서 구동되게 되는데  여기서는 MonadError 인스턴스입니다. Binding 블록은 일련의 순차적 flatMappeable 작업이 포함될 수 있으며, 이를 순차적으로 선언할 수 있습니다. 각 작업의 결과는 Monad Context로 들어올려지고, 다음의 bind()호출을 향해 flatmapped작업을 시행합니다.

마지막으로 Monad Context가 사용된 호출 결과를 자동으로 들어올린 것을 산출할 수 있습니다.

이 Syntactic Sugar (역주: 프로그래밍 언어에서 읽기나 표현을 좀 더 쉽게 만들어주는 문법) 는 많은 언어에서 찾을 수 있습니다. 매번 같은 Monad 유형을 반환하고 같은 Context로 결과가 들어올려지는 대량의 순차적 작업을 가질 때 마다 binding를 사용할 수 있습니다.

하지만 기억하세요: 우리는 Either라는 싱글 유형을 다루고 있기에 binding를 사용할 수 있습니다. 하지만 각기 다른 중첩 유형이 있으면 어떨까요?

이 예제는 kategory.io documented datatypes: EitherT. 의 한 부분에 속합니다. KΛTEGORY문서는 코드와 차후 설명을 제공하고 있어 매우 교훈적입니다.

실제 문제

이전 글에서 모든 Android 앱에 대한 주요 관심사를 해결하기 위해 단계적으로 구축한 것이 이 상황을 깊게 들여다볼 수 있는 예제가 될 수 있습니다. 마지막으로 해당 스택으로 종료되었습니다.

Reader<GetHeroesContext, IO<Either<CharacterError, List<SuperHero>>>

우리는 필요한 의존성을 제공하는 Context를 기대할 수 있는 정규화된 Reader 계산을 수행했습니다. 의존성이 제공되면 IO 에 대한 안전하지 않은 작업이 수행되며 이는 오류 또는 유효한 히어로 목록을 반환합니다.

이 스택은 복잡하여 코드의 가독성을 낮춥니다.

하지만 이 Monad 스택은 완전한 것이 아닙니다. 함수형 패러다임 개발자는 이에 머무르지 않고 Monad Transformers 를 사용하여 이 문제를 해결하려고 합니다.

그래서, Monad Transformer가 무엇인지요?

일반적으로 Monad와 유사하지만 독립 실행형 엔터티가 아니라 기본적인 Monad를 다룹니다.

대부분의 KΛTEGORY 가 제공하는 Monad는 Transformer 등가물이 있습니다.

관례상, Monad의 Transformer 는 동일한 이름에 T를 붙인 이름을 가지고 있습니다. 예를 들어, State의 Transformer 등가물의 이름은 StateT입니다.

만일 Either<L, R> 와 같은 다른 모나드 기능을 얻기 위해 기존 IO<A>과 같은 Monad가 필요하다면, Transformer 를 사용할 수 있습니다.

취할 수 있는 이득은 무엇인가요?

Transformer 를 사용하여 중첩된 유형의 필요성을 제거하고 이전에 있는 것과 동일한 기능을 가진 단일 장치로 모든 것을 축소합니다.

즉, 중첩된 Monad에 의해 야기된 복잡성을 제거하여 스택에 대한 매핑을 만들어 스택에 엑세스 할 수 있습니다. 또, 깊은 스택에서의 작업을 사용할 수 없게 합니다.

따라서 여러분의 코드는 Transformers의 도움으로 간결화 될 수 있습니다.

스택 수정하기

가장 깊은 중첩 유형을 골라봅니다.

IO<Either<CharacterError, List<SuperHero>>>

부작용을 포함하는 IO 계산이 있습니다. 양쪽 모두 이중성의 힘으로 전달하기 위해 우리는 Either Transformer를 사용할 수 있습니다.

Either Transformer의 유형은 EitherT<F, L, A>입니다. F는 기본적인 Monad를, L은 Either의 Left Side, A는 최종 결과 유형입니다.

히어로 목록에 대입해본다면 다음과 같습니다.

EitherT<IOHK, CharacterError, List<SuperHero>>

여기서 IOHK이 일반 유형 슬롯의 IO를 참조하는 방법에 대해 이해할 필요성이 있습니다. 따라서 여기서 IO는 기본적인 Monad가 됩니다.

IOHK는 KΛTEGORY 의 더 높은 종류(higher kind)를 위해 컴파일때 자동으로 생성되는 boilerplate의 일종입니다.

EitherT덕분에 IO는 존재하는 문제를 해결할 수 있게 되었습니다 🎉. 하지만 스택은 조금 더 커졌습니다.

Reader<GetHeroesContext, IO<Either<CharacterError, List<SuperHero>>>

따라서 우리는 이미 변형된 IO에게 Reader 의 힘을 공급하여 시간에 따른 IO 지연 계산 및 오류 처리 기능 뿐만이 아니라 Dependency injection 기능을 제공하도록 합니다.

우리는 Reader Transformer 를 사용하여 이 문제를 달성할 수 있습니다. : ReaderT<F, D, A>

F는 변형될 기본적인 Monad이고, D는 모든 의존성을 가지고 있는 Reader Context, A는 최종 결과 유형입니다.

변형된 유형은 다른 반환된 유형을 통해 작업할 수 있기 때문에 A에 대해 불가지론적 (역주 = 어떤 것에도 상관없이 소프트웨어 프로그램이나 시스템을 사용 가능한)이 되어야 합니다. 즉 시스템에서 임의의 오류가 발생할 수 있다는 것을 알고 있고, 시스템의 오류가 CharacterError의 클래스 요소 중 하나에 매핑될 것이란 것을 알 수 있습니다. (이전 글에서 자세한 상황을 알아보세요.)

이번에는 기본적인 Monad는 EitherT<IOHK, CharacterError, List<SuperHero>>로 변형될 것입니다. 우리는 우리의 Transformer 타입인 ReaderT<F, D, A> 에 맞게 F의 위치를 알맞게 맞출 필요가 있습니다.

이 의미는 IOHK와 CharacterError 유형을 고치기 위해 EitherT를 일부적으로 적용하여 A를 generic로서 남겨야 된다는 것을 의미합니다. EitherT의 유형이 EitherT<F, L, A>인 것에 기억하세요.

ReaderT<EitherTKindPartial<IOHK, CharacterError>, D, A>

이것으로 우리는 변형된 유형의 작업을 할 준비가 되어있습니다. 비록 그것이 여전히 중첩된 형태로 보일지라도, 실제로 필요한 모든 기능을 가진 단일 유형입니다.

여기서 ReaderTKleisli로도 불리고 있습니다. Kleisli 는 KATEGORY 의 type alias 의 일종입니다. 샘플에도 Kleisli로 사용되고 있습니다.

typealias ReaderT<F, D, A>  = Kleisli<F, D, A>

Kleisli<F, D, A>D => F<A>의 wrapper 입니다. 바로 우리가 샘플 저장소의 아키텍쳐에 있는 계산 수준이나 레이어를 가지고 있습니다.

여러분은 변형된 형태를 Result 로 의미하는 것으로 잘못 인식하고 있다는 것을 깨달았을 것입니다. 결국 이 착각은 다른 계층을 가로질러 움직이는 행동 결과로 보여질 것 입니다.

typealias Result<D, A> = 
Kleisli<EitherTKindPartial<IOHK, CharacterError>, D, A>

또한 나는 AsyncResult로 불리는 Kleisli의 모든 기능을 대표하지만 동시에 AsyncResult를 합성하고 bindings를 이용하기 위해 Monad 인스턴스를 동일하게 제공하는 데이터 타입으로 Result를 Wrap 했습니다. 이는 아키텍쳐 구현에 있어 매우 유용하게 쓰일 것입니다.

이제 AsyncResult 유형을 사용해서 아키텍쳐가 적용된 앱을 어떻게 완전하게 하는지 살펴봅시다.

Architecture using AsyncResult

우리는 이제 프레임워크가 되야 하는 Clean 이라는 가장 바깥쪽의 레이어를 사용해 시작할 것입니다. 여기서는 데이터의 원본을 보고 구현할 수 있습니다. 자, 이제 네트워크 데이터 소스 구현체를 살펴보겠습니다.

AsyncResult덕분에 합성되고 변형된 유형을 가질 수 있어, binding 를 사용해 대량의 순차적 작업을 선언할 수 있습니다.

여기서 나는 모든 순차적 작업을 합성했습니다. flatMap, map를 이용하여 실제로 합성된 작업을 실행하지는 않지만 합성을 되돌려놓을 수 있습니다. 이것이 바로 Key이며, Key는 Transformers 로 활성화됩니다.

binding 안에서 시간 내에 실행되는 순차적 작동 블록은 다음과 같은 기능 체인을 실행할 때에 실행됩니다.

  • 히어로를 불러오는 Query 작성
  • Reader를 들어올리는 의존성에 대한 엑세스 권한을 부여하고(Transformers를 사용한 AsyncResult 자체) Reader Context에 접근
  • IO 계산 수행

이러한 작업 중 하나는 선택적으로bind() 를 사용하여 해결할 수 있습니다. 이 경우에는 AsyncResultMonadError인스턴스를 사용하여 결과를 수동으로 묶습니다. 따라서 다음 단계에서는 이전 단계에서의 해결책을 기대할 수 있으므로 결과적으로 결과가 계산될 수 있습니다.

runOnAsyncContext에 대해 자세한 사항을 알고 싶다면 이전 글샘플 저장소를 살펴보세요. 주로 f 람다 코드를 coroutine로 실행하는 IO 계산을 반환하고 성공과 오류 사례 모두에 대한 Lambda 를 제공합니다.

그런 다음 캐시 정책을 구현할 수 있는 일종의 Stub저장소 레이어가 존재합니다.

개인적으로 이 호출 체인에 추가할 로직이 없다면 당연하게도 추가하지 않을 것입니다. 하지만 이것은 단순히 교휸적인 목적이 될 수 있는데, 이 접근법과 객체지향 프로그래밍에 기반한 Clean Architecture 를 비교해보세요.

flatMapping를 사용하여 캐시 정책을 구현하거나 데이터 소스로부터 반환된 합성된 계산을 매핑하는 작업을 쉽게 수행할 수 있습니다.

여기서 나는 모든 기능을 package level 에 정의했고, 모든 의존성이 있는 경우에만 사용할 수 있으며, 모든 의존성이 파라미터로 전달되거나 Reader 에 의해 제공됨을 알립니다. 여기서는 기능에 대한 상태를 보관할 필요가 없기 때문에 이 기능을 담은 적합 인스턴스를 가질 필요가 없습니다. 개인적 의견으로는 객체지향 프로그래밍 패러다임과 함수형 프로그래밍 패러다임은 매우 다르다는 것을 이해하는 것이 중요하다고 봅니다.

저장소에 대한 호출자는 비지니스 로직이 포함된 Use Case가 될 것입니다.

단지 나는 데이터 소스로부터 반환된 계산 결과에서 유효하지 않은 히어로들을 필터링하길 원합니다. AsyncResult를 반환하고 있기에 모든 레벨에서 하나의 싱글 유형으로 쉽게 합성할 수 있습니다.

Presentaion 코드는 다음과 같게 됩니다.

mapflatMap를 사용하여 Use Case의 결과를 매핑하고 효과를 적용할 수 있습니다. 또한 handleErrorWith함수를 통하여 뷰에 에러에 관한 효과를 적용할 수 있습니다.

getSuperHeroes()와 효과를 유발하는 drawHeroes()또는 displayGetHeroesError()들은 최종적으로 풀어지고 효과를 실행할 때 그들은 뷰에 부작용을 적용할 수 있게 AsyncResult<GetHeroesContext, Unit>를 반환합니다.

이 작업에는 아무것도 반환될 필요가 없기 때문에 Unit를 반환합니다. 당연히 GetHeroesContext는 의존 바인딩이 정의된 Reader Context를 고치는 단순 유형입니다.

작업을 수행하기 위해 바인딩 할 수 있는 기능을 사용하는 기능을 적용하는 양쪽의 부작용을 살펴보세요!

나는 주로 View Contract가 의존성으로 정의된 Reader Context에 접근하기 위하여 binding를 사용하고, 뷰에 효과를 적용합니다. 그래서 제가 보기에 여러가지 다른 효과를 적용해야 한다면 다른 식으로 글을 쓸 수 있었습니다.

다시 말해, 모든 연산은 UseCase을 기반으로 하여 이루어집니다. 이는 효과는 여전히 실행되지 않고 그들이 모든 호출 체인을 실행하고 모든 계산과 효과를 발휘하는 것이 옳다고 판단할 때 까지 계속해서 그것들을 계속 미루고 있습니다.

뷰 구현체는 프리젠테이션 레이어에 의하여 합성된 계산을 실행하는 것을 결정합니다. 약간의 복잡성은 우리가 구현한 Higher Kinds가 여전히 보이고 있기 때문입니다. (그 증거로 ev() 라는 함수가 있습니다. 하지만 KEEP-87 이 거의 성공 단계에 도달했습니다! upvote 하는 것을 잊지 마세요 🙂 그렇지 않을 때 HK와 Typeclasses 에 대한 구현체를 유지해야 되고, 사용하기 쉽게 만들기 위하여 반복해야 되기 때문입니다.

테스트는 어떻게 진행되는가요?

Reader 를 대신할 때 (즉 Reader Transformer) 테스트는 사소한 문제로 변합니다. 여러분은 단순히 필요한 의존성에 대한 Mocks를 제공하는 각기 다른 Context 구현체를 제공하면 됩니다.

따라서 실질적인 생산 코드 베이스를 계속 연습할 수 있고 전체 시스템 실행에 대해 주장(assert)하기 위해 다른 Context을 그냥 지나쳐 버릴 수 있습니다.. 블랙 박스 테스트는 이 시나리오에서 꽤나 설득력이 있습니다.

자, 이제 Reader context를 어떻게 선언하는지 빠르게 살펴보고 끝낼 것입니다.

이 방법은 제가 안드로이드 앱을 Dagger의 애플리케이션, 액티비티의 스코프를 이용해 구축할 때 사용하고 있는 것을 흉내낸 것입니다.

마지막에는 스코프에 따른 다른 구현 방법을 가진 sealed hierachy가 될 수 있습니다. 이 접근법은 Android Context, 네비게이터, api 클라이언트 또는 스레드 구현체 같은 전역 의존성에 대한 상속 여부를 정의하는 데에 도움이 됩니다. 아마도 이들은 애플리케이션 스코프 내에 있을 것입니다.

하지만 그들 모두는 아래에 선언된 ‘sub-scopes’ 에 대해 완전히 상속 가능합니다. 따라서 여러분은 sealed class로부터 메서드와 속성을 상속하기 위해 mocks를 제공하는 고유의 sealed class 구현체를 추가할 수 있습니다🎉.

액티비티 스코프에서는 현재 보여지는 액티비티의 context를 상속할 수 있으며, 다른 view contract의 인스턴스를 제공할 수 있습니다. 그래프 및 구성요소가 뷰 구현체에서 생성되기 때문에 이 모든 것을 쉽게 제공할 수 있습니다. Dagger로 하는 것과 같이 말입니다.


마무리

샘플 저장소에 있는 monad-transformer 모듈을 살펴보는 것을 잊지 마세요! 이 모듈은 히어로 목록과 상세 화면액티비티를 이 접근법을 사용하여 구현하는 것을 보여줍니다.

이 주제와 더 많은 것을 알고 싶다면, 트위터 @JorgeCastilloPr 를 팔로우 하는 것을 잊지 마세요!

아직 시리즈의 이전 글을 살펴보지 않았다면 살펴보세요!

다음 포스트에서는 KATEGORY 로 달성 가능한 ‘Tagless Final’ 라는 좀 더 심화된 함수형 패러다임 스타일에 대해 살펴볼 것입니다. Typeclasses로 정의된 추상적인 행동 위에 있는 완전한 게산 트리가 어떻게 정의되는 지 알 수 있고, 나중에 구체적인 인스턴스를 전달할 때 구현 세부 사항을 제공하는 방법을 알 수 있을 것입니다.

아마 매우 흥미로울 것입니다. 계속 지켜보세요!

옵티머스 프라임이 이 글을 승인합니다 🙂

[번역] Kotlin Functional Programming I: Monad Stack


이 글은 Jorge Castillo 가 Medium에 올린 Kotlin Function Programming I: Monad Stack 의 번역 글입니다. 첫 번역이라 다소 오역이 많을 수 있습니다.

This post is Korean translated post of Kotlin Function Programming I: Monad Stack , author is Jorge Castillo. Thanks for great post.


“너희는 스스로 성전을 짓고, 마루를 바닥에 쌓아두고, 성전에 있을 법한 문제에 유념하면 하늘을 만질 수 있다. 그러면 그 절에서 영원토록 살 수 있다.”

평화롭지 않나요? 그리고 이 문구는 진실이기도 합니다.

아직 이 시리즈의 첫번째 포스트를 읽지 않았다면, 이 게시글을 먼저 읽는 것을 추천합니다. 여기서 언급될 몇몇 개념들이 이 게시글에 쓰여졌기 때문입니다.

함수형 프로그래밍에 대해 생각해보면, 우리는 한 가지 핵심 개념을 이해하고 있어야 합니다 : 문제를 한 번에 해결하도록 하고, 그 해결책은 영원히 재사용 할 수 있게 해야 한다.

함수형 패러다임으로 코딩을 할 때에 대부분의 문제들은 평범한 해결책 하나로 해결됩니다. 그 해결책들은 구현 세부 사항이나 의미론에 한정되지 않아야 합니다. 예를 들어보자면 Asynchrony, IO, Threading, Dependency Injection 또는 다양한 중첩된 아키텍쳐에 따른 의존성을 대체할 수 있는 전체적 개념에 대해 이야기할 수 있습니다.

따라서 이러한 문제는 모든 시스템에 핵심 요소가 될 수 있습니다. 이상적으로는 해결책에 대한 구현 작업을 한 번만 수행해야 합니다. 이미 해결된 모든 애플리케이션은 사용자가 작성한 모든 앱 또는 백엔드와 같은 다른 플랫폼에서도 사용할 수 있습니다. 물론, Kotlin 으로 작성되었기 때문이죠.

이를 매우 투명하게 반영하기 위해서 우리는 자체 애플리케이션 아키텍쳐(Application architecture) 를 만들어 단게별로 전체 스택을 구성할 수 있습니다.

Error 와 Success에 대한 모델링

거의 모든 시스템에는 외부 서비스에서 데이터를 가져오는 작업이 필요합니다. 예로 데이터베이스, API, 또는 외부 캐시들이 있습니다. 저는 단지 Android 앱에 대해서만 이야기 하는 것은 아닙니다.

이러한 데이터 서비스는 응답에 2가지의 각각 다른 시나리오를 제공합니다. 서비스에 요청해서 데이터를 가져오거나, 또는 예외로 처리되는 오류 사항입니다. 이 결과들은 매우 명확한 이중성을 가지고 있으며, 코드에 명시적으로 반영할 수 있는 방법을 찾아야 합니다.

Clean Architecture 에 대해 생각한다면 ThreadPoolExecutor가 제공하는 외부 DataSource또는 Repositories에 의해 던져진 예외는 취소한 다음 Callback를 사용하여 호출하는 등의 재사용된 스레드에 대한 사용 예제를 알고 있을 것입니다.

이러한 접근 방식은 단순한 애플리케이션에는 유용하지만 일부 내제된 문제가 있을 수 있습니다.

우선 오류 측면에서 예외를 콜백 전파(Callback propagation) 으로 전환해야 합니다. 예외가 Thread limit를 초과할 수 없기 때문에 이러한 현상이 일어납니다.

그리고 실질적인 투명성(referential transparency) 문제도 있습니다. Callback는 반환되는 유형을 살펴서 함수가 반환할 내용을 반영할 수 없기 때문에 Callback는 이와 같은 방식을 깨트리게 됩니다.

하지만 중요한 점은 우리가 두 가지 방법을 통해 결과를 얻을 수 있다는 점입니다. 결국 양쪽 모두 동일한 수행 결과 안에 있는 이중적인 사실의 일부분입니다. 그렇지요? 우리는 이 이중성을 단일 지점(Single possible branch) 으로 줄일 수 있어야 합니다.

RxJava를 이용하여 두 개의 결과를 하나의 통로로 변환시킬 수 있습니다. 이 방법은 객체지향 프로그래밍 패러다임에 있다면 흥미로운 방법인데, 이는 Flow가 항상 단일 통로로 감소되기 때문입니다.

하지만 우리가 KΛTEGORY 를 사용해 함수형 프로그래밍 방법으로 문제를 해결하면 어떨까요?

우리는 Either<A, B>유형으로 결과에 대한 이중성을 쉽게 반영할 수 있습니다. Either는 분리 동맹입니다. 즉, A 또는 B이지 A와 B는 절대로 성립할 수 없습니다.

참고로 Either<A, B>는 sealed class 로 Left<a: A>또는 Right(b: B)의 2개의 구현체를 가지고 있습니다. (역주: sealed class 에 대한 설명은 Kotlin – Sealed class for Restricted Class Hierarchies 글에서 볼 수 있습니다. )

함수형 패러다임의 협약에 따라 예외와 성공에 대한 사례를 Either 를 사용하여 제시할 때 왼쪽은 예외, 오른쪽은 성공에 대한 사례를 제시해야 합니다.

여기서 볼 수 있다싶이, Either는 우리의 목적을 완전히 채울 수 있습니다. 그러므로 Kotlin 으로 넘어가서 Either<CharacterError, List<SuperHero>>를 사용하여 어떻게 이중성을 모델링하여 이득을 볼 수 있는지 살펴봅시다.

첫번째로, sealed class를 사용하여 발생할 수 있는 예외들을 정의하려고 합니다. 그 후에는 외부 소스에서 일어날 수 있는 모든 가능성을 정의하는 것입니다. DataSource또는 Repository에서 발생하는 어떠한 예외는 위에 선언된 모델에게 매핑되어야 합니다.

자, 이제 슈퍼히어로 들의 데이터를 가져오기 위한 DataSource 구현체를 살펴봅시다.

자, 히어로들은 서비스를 통해 가져와서 정의한 모델에 맞게 매핑됩니다. 그리고는 Right(heroes)로 감싸져 반환됩니다. 이는 성공 결과에 대한 오른쪽 부분입니다.

하지만 무언가 잘못되면 예외가 발견되고 모델에 매핑됩니다. 그리고는 Left(error)로 감싸져 반환됩니다.

따라서 데이터 레이어 들은 완전히 명시적인 유형 (Either<CharacterError, List<SuperHero>>)에 의해 반환됩니다. 메서드 구현체를 보면, 요청자는 단순히 오류를 반환하거나 유효한 히어로 리스트를 반환할 것임을 알 수 있습니다. 간단해요. 그렇죠?

이 사례는 매우 직관적인데, Either는 두 개의 가능한 값을 fold 하여  두 개의 다른 결과 유형에 대해 두 개의 람다를 제공합니다.

DataSource에서 돌아온 값에 따라 (Left 이거나, Right거나) 해당 람다가 실행됩니다.

따라서 예외를 사용하여 오류를 기록하고 성공적인 값을 반환한 후에도 여전히 왼쪽에 있는 그대로의 값을 반환합니다. 그렇지 않을 경우에는 유효하지 않은 히어로 리스트를 필터링하여 올바른 리스트를 가져옵니다. (예를 들어 유효한 이미지 URL이 없는 것과 같습니다.)

따라서 전체 코드는 다음과 같을 수 있습니다. 이미 구성된 연산을 통해 각각 다른 케이스에 따라 수행될 수 있습니다.

아시다 싶이, 우리는 함수형 파라미터로서 항상 의존성을 수동으로 전달할 수 있다는 것을 알 수 있습니다.

함수형 프로그래밍에서는 데이터를 변환 / 조작하는 중간 작업에 대해 대부분의 시간을 사용하지 않습니다. 수행 결과에 따른 오류 또는 성공적인 히어로 리스트 만이 있을 뿐입니다.

아직 살펴보진 못했지만 제가 보여드린 모든 기능들이 패키지 레벨에서 정의되어 있습니다. 그들은 어떠한 경우에도 속하지 않습니다. 즉, 부작용 없는 순수한 기능을 가진 순수 함수이기 때문에, 공유 상태가 없고 외부 상태에도 접근할 수 없기 때문에 이러한 ‘구현 세부 사항’ 에는 동봉되지 않습니다. 순수 함수는 다양한 파라미터(종속성)을 제공하고 이를 통해 결과를 제공할 뿐입니다. 이것이 전부입니다.

따라서 편의상 의존성은 함수 파라미터로서 전달됩니다. 네, 이 글을 조금만 더 읽는다면 그 것들을 없앨 방법을 찾을 수 있을 것입니다. (DI. Yay! 🎊)

만약 KΛTEGORY를 사용한 에러 핸들링 전략을 보고 싶다면, 공식 문서 상의 이 섹션을 보세요.


그래서 우리의 웅장한 사원이 눈 앞에 나타나기 시작했습니다. 우리는 이미 기초가 마련되어 있습니다. 어쩌면 벽이 필요할까요? 겨울이 다가오고 있으니 계속 움직여 봅시다.

Async + Threading

아마도 여러분은 Asynchrony 나 Threading 에 대해 무시하고 있다는 것을 알아챘을 것입니다. 하지만 우리는 그것들을 다룰 수 있는 접근법을 찾아야 합니다.

매번 우리가 화면에 어떤 것을 그리거나 외부 데이터 소스에 대해 쿼리를 요청할 때 마다 실제로 수행하는 작업은 I/O 계산입니다. 이러한 계산은 부작용을 낳기 때문에 우리의 함수형 패러다임에서는 좋은 역할은 아닙니다. 우리는 Presentation 레이어와 그 밖에 모든 레이어를 시작하기 위해 순수하게 시작할 필요가 있습니다. 따라서 최소한의 DataSource를 위해서는 무엇인가 해야 합니다.

여기서 IO Monad 가 진가를 발휘합니다. 지금 당장은 ‘M’ 이란 글자를 잊어버리세요. 솔직하게 말하자면 이것은 접근법을 이해하는 데 필요하지 않고, 더 쉬워질 것입니다. 나는 그것이 무엇인지는 확실히 알고 있고, 아마 이 시리즈의 끝이면 여러분도 알 수 있을 것입니다.

IO는 부작용, 계산을 포함하여 순수하게 만듭니다. 아직 실행되고 있지 않기 때문에 우리가 그것을 실행하기로 결정하면서 안전하지 않은 수행을 하기로 결정하는 순간에 달려있기 때문입니다.

예를 들어 IO는 Haskell 에서 잘 알려지고 중요합니다. 어떠한 부작용도 언어에선 허용되지 않기 때문입니다. IO 덕분에 우리는 Monad 스택을 유지할 수 있고, 호출하는 트리의 반환 유형에 대해 명시적으로 계산을 수행할 수 있기 때문입니다.

자, 우리의 DataSource구현체를 좀 더 강화시켜 봅시다.

runInAsyncContext는 문법을 위해 생성한 함수입니다. 우리는 Coroutine 안에 있는 람다를 실행하고 그 연산을 IO의 Context 로서 들어 올리기 위해 사용하고 있습니다.

각각에 대해 파라미터에 대해 설명해보면…

  • f: coroutine 안에서 실행될 함수
  • onError: 계산 과정에서 예외가 발생될 경우 실행될 람다입니다. throwableCharacterError로 모델링합니다.
  • onSuccess: 계산이 성공했을 때 실행될 람다입니다. 히어로에 대한 리스트를 매핑하고 반환하기 전에 Right로 감쌉니다.
  • AC: AsyncContext, 제한되지 않은 콜백으로부터 ‘IO’ 와 같은 비동기를 지원하는 유형으로 데이터를 이동하는 typeclass 입니다.

보시다싶이, 반환 유형은 실행 결과에 대해 명시적으로 선언되어 있습니다 : IO<Either<CharacterError, List<SuperHero>>>

이 의미는 DataSource가 IO 계산을 반환하는데, IO 계산은 CharacterErrorList<SuperHero>를 얻을 수 있게 해줍니다. 의미론적으로 유형이 스스로 말하는 것입니다.

실제 사용 사례를 살펴보면, 좀 특별할 것입니다.

이 사용 사례 함수는 map 함수를 두 번 호출하는데, 이렇게 되어야 히어로의 데이터 소스에 대한 2개의 중첩된 Monad 가 있기 때문입니다. 따라서 IO로 인스턴스를 map하여 나중에 Either로 매핑할 수 있게 합니다. 이 의미는 계산이 감싸지지 않고 실행된다는 의미는 아니고, Monad 를 이용하여 선언적으로 연산 스택을 작성합니다. 모든 작업은 최대한 나중으로 연기됩니다.

Monad 스택의 작성 이후에 이 두 번의 매핑을 자연스러운 반복을 제공하는 흥미로운 스타일로 단순화 할 것입니다. 아마도 전체 스택을 한 개의 유형으로 합성해서 자연스럽게 이 문제를 해결할 수 있는지 보게 되겠죠.

그나저나, Either<A, B>는 오른쪽에 치중되었다는 것을 알리고 싶습니다. 이 의미는 mapflatMap는 오른쪽에 값이 적용되고, 왼쪽은 값이 남아있기 때문입니다.

드디어 우리는 우리의 Presentation 레이어에 도달했습니다. 우리는 IO 에게 부작용이 있어서 이상적인 형태는 아니지만 일단은 안전하지 않은 작업을 하도록 합니다. 다만, 단지 반복 과정에 대해서만 입니다.

여기에서는 지난번에 했던 것과 비슷하나 이번에는 IO가 작업을 실행합니다. 따라서 unsafeRunAsync는 IO를 결과적으로 풀어서 계산을 실행할 수 있도록 정리합니다. 그리고 결과값을 람다로 넘겨줍니다.

이제 뷰는 결과에 따라서 오류를 표시하거나, 히어로를 표시할 수 있습니다.

하지만 이것은 차후에도 반복할 수 있습니다. 이상적으로 우리는 시스템의 가장자리에 있는 단일 지점에 영향을 미칠 것입니다. Android는 Activity 나 View의 오버라이딩 메소드일 것이며, 나머지 프로그램들은 엔드포인트나 main 메소드가 될 것입니다.

이 곳이 바로 순수성이 안전하지 않은 작업으로 바뀌는 곳인데,  안드로이드에서 공유 상태를 가지고 화면에 렌더링하기 위해 필요하기 때문입니다. 우리는 적어도 이러한 가능성 있는 문제들을 레이어들에 분리하고 전체적인 아키텍쳐 디자인을 순수성에 기반하게 만듭니다.

이 것을 달성하기 위해서는 lazy evaluation 라고 불리는 것을 적용합니다. 우리는 단순히 호출 트리에 모든 기능들을 넣어두면 됩니다. 앞선 글에서도 설명했듯이 이미 계산된 결과 대신 실행하는 함수를 넘길 수 있습니다.

따라서 우리는 우리의 완전한 실행 트리를 아래와 같이 합성할 수 있습니다. (pseudocode)

  • presenter(deps) = { deps -> useCase(deps) }
  • useCase(deps) = { deps -> dataSource(deps) }
  • dataSource(deps) = {deps -> deps.apiClient.fetchHeroes() }

이 단계들은 이미 계산된 결과 대신 함수를 반환합니다. 이 함수들은 값이 넘겨질 때 계산을 수행하지, 미리 수행되지 않습니다. 결과적으로 DataSource가 필요한 의존성 들을 선택해서 작업을 할 수 있게 합니다.

따라서 뷰가 Presenter / ViewModel를 호출할 때 이미 계산된 결과 값이 아닌, 계산 결과에 따른 함수를 반환할 수 있도록 합니다. 그래서 전체 실행 tree를 최종적으로 풀어서 필요한 의존성들을 통과할 시기를 결정하는 것이 View Implementation 입니다.

하지만 의존성을 계속 넘기는건 고통받는데, 이걸 자동으로 할 수 있지 않을까요?

분명하게 그건 Dependency Injection을 말하는 거겠죠.


우리는 이미 벽을 모두 세웠습니다. 아직까지 지붕은 없으나 거의 다 했습니다. 적어도 야생 스님들은 더 이상 밤에 몰래 사원에 들어갈 수 없을 것입니다. 👏👏 그리고 우리는 심한 감기 😓😓 에 걸릴 수도 있지만 일단 사원 안에서 사는 것을 생각할 수 있습니다.

아마 우리는 조금만 더 반복하면 될 것입니다.

Dependency Injection

Dependency Injection 에 대해서는 우리는 조금 이상한 이름인 Render Monad 라는 것을 사용할 것 입니다. 새로운 방법에 대해 알기 전에 이 글을 먼저 읽어보는 것을 추천합니다.

Monad 스택을 합성하고 있기에, Reader 를 사용하여 완벽하게 맞을 수 있게 하길 원합니다.

Reader는 (D) -> A 유형으로 계산을 감싸며, 저런 유형으로 계산을 활성화 시킬 수 있도록 합니다. 

D 는 Reader Context 를 가리키는데, 연산이 실행되는 데에 필요한 의존성을 나타냅니다. 이러한 계산들은 현재 우리가 가지고 있는 각각의 아키텍쳐에 있어서 가지고 있는 것과 정확히 같은 것들입니다. 그렇죠?

또한 Reader 들이 모든 것을 계산하는 방식 덕분에 자동적으로 의존성을 약화시킵니다.

따라서 우리에겐 해결해야 될 두 가지 염려할 사항이 있습니다.

  • 연산을 수행하기 위해 모든 레벨에서 연산을 실행합니다. (의존성을 통과하기를 기다리는 함수)
  • 서로 다른 기능의 호출을 통해 의존성을 자동으로 전달하여 의존성을 무시하므로 직접 사용할 필요가 없습니다.

이 아이디어는 Reader 를 사용하여 반환 유형을 감싸는 것입니다. 자, 여기에 DataSource의 다른 구현체가 있습니다.

무서워 하지 마세요. 나도 우리가 유형에 대해 조금씩 상실하고 있다는 것을 압니다. 하지만 전에도 말했지만 이 시리즈의 다음 글에서 이 것을 고쳐나갈 것 입니다. 지금은 저를 믿으세요! 🤗

아마도 여러분은 전에 있던 계산이 이미 있다는 것을 눈치챘을 것입니다. 다만 이제는 Reader로 매핑하고 있다는 점입니다. 하지만 Reader는 어디에서 올까요? 언뜻 보면 정적인 것 처럼 보는 것 같은데, 그렇지 않나요?

함수의 시작 부분을 보면 아래와 같은 문장을 찾을 수 있습니다.

Reader.ask<GetHeroesContext>().map { ctx -> ... }

GetHeroesContext 는 Reader Context, 즉 D입니다. GetHeroesContext는 필요한 모든 의존성을 제공하는 데이터 클래스 입니다. Context는 이전에 계산되는 것이 아닌 전체 계산 트리를 실행하는 순간에 인스턴스화 됩니다.

D에 대한 의존도는 완전한 호출 체인을 위해 제가 필요로 하는 것 입니다. 다른 말로 말하자면 D는 Dagger 의존 그래프나 액티비티, 애플리케이션 별로 구축하는 모든 바인딩을 포함하는 구성 요소에 적합합니다.

ask()호출은 Reader 의 companion object 들 중 한 부분인데, { (D) -> D }로 계산을 감싸서 ReaderT 로서 사용할 수 있도록 정적으로 반환합니다. 따라서 우리는 Reader에게 모든 의존성을 포함하고 있는 D에게 접근하여 map할 수 있습니다. 그것이 바로 람다 안에 있는 저것들을 사용할 수 있는 이유입니다.

따라서 반환 유형은 다음과 같습니다.

Reader<GetHeroesContext, IO<Either<Error, List<SuperHero>>>>

이러한 유형의 중첩은 하나의 중첩된 유형으로 축소하기 위한 추가적인 반복이 필요합니다. 그것이 바로 어떤 함수형 패러다임 개발자라도 큰 Monad 스택을 다룰 수 있게 하는 것입니다. 하지만 아직까지는 공개할 수 없습니다. 😉

유형이 어떻게 되어있는지 살펴보세요.

의존성(GetHeroesContext) 를 실행하기 위해 일부 의존성을 기다리고 있는 계산(Reader) 를 지연시킵니다. 이 때서야 오류 또는 유효한 히어로 목록을 반환할 수 있는 IO 계산을 수행할 수 있습니다.

만약 우리가 호출 체인으로 돌아간다면, 우리는 사용 사례가 예전과 같은 방식으로 변화가 없이 진행되고 있는지 알 수 있습니다. Noise를 줄이기 위해 반환 유형을 제거했지만 가능한 추가해주세요. 반환 유형을 명확히 명시하는 것은 좋은 일 입니다.

또한 Presenter 코드는 정말로 비슷하지만 이제 우리는 다른 작업을 하기 전에 Context를 가진 Render를 들어올리기도 합니다. 따라서 의존성이 포함된 Context에 대한 접근 권한을 자동으로 확보할 수 있습니다.

이번엔 흥미로운 일을 하려고 합니다. 왜냐하면 우리는 데이터 클래스이기 때문에 그 Context에 대한 해체를 적용합니다. 다만 Context는 여전히 존재합니다.

(_, view: SuperHeroesListView) -> ...

우리가 의존성들 중 하나에 접근하기 위하여 (아마 MVP view contract가 될 것입니다)  신속히 접근할 수 있도록 해체를 신청할 수 있습니다.

나머지 부분은 예전에 했던 것과 똑같이 계속되고 있습니다. 하지만 이번에는 계산을 마치고 Reader라는 수식어를 반환합니다. 이제 View Implementation 을 통해 Presentation Pure function (=프리젠테이션 순수 기능) 을 호출할 수 있으며, 나중에 의존성이 있는 상태에서 준비되면 선택할 수 있습니다.

자, 이제 우리가 사용하려고 하는 Reader context를 넘길 차례입니다. 👏

이 덕분에, 우리는 Presenter로부터 감지될 수 있는 효과를 이끌어 낼 수 있었고, 완전히 순수한 실행 트리가 완성되었습니다. 부작용도 없고, 상태도 없습니다. 대승리네요.

의존성 트리의 어느 지점에서든 의존성을 테스트할 때 의존성을 바꿔야 하는 경우에는 복잡한 프레임워크나 라이브러리가 필요하지 않습니다. 실제 상황에서 확장할 수 있는 다른 Context의 Instance를 제공할 수 있어야 하며, Mockito 가 수행하는 것과 동일한 인스턴스 내에서 필요한 인스턴스를 제공해야 합니다.


정리

올바르게 구축된 Monad 스택은 수년간 안드로이드 앱이 제공할 수 있는 모든 우려 사항을 해결할 수 있습니다. 이러한 문제를 해결하기 위해 사용되는 방법과 접근 방식은 오류 처리, Dependency Injection, IO 계산 등과 같은 완전한 일반적인 문제입니다.

이러한 전략들은 향후 작성하는 다른 모든 애플리케이션과 공유할 수 있습니다. 애플리케이션이 아니더라도 Kotlin을 실행할 수 있는 플랫폼이라면 어떤 것이든지 공유할 수 있습니다. 실행할 수 없다고 해도 똑같은 접근법을 이용해 함수형 패러다임으로 전략을 도입할 수 있습니다.

아마도 RxJava가 도입되기 시작했을 때와 같이 안드로이드 개발자에게는 잘 쓰이지 않던 새로운 패러다임이다 보니까, 어느정도 익숙하지 않은 건 사실입니다.

그것이 반드시 나쁘거나, 성공할 수 없다는 것은 아닙니다. 이전 글에서 설명했던 것과 같은 매우 흥미로운 혜택을 제공하기 때문에 적어도 시도해보고 조사할 가치는 충분히 있습니다. 저는 여러분이 이 것을 이해할 수 있도록 노력하라고 젱나합니다. 왜냐하면 여러분이 이해하는 것 처럼 더 나은 개발자가 될 수 있기 때문입니다.  😊

다음 장을 기다리세요. Monad Transformers 라는 새롭고 멋진 기능을 사용하여 중첩된 유형을 단순화 시킬 것입니다. 이것이 바로 진정한 함수형 프로그래머가 이 것에 대해 반복하는 것입니다.

샘플 저장소에 다가가는 것을 잊지 마세요! nested-monads 라는 모듈에서 실제 안드로이드 앱에 이 접근 방법이 어떻게 쓰여지고 있는지 보여주고 있습니다.

트위터에서 @JorgeCastilloPr 를 추가하면 이 주제와 다른 것들에 대해 알 수 있습니다.


“마침내 웅장한 사원을 짓게 되었습니다. 공기를 느끼며 하늘을 만져보세요. 그리고 안에서 영원히 평화롭게 사는 것 입니다. “