관리자

Reactive Programming과 Functional Reactive Programming 의 개념을 알아보자

 

Reactive Programming

Wikipedia는 역시 너무 어렵다.

a declarative programming paradigm concerned with data streams and the propagation of change. 
// 데이터의 순서와 변경과 관련된 선언적 프로그래밍 패러다임

 

더 직관적인 뜻을 찾아보자.

Reactive programming is programming with asynchronous data streams.
// 비동기 데이터 흐름을 다루는 프로그래밍
// 출처: https://gist.github.com/staltz/868e7e9bc2a7b8c1f754

 

글만 보면 막연하니, 그림을 보자.

블로그에 글을 작성하는 상황을 예를 들어서 생각해보자,

Data stream이란 시간의 흐름에 따라 진행되는 이벤트의 sequence를 말한다.

조금 쉽게 생각하면, 연속된 데이터의 처리/operation라고 할 수 있다.

블로그에 글을 작성하는 경우에는, 위의 순서로 이벤트가 발생한다 / 데이터가 처리된다.

 

reactive programming은 비동기 이벤트 흐름을 다루며, 이름 그대로 빠르게 반응하는 것을 목표로 한다.
빠르게 반응하기 위해서는 이벤트가 발생하는지 관찰하고(observe) 값이 변할 때마다 새로 연산을 수행해아한다.

 

이렇게, 객체 A에 변화가 생기면 객체 A를 관찰하는 observer들에게 변화를 통지하는 패턴을 observation design pattern이라고 한다.
비동기 이벤트를 처리하는 reactive programming은 observation design pattern을 기본으로 한다.

 

이벤트 하나가 완료되면, value / error / complete signal 중 하나가 발생한다.
observer는 발생값(value/error/complete signal)에 따라 다른 함수(handler)를 실행한다.

이벤트의 처리 신호를 듣는다(listen)고도 표현하며 처리 신호를 observer에게 보내는 것을 subscribe라고도 한다.

 

 

Functional Reactive Programming

Functional Reactive Programming은 Reactive Programming을 functional programming을 이용해서 구현한 것이다.

functional programming이란, 수학적 의미의 함수의 역할이 강조된 프로그래밍 기법이다.

수학적 의미에서의 함수란, input을 넣으면 output이 나오는 box로 생각할 수 있다.

 

Functional programming에서는 mutuable variable, assignment, loops를 지양하며, input -> output의 관계에 집중한다.

예를 들면, imperative programming에서는 흔히, 아래처럼 함수를 작성하지만,

def add(a:Int):Int = a + 3

 

functional programming에서는 state를 갖는 것을 피하고, 숨겨진 input과 output을 없도록 하기 위해서 아래처럼 작성한다.

def add(a:Int, b:Int) = a+b

여기서 숨겨진 input과 숨겨진 output을 각각 side-cause, side-effect라고 한다.

이것들이 많아지면 코드를 변경할 때 예상하지 못한 결과가 나올 수 있고,

코드를 변경할 때 모든 함수의 내부를 확인하면서 디버깅을 해야하는 어려움이 있다.

 

functional reactive programming은 결국 side effect를 줄이면서 reactive programming을 구현하는 방법인 것이다!

구체적인 구현 코드는 다음 글에서 알아보자

 

4 Reactive principles

Reactive programming은 4가지의 principle을 갖는다.

1) responsive : 빠르게 반응한다. reactive programming의 방법이기도 하다.
2) resilient : 어떤 상황에서도 responsive하게 동작하도록 한다.
3) scalable : load에 따라 responsive할 수 있도록 시스템을 업그레이드할 수 있다.
4) message driven : reactive programming을 구현하는 방법이다.
event-driven, actor-based, event-driven + actor-based가 있다.
event-driven : observer가 event를 관찰한다.
actor-based : message가 actor에게 직접 전달된다.

 

 

 

참고:

https://brunch.co.kr/@oemilk/79

https://m.blog.naver.com/jdub7138/220983291803#

https://gist.github.com/staltz/868e7e9bc2a7b8c1f754

https://blog.redelastic.com/what-is-reactive-programming-bc9fa7f4a7fc

1. Map에서 값을 꺼내올때

 

제일 당황스러웠던 것 중 하나가 아래의 경우였다.

for{
  k <- some List K
  L <- some Map M.get(k)
  c <- Some List L}
yield c

// M은 list를 value로 갖는 Map이다

 

간단히 말해서

List K에서 원소(k)를 뽑아서,

k에 해당하는 값(List L)을 Map M에서 찾아서

L의 원소 c를 가져오는 코드이다.

 

위에서 당연히 c에 L의 원소가 들어간다고 생각했는데, 그렇지 않았다.

두번째 줄 Map에서 원소를 꺼내 올때, 리스트가 나오는 것이 아니라 Some(value 인 List L) 형태로 나온다.

Map에 key가 없는 경우 때문에 이렇게 동작한는 것 같다.

그래서 Some을 unbind하기 위해서 getOrElse()를 사용해야한다.

그런데 나는 여기서 getOrElse()를 바로 사용하면 내 로직 상의 문제로 원하는대로 동작하지 않았다...

그렇지만 some에서 값을 꺼내오기 위해서는 어디선가 getOrElse()를 써야했고,

getOrElse에 None은 에러가 나고 List()는 생각과 다르게 동작하고, 난장 대잔치였다...

 

 

2. 빈 리스트 : List()에서 값을 꺼내올 때

 

List()는 값이 없기 때문에, 값을 꺼낸 뒤에 일어나는 operation들은 수행되지 않는다.

빈 리스트에 map을 하면 아무 일도 안 일어나는 것을 생각하면

(return값이 빈 리스트 List()이다. 전달한 함수 자체가 실행되지 않는다.)

당연한 부분인데, 일단 for {...} yield가 map으로 바뀐다는 것을 생각조차 못하고 있었으니...

전혀 생각하지 못한 부분이었다.

 

잘 모를 때는, line by line으로 하나씩 print해보자

 

 

3. =과 <-의 차이

 

yield를 사용하는지 여부에 따라 return 값이 scala인지 vector인지 결정된다.

그래서인지 {} 내부에서 <-를 사용할 수 있을 때와 없을 때가 있었다.

아직 정확히 파악은 못했지만, Map처럼 값을 꺼내올 때, Nothing일 수 있는 경우에는 =을 사용할 수 없는 것 같다.

1번에서 고생할 때, <-를 =로도 바꿔보기도 했는데 compile error가 나는 경우도 있었다.

반대로 yield가 없을 때 <-에서 에러가 나고 =가 잘 작동하는 경우도 있었다.

아직 잘 모르겠다 ㅠㅠㅠ 이건 좀 더 공부해봐야겠다.

 

Monad에 대한 이해가 더 필요한 부분인가 싶기도 하고...

하나씩 정리하다보면 조금 알지 않을까 싶다...

 

일단은 for 문이 어떤 function으로 변환되는지를 이해하는 것이 

제대로 작동하는 코드를 작성하는 첫걸음인 것 같다.

https://docs.scala-lang.org/tutorials/FAQ/yield.html

 

for {...} yield 는 python의 yield와는 다르다.

 

python의 yield는 iterator를 만든다.

scala의 yield는 high order functionsyntactic sugar일 뿐이다.

 

scala는

for (enumerators) yield e

형태를 이용해서 sequence를 표현하는데, 이것을 for comprehension이라 한다.

https://docs.scala-lang.org/tour/for-comprehensions.html

 

for {...} yield는 이러한 for comprehension 형태로 high order function을 나타낸 것뿐이다.

 

javascript의 callback 지옥처럼, 여러 개의 high order function을 이용하면,

다음처럼 계속해서 nested되어 코드가 보기 좋지 않다.

l.flatMap(sl => sl.filter(el => el > 0).map(el => el.toString.length))

그래서 위 코드를 for{...} yield를 이용해서 다음처럼 바꿀 수 있다.

for{
  sl <- l
  el <- sl
  if el > 0
} yield el.toString.length

(예시는 scala documenation에서 가져왔습니다.)

 

실제로 for {...} yield 형태로 작성한 코드는 foreach, map, flatMap, filter, withFilter변환된다고 한다.

어떻게 변환되는지 알아보자

 

 

1.foreach

for(x <- c1; y <- c2) {...}
c1.foreach(x => c2.foreach(y => c3.foreach(z => {...})))

 

 

2.map

for(x <- c; y = ...) yield {...}
c.map(x => (x, ...)).map((x,y) => {...})

 

y = ... 부분이 없다면, vector가 만들어지는 기본 for {....} yield 가 된다.

생각해보면 당연히 이것은 map function과 동일하다

for(x <- c) yield {...}
c.map(x => (x, ...))

 

 

3.flatMap

for(x <- c1; y <- c2) yield {...}
c1.flatMap(x => c2.map(y =>{...}))

 

 

4. withFilter / filter

for(x <- c; if cond) yield {...}

withFilter가 정의되어 있으면 withFilter로,

withFilter가 정의되지 않으면 filter로 변환한다.

c.withFilter(x => cond).map(x => {...})

 

개인적으로 처음 for {...} yield를 사용할 때, high order function으로 변환된다는 것을 제대로 이해하지 못해 고생했다.

단순히 map의 chaining으로 변환된다고 생각해서 막무가내로 코드를 작성했는데, compile조차도 안되고 난리도 아니었다.

error 원인도 다양했다.

{} 안에서 <-가 안되거나, type mismatch, withFilter is not defined.... 등등

 

처음에는 withFilter is not defined를 보면서, 이게 왜 나오지...? 생각했는데,

high order function의 syntatic sugar라는 것을 알고 나니 이해가 되었다.

물론 그걸 이해했다고 해서 에러를 해결할 수 있는 것은 아니다...@

 

그리고 무조건적으로 high order function으로 바뀐다고 할 수도 없는 것 같다.

scala documentation에 

a syntactic sugar for composition of multiple monadic operations

라고 표현된 것을 보면, 추가적인 의미가 있는 것 같다.

 

몇 가지 실험한 결과를 정리해서 다른 글에 정리해보려 한다.

 

그리고 제발, documentation을 잘 읽자.....

element의 class 이름을 확인하는 method

element.getClass

 

https://www.geeksforgeeks.org/determine-the-class-of-a-scala-object/

scala에서는 for {}를 통해 중첩 for문을 만들 수 있다.

단순히 여러 조건을 나열하는게 아니라

line마다 새로운 loop가 생긴다.

 

scala documentation에 따르면 for ... yield는 foreach, map, flatMap, filter or withFilter의 syntatic sugar일 뿐이다.

즉, foreach, map, flatMap, withFilter를 좀 더 편리하게 풀어쓴 것뿐이다.

취소선으로 표시한 부분은 좀 더 실험을 해봐야겠다.

 

1. for { 1 줄 } yield

 val t = for {
  e1 <- 1 to 3
} yield {
  e1
}

def main(args: Array[String]) {
  println(t)
}

// result
// Vector(1, 2, 3)

 

2. for { 2줄 } yield

for는 한번만 작성했지만, nested for loop같은 결과가 나오는 것을 확인할 수 있다.

generate되는 순서도 우리가 생각하는 for문과 동일하다!

새로운 line마다 새로운 for loop가 시작되는 것처럼,

첫번째 line이 outer loop이고, 아래로 갈수록 inner loop이다.

 val t = for {
  e1 <- 1 to 3
  e2 <- List('a', 'b', 'c', 'd')
} yield {
  (e1, e2)
}

def main(args: Array[String]) {
  println(t)
}


// result
// Vector((1,a), (1,b), (1,c), (1,d), (2,a), (2,b), (2,c), (2,d), (3,a), (3,b), (3,c), (3,d))

 

3. for { 3 줄 } yield

yield의 값이 (e1,e2)로 2번과 동일하지만, 결과는 2배로 길어진 것을 확인할 수 있다.

val t = for {
  e1 <- 1 to 3
  e2 <- List('a', 'b', 'c', 'd')
  e3 <- List('Z', 'X')
} yield {
  (e1, e2)
}

def main(args: Array[String]) {
  println(t)
}
// result
// Vector((1,a), (1,a), (1,b), (1,b), (1,c), (1,c), (1,d), (1,d), (2,a), (2,a), (2,b), (2,b), (2,c), (2,c), (2,d), (2,d), (3,a), (3,a), (3,b), (3,b), (3,c), (3,c), (3,d), (3,d))

+ Recent posts