하마
3k
2017-03-20 17:26:40.0 작성 2017-03-21 12:51:58.0 수정됨
1
255

[Play2] 평범한 사람들을 위한 Iteratees 이해 (Scala기반)


http://mandubian.com/2012/08/27/understanding-play2-iteratees-for-normal-humans/  내용을 힘들게 번역하고 감수했습니다. 후반부에 많이 졸렸지만... Scala 와 Play2 를 시작하는 분에겐 도움이 되었으면 합니다. 


참고로 Play2 는 뼛속부터 비동기로 이루어져 있기 때문에 최강의 고성능&부드러운 서버라는 장점을 가지고 있지만, 비동기라는 그리 직관적이지 않은 기술을 내부에 포함하고 있기 때문에 때론 굉장히 헥깔리게 만들기도 합니다. 하지만 Scala 언어및 다양한 동시성 라이브러리의 지원으로 추상층을 끌어올려 아주 간단한 코드로 그런 강력한 능력을 얻게 해줍니다.

우리가 SQL 문을 작성할때, 그 짧은 코딩으로 매우 많은 일들이 물밑에서 이루어지는 것처럼 즉 모든것을 알지 않아도 편하게 소기의 성과를 이루는 것처럼, Play2 내부에서 이루어지는 모든것을을 굳이 이해하지 않아도 됩니다. "해결" 을 하는게 응용개발자의 목적이니까요. 



평범한 사람들을 위한 Play2 Iteratees 의 이해 


Play2 를 시작하고 나면 아마  Iteratee 와 그의 형제들인  Enumerator and Enumeratee에 대해 관심이 생겼을꺼야. 그리고 나서 좀 어버버하겠지 ㅋㅋ 이 기사의 목적은 어버버하고 있는 평범한 우리 모두를 위한 정리라고 보면 되. 거창한 함수형/수학 이론을 배제하고 말이지.



이 게시물은 Iteratee / Enumerator / Enumeratee 모든것을 설명하는 것이 아님. 
나중에 기회되면 Iteratee/Enumerator/Enumeratee 에 관련된 주요 실습코드에 대해 작성할 예정.


소개

Play2 doc문서에서 Iteratees 는 데이타 스트림을 논블럭에서 리액티브적으로 조작할 수 있고 분산환경에서 현대적인 웹 프로그래밍을 할때  일반적이고 강한 구성을 할 수 있는 툴이라고 소개한다.

 먼가 쩔지? 그렇지 않나?
 근데 Iteratee 가 정확히 먼데?
 Iteratee 와 일반적인 Iterator 와 무슨 차이가 있어?
 어떻게 사용하고 어떤 상황에서 사용하지?
 꽤 복잡해 보이는데  안그런가?

만약 당신이 실용주의자 (게으르고) 이고 필요한 몇가지 것들만 알길 원한다면 

  • Iteratee 는 넌블럭과 비동기적으로 데이터들을 순회하는 추상층이다. 
  • Iteratee Enumerator 라 불리는 (소비하는것과) 동일한 타입의 데이타 뭉치들을 생산하는 것으로 부터 해당 타입의 데이터를 소비 하는데 사용된다.
  • Iteratee 는 iteration 스텝마다 데이터 청크에 대한 결과를 연속,순차적으로 계산 할 수 있다.
  • Iteratee 는 여러 "Enumerators" 를 넘어 재사용될 수 있게 쓰레드 안전하고 불변객체를 다룬다.

첫번째 충고 : GOOGLE 에서 ITERATEES 에 대한 정보를 찾지 마라.  

Google에서 Iteratee를 검색하면 순수한 기능적 접근 방식이나 수학 이론을 기반으로 하는 매우 모호한 설명을 찾을 수 있다. 심지어 Play Framework에 대한 문서  (역주:  초보자에게 매우 어려울 수도 있는 low 접근법에 대해 Iteratee 를 설명함 ;;; 사실 Play2 에 대한 설명은 대부분 어려운거 같다. 그러다 보니 유튜브나 블로그를 통한 쉽게 설명한 글이 나오지도 않는거 같다. 원래 직관적이지 않은 비동기는 어려우니깐 라고 말하고 싶기도 하다.) 도 마찬가지~

Play2의 초심자로서, 데이터 청크를 조작하는 추상적인 방식으로 제시 된 Iteratee 개념을 다루는 것은 어려울 수도 있긴해요.. 희귀하고 오컬트적으로 복잡하게 보일 수 도 있고..그래서 사용하기 싫어지고..

하지만 Iteratees가 웹 애플리케이션에서 데이터 흐름을 조작하는 데  흥미롭고 강력한 새로운 방법을 제공하기 때문에 이 훌륭한 개념을 사용 안 하는것은 부끄러운 일이다. 포기하지마시라~

나는 여러분들을 포기하고 싶지 않기 때문에 최대한 간단한 방법으로 설명하려고 한다. 기능적 개념에 대한 이론적인 전문가인 척하지 않을 것이며 ,뭐 잘못된 것들을 말할 수도 있지만 Iteratee가 의미하는 것을 평범한 여러분이 잘 반영 할 수 있는 방법으로 글을 쓸 것이다. 


이 기사에서는 코드 샘플에 대해 스칼라를 사용하지만 코딩에 대한 어느정도의 개념을 갖춘 사람이라면 코드를 이해 할 수 있을 것으로 본다. 따라서 너무 심하게 이질적인 연산자를 사용하지는 않기로 약속하며 > <>> <>> <>> <> . 코드 샘플은  Play2.1 마스터 코드를 기반으로 하므로 Iteratee의 코드가 단순화되고 앞으로 사용하기에는 더 합리적일 것이다.  따라서 API가 Play2.0.x와 같이 보이지 않는 경우 놀랄 필요가 없습니다요.


iteration 떠올려 보기  

Iteratee 의 바다로 뛰어 들기 전에, 나는 내가 iteration 이라고 부르는 것을 명확히 하고 Iterator의 개념에서 Iteratee로 점진적으로 가려고 한다.

Java에서 찾을 수 있는 Iterator 개념을 알죠?  반복자 (Iterator)는 컬렉션에 대해  반복의 각 단계에서 무언가를 수행 한다. List [Int]의 모든 정수를 합하는 매우 간단한 반복부터 시작해본다.


 Iterator의 자바스러운 구현 방식 

val l = List(1, 234, 455, 987)

var total = 0 
var it = l.iterator
while( it.hasNext ) {
  total += it.next
}

total
=> resXXX : Int = 1677

특별할것도 없다. 컬렉션을 순회하는 이 코드의 의미는:

  • 컬렉션에서 iterator 를 얻고,
  • itorator 로 부터 요소를 얻고 (만약 있다면 ) ,
  • 뭔가를 하고 : 여기서는 요소값을 total 변수에 더함 ,
  • 요소가 더 있다면 다음 요소로 이동
  • 반복
  • 요소가 없을 때까지 순회한다..

클렉션을 순회하는 동안 우리가 조작 하는 것들은:

  • iteration 의 상태 ( 끝났냐? 더 이상 요소가 없나?)
  • A context 업데이트 (여기서는 total) 
  • An action updating the context

 Scala for-comprehension 으로 다시 작성해보자

for( item <- l ) { total += item }

직접 iterator 를 사용하는 것 보다 나아졌다.


좀 더 함수형적인 방식으로 다시 작성해보자

l.foreach{ item => total += item }

List.foreach 함수는 익명 함수 (Int => Unit)를 매개 변수로 받아들이고 목록을 반복한다. 목록의 각 요소에 대해 컨텍스트 (여기서는 총계)를 업데이트 할 수있는 함수를 호출한다.

익명 함수는 컬렉션을 반복하는 동안 각 루프에서 실행되는 액션을 포함한다.


더 일반적인방식으로 다시 작성해보자

익명의 함수는 다른 장소에서 재사용 될 수 있도록 변수에 저장 될 수 있습니다.

val l = List(1, 234, 455, 987)
val l2 = List(134, 664, 987, 456)
var total = 0
def step(item: Int) = total += item
l foreach step

total = 0
l2 foreach step

아마 나에게 말하고 싶은게 있을거 같다: "이것은 구린 디자인이야. 저 함수는 부수효과를 가지고 있고, 좋은 디자인이 아닌 변수를 사용하고 있지. 심지어 두 번째 호출에서는 총계를 0으로 재설정해야해."

사실 맞는 말이다 -.-:

부수효과를 가진 함수는 꽤 위험하지  왜냐하면 그것들은 함수의 외부에 있는 어떤 것의 상태를 바꾸기 때문이야. 이 상태는 함수와 배타적이지 않고 잠재적으로 다른 스레드의 다른 엔티티에 의해 변경 될 수 있어. 부수효과가 있는 함수는 깨끗하고 견고한 디자인을 권장하지 않으며 스칼라와 같은 함수 언어는 이런 함수를 엄격하게 (예 : IO 작업) 줄이는 경향이 있다.

변형가능 변수는 위험성을 내포하고 있어 왜냐하면 2 개의 쓰레드가 변수의 값을 변경하려 한다면, 누가 이기지? 이 경우에는 Play2 (비 블로킹 웹앱)가 쩌는 웹프레임워크가 되는 이유 중 하나를 망가뜨리는 변수를 쓰는 동안 스레드 차단을 의미하는 동기화가 필요해.. 낭비지..

부수효과 없는 불변적인 방식으로 코드 재작성하기 

def foreach(l: List[Int]) = {
  def step(l: List[Int], total: Int): Int = {
    l match {
      case List() => total
      case List(elt) => total + elt
      case head :: tail => step(tail, total + head)
    }
  }

  step(l, 0)
}

foreach(l)

코드가 꽤 늘었네 그렇지?

하지만 적어도 

  • var total 가 사라졌고.
  • step 함수는 반복의 각 단계에서 실행되는 동작으로 이전보다 더 많은 작업을 수행하며 step 는 반복 상태도 관리하며 다음과 같이 실행됩니다.

    • 만약 리스트가 비었으면 현재 total 을 리턴
    • 만약 요소가 리스트에 한개라면 total + elt 를 리턴 
    • 만약 리스트에 1개보다 더 많다면, step 를 tail 요소들과 리턴, 새로운 total은  total + head게됨


따라서 반복의 각 단계에서 이전 반복의 결과에 따라 단계는 2 가지 상태 중 하나를 선택할 수 있다.

  • 요소가 더 많으므로 반복을 계속하십시오.
  • 목록의 끝에 도달했거나 요소가 전혀 없으므로 반복을 중지하십시오.

알림 :

  • step 은 꼬리재귀 함수(재귀가 끝날 때 전체 호출 스택을 펼치지 않고 즉시 반환합니다.) 스택 오버플로를 방지하고 Iterator를 사용하여 이전 코드와 거의 비슷하게 동작합니다.) 
  • step 목록의 나머지 요소 및 새 합계를 다음 단계로 전송합니다.
  • step 부작용이없는 총계를 반환합니다.

그래서,이 코드는 각 단계에서 목록의 일부를 다시 복사하기 때문에 (단지 요소에 대한 참조만) 부작용이 없고 불변의 데이터 구조만 사용하기 때문에 조금 더 많은 메모리를 사용한다. 이것은 매우 강력하고 아무 문제없이 편한 마음의 배포를 할 수 있게 된다.


스칼라 컬렉션이 제공하는 훌륭한 기능을 사용하여 매우 짧은 방법으로 코드를 작성할 수있다.

l.foldLeft(0){ (total, elt) => total + elt }


이제 본론으로 들어가 보죠


하나씩  Iterator & Iteratees 에 대해 알아보자

이제 반복에 대해 명확히 했으므로 우리의 반복문으로 돌아가 보자!


이전 반복 메커니즘을 일반화 하고 다음과 같이 작성할 수 있다고 가정 해 보면:

def sumElements(...) = ...
def prodElements(...) = ...
def printElements(...) = ...

l.iterate(sumElements)
l.iterate(prodElements)
l.iterate(printElements)

예, 스칼라 컬렉션 API를 사용하면 많은 일을 할 수 있다 :)

다른것을 사용하여 첫 번째 반복과 구성 한다고 가정 해 보면:

def groupElements(...) = ...
def printElements(...) = ...

l.iterate(groupElements).iterate(printElements)


이 반복을 컬렉션이 아닌 다른 것에 적용하려고한다고 가정 해보자

  • 파일에 의해 점진적으로 생성되는 데이터 스트림, 네트워크 연결, 데이터베이스 연결,
  • 어떤 알고리즘에 의해 생성 된 데이터 흐름,
  • 스케줄러 또는 액터와 같은 비동기 데이터 생성자로부터의 데이터 흐름.

Iteratees는 정확히 이 의미이다 ...


여기에 반복문을 사용하여 이전 합계 반복을 작성하는 방법이 있다

val enumerator = Enumerator(1, 234, 455, 987)
enumerator.run( Iteratee.fold(0){ (total, elt) => total + elt } ) 

좋아, 이전 코드처럼 보이고 더 많은 일을 하지 않는 것 같군..

적어도, 그렇게 복잡하지는 않습니까? 그러나 코드를 보면 Iteratee는 Enumerator와 함께 사용되는 부분이 좀 어색할 것이며 이 두 개념이 밀접 하게 관련되어 있음을 알 수 있을 것이다.

앞으로 점점 둘 사이의 어색함은 줄어 들 것임을 약속한다.  
즉 enumerator 는 생산하고  Iteratee 는 그것을 가져다 무엇인가를 하며 소비하는 모습 말이다. 


이제 단계별 접근 방식으로 이러한 개념을 하나씩 살펴 보자.



><> Enumerator 에 대하여 ><>

Enumerator는 컬렉션 또는 배열보다 일반적인 개념이다.


지금까지 우리는 반복을 위해 컬렉션을 사용했지만 앞서 설명했듯이, 우리는 보다 일반적인 것들도 반복 할 수 있다. 즉각적으로 또는 비동기적으로 사용할 수 있는 간단한 데이터 청크를 생성 할 수 있을 것이다.

Enumerator 의 디자인 목적

간단한 Enumerator 의 몇 가지 예 :  

// 리스트와 같은 형식으로 다양한 타입에 대해 만들어지고 있는 모습

val stringEnumerator: Enumerator[String] = Enumerate("alpha", "beta", "gamma")

val integerEnumerator: Enumerator[Int] = Enumerate(123, 456, 789)

val doubleEnumerator: Enumerator[Double] = Enumerate(123.345, 456.543, 789.123)

// 파일로 부터 생산되는 모습 

val fileEnumerator: Enumerator[Array[Byte]] = Enumerator.fromFile("myfile.txt")

// 비동기적으로 즉흥적으로 만들어 지고 있는 모습 

val dateGenerator: Enumerator[String] = Enumerator.generateM(
  play.api.libs.concurrent.Promise.timeout(
    Some("current time %s".format((new java.util.Date()))),
    500
  )
)

Enumerator 는 정적으로 typed 된 데이터 청크의 PRODUCER이다.

Enumerator[E] 는 E 타입의 데이터 청크를 생성하며 다음과 같은 3 가지 종류가 있을 수 있다.

  • [E]는 타입 E의 데이터. 예를 들어, Input [피자]는 피자 데이터들 이다.
  • Input.Empty는 enumerator 가 비어 있음을 나타낸다. 예를 들어 빈 파일을 스트리밍하는 enumerator .
  • Input.EOF는 enumerator 가 끝났음을 의미한다.예를 들어, enumerator 는 파일을 스트리밍하고 파일의 끝에 도달 할 것이다.


위에서 제시한 청크 종류와 상태 (더있다 / 아니오 / 더 이상의 요소가 있음) 사이에 평행선을 그릴 수 있습니다.

사실, Enumerator[E] [E]는 Input[E]를 포함하므로 Input[E]을 입력 할 수 있습니다.

val pizza = Pizza("napolitana")
val enumerator: Enumerator[Pizza] = Enumerator.enumInput(Input.el(pizza))

// 빈 Enumerator 를 만듬.
val enumerator: Enumerator[Pizza] = Enumerator.enumInput(Input.Empty)

Enumerator 는 논블럭킹 생산자이다.


Play2의 주요 아이디어 및 강점은 넌블럭 & 비동기이다. 따라서, Enumerator / Iteratee는 이러한 철학을 반영하는게 당연하다. Enumerator 는 청크를 완전히 비동기 및 넌블럭 방식으로 생성하는데 즉, Enumerator 개념은 기본적으로 활성화 된 프로세스 또는 백그라운드 작업으로 인해 데이터 청크와 기본적인 관련이 없다고 할 수 있다.

위의 코드 조각을 dateGenerator 과 함께 기억하라. 이 코드는 Enumerator / Iteratee의 비동기 및 넌블럭 특성을 정확히 반영하고 있을까?

val dateGenerator: Enumerator[String] = Enumerator.generateM(
  play.api.libs.concurrent.Promise.timeout(
    Some("current time %s".format((new java.util.Date()))),
    500
  )
)


What’s a Promise?

.

그것을 이해하기 위해서는 새로운 글타래가 요구될 것이지만 대략적으로 말해보면 Promise [String]은 다음과 같은 의미이다. "미래에 있을  String 결과(또는 오류)를 제공 하는 것."  한편 현재 스레드를 차단하지 않고 바로 해제한다. 

역주 : Future 와 구분된다. 자세히 알고 싶으면 아래를 참고하시라.
Promise 란 ? http://hamait.tistory.com/771

Future 란? http://hamait.tistory.com/763

Enumerator 로 생산되는 것을 소비할 소비자가 필요하다.


넌블럭 속성으로 인해 아무도 이러한 청크를 사용하지 않으면 Enumerator 는 아무 것도 차단하지 않고 또한 아무 숨겨진 런타임 리소스를 소비하지도 않게된다.따라서 Enumerator는 소비 할 사람이 있는 경우에만 데이터 청크를 생성 하는게 의미 있어 진다.

그렇다면 Enumerator 에 의해 생성 된 데이터 청크들을 소비하는 것은 무엇일까?

빙고!!!  : Iteratee이다.


><> Iteratee 에 대하여 ><>

Iteratee 는 Enumerator를 반복 할 수 있는 일반적인 "도구" 이다. 


한 문장으로 뽑아본다면:

Iteratee는 순수 함수 프로그래밍에서 반복 개념의 일반화로 볼 수 있다.

Iterator는 반복되는 컬렉션에서 만들어 지지만 Iteratee는 반복되는 Enumerator에 대해 존재하는 일반 엔터티이다.


Iterator와 Iteratee의 차이점에 대해서 기억이 납니까?  아니라구요.....: 헐..

  •  Iteratee는 Enumerator (또는 다른 것)에 의해 생성된 데이터를 반복 할 수있는 일반적인 엔티티이다.
  •  Iteratee는 반복될 Enumerator와는 별도로 생성되고 Enumerator 가 제공됩니다.
  •  Iteratee immutable 이고 stateless 이며 서로 다른 enumerators 를 위해 재사용될 수 있다.

한마디로

Iteratee는 Enumerator에 적용되거나 Enumerator를 통해 실행됩니다.


Enumerator [Int]의 모든 요소의 합계를 계산하는 위의 예제가 기억나는지 모르겠다. Iteratee를 한 번 만들고 다른 Enumerators 에서 여러 번 재사용 할 수 있다는 것을 보여주는 동일한 코드가 있다. 

아래 코드를 보자.

val iterator = Iteratee.fold(0){ (total, elt) => total + elt }

val e1 = Enumerator(1, 234, 455, 987)
val e2 = Enumerator(345, 123, 476, 187687)


e1(iterator)      // or e1.apply(iterator)
e2(iterator)


val result1 = e1.run(iterator) // or e1 run iterator
val result2 = e2.run(iterator)

Enumerator.apply와 Enumerator.run은 약간 다른 함수이며 나중에 설명 할 것이니 안심하시라.

Iteratee 는 active 한 데이터 소비자이다.

기본적으로 Iteratee는 첫 번째 데이터 청크를 기다리고 바로 다음에 반복 메커니즘을 시작한다. Iteratee는 계산이 끝났다고 생각할 때까지 데이터 소비를 계속하며 초기화가 완료되면 Iteratee는 전체 반복 프로세스를 완전히 담당하고 중지 시기를 결정한다.


val iterator = Iteratee.fold(0){ (total, elt) => total + elt }

val e = Enumerator(1, 234, 455, 987)

enumerator(iterator)

위에서 설명한 것처럼 Enumerator는 데이터 청크 생산자이며 소비자가 이러한 데이터 청크를 소비 할 것으로 기대된다. 소비되고 반복되는것을 위해서는 Enumerator는 Iteratee 에 삽입 되거나 플러그드 되야 하며, 보다 정확하게는 첫 번째 데이터 묶음을 Iteratee에 주입 / 푸시 해야한다.

당연히 Iteratee는 Enumerator의 생산 속도에 의존된다. 느린 경우 Iteratee도 느리다.

종속성주입및 제어역전 패턴과 비슷하게 Iteratee / Enumerator 관계를 생각해 볼 수 있습니다.

Iteratee Iteratee는 ”1-chunk-loop” 함수이다.

Iteratee는 iteration이 끝났다고 생각할 때까지 하나씩 청크를 소비한다. 사실, Iteratee의 실제 범위는 덩어리 하나의 처리로 제한되며 이것이 하나의 데이터 청크를 소비 할 수 있는 함수로 정의 할 수 있는 이유이다.

Iteratee 정적 타입된 청크를 허용하고 정적 타입된 결과를 계산한다.

Iterator는 생성된 컬렉션에서 오는 데이터 청크를 반복하는 반면,  Iteratee는 좀 더 야심적입니다. 즉, 데이터를 소비하면서 무언가를 계산할 수 있습니다.

 Iteratee 모습이 이러기 때문인데 :

trait Iteratee[E, +A]

// E 는 데이터 청크들의 데이터의 타입.  따라서 오직 Enumerator[E] 에 의해 적용 됩니다.

// A 는 iteration 의 결과 타입 입니다. (역주: +A 는 C[T’]는 C[T]의 하위 클래스이다라는 공변성을 말합니다.)


첫 번째 샘플로 돌아가 봅시다 : Enumerator[Int] 가 생성한 모든 정수의 합계를 계산합니다.


val iterator = Iteratee.fold(0){ (total, elt) => total + elt }
val e = Enumerator(1, 234, 455, 987)

// runs the iteratee over the enumerator and retrieve the result

val total: Promise[Int] = enumerator run iterator


run의 사용법에 주목하자 : 우리는 비동기적인 세계에 있기 때문에 합계 자체 value 가 아니라 합계가 Promise [Int]임을 알 수있다. 실제 합계를 얻으려면 scala concurrent blocking 인 Await._ 함수를 사용할 수 있다. 그러나 이것은 블로킹 API 이기 때문에 반드시 필요한 곳이 아니라면 권장하지 않는다.  Play2는 완전히 비동기 / 논블럭이므로 Promise.map / flatMap을 사용하여 전파하는 것이 가장 좋겠다.


하나의 결과값을 얻기 위한 행위 뿐 아니라, 소비되는 모든 청크를 println 할 수 도 있고  

val e = Enumerator(1, 234, 455, 987)
e(Iteratee.foreach( println _ ))


e.apply(Iteratee.foreach( println _ ))


모든 청크를 List로 연결 할 수 도 있다. 예를 들어:

val enumerator = Enumerator(1, 234, 455, 987)

val list: Promise[List[Int]] = enumerator run Iteratee.getChunks[Int]


Iteratee 는 반복을 통해 불변 컨텍스트 및 상태를 전파 할 수 있다.

최종 합계를 계산할 수 있으려면 Iteratee는 부분 합계를 반복 단계를 따라 전파해야 한다.

즉, Iteratee는 이전 단계에서 컨텍스트 (이전의 총합)를 수신 한 다음 현재 데이터 덩어리로 새 컨텍스트를 계산할 수 있다. (새 총계 = 이전의 총계 + 현재 요소)  그리고 이 컨텍스트를 다음 단계 (다음 단계가 필요할 경우) 로 전파한다. 


Iteratee 는 간단한 상태 머신이다.

좋아, 멋지다. 하지만 Iteratee가 iterating을 멈추어야 한다는 것을 어떻게 알 수 있을까? 오류 혹은 EOF가 있거나 Enumerator 의 끝에 도달하면 어떻게 될까?

따라서 컨텍스트 외에도 Iteratee는 이전 상태를 받아야 할 일을 결정하고 다음 단계로 보내질 새 상태를 잠재적으로 계산해야 한다.

이제 위에서 설명한 고전적인 반복 상태를 기억하라. Iteratee의 경우 거의 동일한 2 가지 반복 상태가 있다.

  • State Cont : 반복은 다음 청크로 계속 진행될 수 있으며 잠재적으로 새로운 컨텍스트를 계산할 수 있다.
  • State Done : 프로세스가 끝났음을 알리고 결과 컨텍스트 값을 반환 할 수 있다.

그리고 꽤 논리적으로 보이는 제 3의 것:

  • State Error : 현재 단계에서 오류가 발생했음을 알리고 반복 반복을 중지합니다.


이러한 관점에서 우리는 Iteratee가  상태를 Done 또는 Error 로 전환하기 위한 조건을 탐지 할 때까지 상태 Cont를 반복하는 것을 담당하는 상태 기계라고 생각할 수 있게된다.

Iteratee 상태인 Done/Error/Cont 또한  Iteratee이다.

Iteratee는1-chunk-loop 함수로 정의되며 주요 목적은 하나의 상태에서 다른 상태로 변경하는 것임을 기억하시라. 그것들도 Iteratee 라고 생각해야한다.

우리는 3가지 상태의  Iteratees 를 가지고 있다.

Done[E, A](a: A, remaining: Input[E])

  • a:A 이전 단계로 부터 받은 컨텍스트
  • remaining: Input[E] 다음 청크를 표현 


Error[E](msg: String, input: Input[E])

이건 매우 이해하기 쉽습니다 : 오류 메시지와 실패한 입력.


Cont[E, A](k: Input[E] => Iteratee[E, A])

이것은 Input [E]를 취하고 또 다른 Iteratee [E, A]를 반환하는 함수에서 만들어지는 가장 복잡한 상태이다. 이 이론을 너무 깊이 생각하지 않고도 Input [E] => Iteratee [E, A]는 하나의 입력을 소비하고 다른 입력을 소비하고 다른 상태를 반환 할 수있는 새로운 상태 / iteratee를 리턴하는 방법이라는 것을 쉽게 이해할 수 있을 것이다. / iteratee 등 ... 상태가 완료 또는 오류에 도달 할 때까지.

이 구조는 (전형적인 기능적 방식으로) 반복 메커니즘을 공급(feeding)하는 것을 보장합니다.


Enumerator [Int]에 두 개의 첫 번째 요소의 총계를 계산하는 Iteratee를 작성해 보자.


def total2Chunks: Iteratee[Int, Int] = {
  
  def step(idx: Int, total: Int)(i: Input[Int]): Iteratee[Int, Int] = i match {
  
    case Input.EOF | Input.Empty => Done(total, Input.EOF)
  
    case Input.El(e) =>
  
      if(idx < 2) Cont[Int, Int](i => step(idx+1, total + e)(i))
  
      else Done(total, Input.EOF)
  }
 
  ( Cont[Int, Int](i => step(0, 0)(i)) )
}


val promiseTotal = Enumerator(10, 20, 5) run total2Chunks
promiseTotal.map(println _)

=> prints 30



이 예제를 사용하면 Iteratee를 작성하는 것이 받은 Chunk의 유형과 새 State / Iteratee를 반환하는 방법에 따라 각 단계에서 수행 할 작업을 선택하는 것과 크게 다르지 않다는 것을 이해할 수 있게 된다.



아직 졸리지  않은 사람들을 위한 몇 가지 수면제

Enumerator 는 단지 Iteratee를 다루는 도우미 일 뿐이다.

보시다시피, Iteratee API에는 Enumerator에 대한 언급이 없다.

이것은 Enumerator가 Iteratee와 상호작용하는데 필요한 단순한 헬퍼 일 뿐이기 때문에 Iteratee에 자체적으로 연결하여 첫 번째 데이터 청크를 주입 할 수 있다. 그러나 Iteratee가 Play2의 모든 곳에서 정말 쉽고 잘 통합되어 있기 때문에 Enumerator가 필요 없다.


 Enumerator.apply(Iteratee) 와 Enumerator.run(Iteratee) 차이점

앞에서 언급했던이 지점으로 돌아가 보자. Enumerator에서 주요 API의 서명을 살펴보면 

trait Enumerator[E] {

  def apply[A](i: Iteratee[E, A]): Promise[Iteratee[E, A]]
  ...
  def run[A](i: Iteratee[E, A]): Promise[A] = |>>>(i)
}

apply 는 마지막  Iteratee/State 를 리턴한다.

apply 함수는 청크를 소비하고 해당 작업을 수행하고 Iteratee의 Promise를 반환하는 Iteratee에  Enumerator를 주입한다. 이전 설명에서, 반환된 Iteratee가  Enumerator에서 요구한 청크를 소비 한 후 마지막 상태 일 수 있음을 스스로 판단 할 수 있다.


run 는 Promise[Result] 를 리턴한다.

run 3가지 단계를 가진다.

  1. 이전 apply 호
  2. Input.EOF 를 끝났음을 확인하기위해 Iteratee 에 삽입 
  3. Iteratee 로 부터 promise 로써 마지막 컨텍스트를 얻는다. 

예를 보면 :

val iterator = Iteratee.fold(0){ (total, elt) => total + elt }
val e = Enumerator(1, 234, 455, 987)

// just lets the iterator consume all chunks but doesn't require result right now

val totalIteratee: Promise[Iteratee[Int, Int]] = enumerator apply iterator

// runs the iteratee over the enumerator and retrieves the result as a promise

val total: Promise[Int] = enumerator run iterator


한줄 요약 

Iteratee의 결과가 필요할 때 run을 사용해야합니다.

결과를 검색하지 않고 Enumerator를 통해 Iteratee를 적용해야하는 경우 apply



Iteratee 는 Promise[Iteratee] 이다.(매우 중요) 

One more thing to know about an Iteratee is that Iteratee is a Promise[Iteratee] by definition.

Iteratee에 대해 알아야 할 또 하나의 사실은 Iteratee가 정의에 의한 약속 [Iteratee]이라는 것이다.

// converts a Promise[Iteratee] to Iteratee

val p: Promise[Iteratee[E, A]] = ...
val it: Iteratee[E, A] = Iteratee.flatten(p)

// converts an Iteratee to a Promise[Iteratee]

// pure promise

val p1: Promise[Iteratee[E, A]] = Promise.pure(it)

// using unflatten

val p2: Promise[Iteratee[E, A]] = it.unflatten.map( _.it )

// unflatten returns a technical structure called Step wrapping the Iteratee in _.it

Iteratee <=> Promise[Iteratee]

즉, Iteratee에서 매우 게으른 방식으로 코드를 작성할 수 있습니다. Iteratee를 사용하면 Promise로 전환하고 원하는대로 되돌릴 수 있습니다.



Enumeratee 에 대하여..

2 번째 충고 :  다 왔어. 당황하지마… Enumeratee 개념은 정말 단순해

Enumeratee 는 단지  Enumerator 와 Iteratee 사이의 파이프 어탭터일뿐 


Enumerator [Int]와 Iteratee [String, List [String]]가 있다고 상상해보라.

Int를 String으로 변환 할 수 있다. 그렇지 않나?하지만 그렇게 하기 위해서는 

Int의 덩어리를 String의 덩어리로 변환 한 다음 Iteratee에 삽입해야 한다.

val enumerator = Enumerator(123, 345, 456)
val iteratee: Iteratee[String, List[String]] =val list: List[String] = enumerator through Enumeratee.map( _.toString ) run iteratee

무슨일이 벌어졌나?
Enumerator [Int]와 Enumeratee [Int, String]를 Iteratee [String, List [String]]로 연결 했을뿐이다.

2번째 스텝:

val stringEnumerator: Enumerator[String] = enumerator through Enumeratee.map( _.toString )
val list: List[String] = stringEnumerator run iteratee

따라서 Enumeratee는 사용자 지정 열거자를 Play2 API에서 제공하는 일반 Iteratee와 함께 사용하도록 변환하는 데 매우 유용한 도구라는 것을 이해할 수 있게 되었다.

Enumerator / Iteratee로 코딩하는 동안 이것이 가장 많이 사용할 도구라고 확신 한다.


Enumeratee 는 Iteratee 없이 Enumerator 로 될 수 있다.


이는 Enumeratee의 매우 유용한 기능입니다. 열거 형 [From]을 열거 형 [To, From]으로 열거 형 [To]으로 변형 할 수 있습니다.

Signature of Enumeratee is quite explicit:

Enumeratee[From, To]

다음처럼 사용 할 수 있다.

val stringEnumerator: Enumerator[String] = enumerator through Enumeratee.map( _.toString )

Enumeratee 는 Iteratee로 변환 할 수 있다.

이것은 약간의 낯선 기능일 것인데 반복문 [To, A]을 반복자 [From, A]에서 Enumeratee[From, To]로 변형 할 수 있기 때문이다.


val stringIteratee: Iteratee[String, List[String]] =val intIteratee: Iteratee[Int, List[String]] = Enumeratee.map[Int, String]( _.toString ) transform stringIteratee

Enumeratee 는 Enumeratee로 구성될 수 있다.

Yes, this is the final very useful feature of Enumeratee.

val enumeratee1: Enumeratee[Type1, Type2] =val enumeratee2: Enumeratee[Type2, Type3] =val enumeratee3: Enumeratee[Type1, Type3] = enumeratee1 compose enumeratee2

다시 한번 말하지만, 일반적인 Enumeratees를 만든 다음 이를 사용자 지정 Enumerator / Iteratee에 필요한 사용자 지정 Enumeratee로 작성할 수 있다는 것을 쉽게 알 수 있습니다.


결론 


어쨌든 Iteratee / Enumerator / Enumeratee를 사용해야하는 이유는 무엇일까?


최신 웹 응용 프로그램은 더 이상 동적으로 생성 된 페이지가 아닙니다. 이제 다른 소스에서 가져온 데이터 흐름을 다양한 형식으로 서로 다양하게 조작하고 있습니다. 수많은 고객에게 엄청난 양의 데이터를 제공하고 분산 된 환경에서 작업해야 할 수도 있습니다.

Iteratee는 실시간으로 데이터 흐름을 처리하기에 안전하고 변경 불가능하며 매우 유용하기 때문에 이러한 경우에 매우 적합하다고 볼 수 있습니다. 


Note : 이상한 연산자들 

&>, >> >>, >> >>> 및 유명한 물고기 연산자> <>와 같은 Iteratee / Enumerator / Enumeratee를 기반으로하는 코드에서 이러한 연산자를 많이 볼 수 있습니다. through, apply, applyOn 또는 compose와 같은 실제 명시적인 단어의 별칭이 있습니다. 어떤 사람들은 오퍼레이터가 더 명확하고 더 간결하며, 어떤 사람들은 단어를 선호 할 것입니다.

예를들면  &> 는  through의 약자이다. 필터링을 한다는 뜻이다.



2
1
  • 댓글 1

  • duckgu
    144
    2017-03-21 09:16:43.0

    좋은 내용 감사합니다.

    0
  • 로그인을 하시면 댓글을 등록할 수 있습니다.