관리 메뉴

HAMA 블로그

평범한 사람들을 위한 Play2 Iteratees 의 이해 [번역] 본문

PlayFramework2

평범한 사람들을 위한 Play2 Iteratees 의 이해 [번역]

[하마] 이승현 (wowlsh93@gmail.com) 2017. 3. 20. 12:28



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



http://mandubian.com/2012/08/27/understanding-play2-iteratees-for-normal-humans/   
내용을 번
역하고 감수했습니다. 후반부에 많이 졸렸지만... Scala 와 Play2 를 시작하는 분에겐 도움이 되었으면 합니다.  좀더 간단한 내용을 원하시는 아래 링크도 참고 하십시요.
http://hamait.tistory.com/767


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]을 입력 할 수 있습니다.

// create an enumerator containing one chunk of pizza

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

// create an enumerator containing no pizza

val enumerator: Enumerator[Pizza] = Enumerator.enumInput(Input.Empty)

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


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

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

// an Enumerator generated by a callback

// it generates a string containing current time every 500 milliseconds

// notice the Promise.timeout which provide a non-blocking mechanism

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)

// this injects the enumerator into the iteratee 

// = pushes the first chunk of data into the iteratee

enumerator(iterator)

// the iteratee then consumes as many chunks as it requires

// don't bother about the result of this, we will explain later

위에서 설명한 것처럼 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의 약자이다. 필터링을 한다는 뜻이다.




Comments