관리자

abstact class vs. trait

abstract class

abstract class는 class의 template으로,
implementation은 하지 않고 
어떤 variable과 method가 필요한지만 정의한다.

어떤 abstract class를 상속하는 class를 정의할 때는
abstract class에 명시된 값과 함수들을 모두 implement(작성)해야한다.

 

trait

trait도 abstract class와 동일한 개념이다.
하지만 abstract class는 1개의 class만 상속할 수 있는 반면,
trait는 여러 class를 상속할 수 있다.

따라서 가급적이면 trait을 쓰는 것이 좋다.

abstract class를 사용해야하는 경우

1) constructor argument를 요구하는 base class를 만드는 경우
2) java에서 이용할 코드인 경우

1) 의 예시
trait Animal(name: String)
//compile error


case class

scala에는 case class라는 재미있는 class가 있다.
switch case 문처럼 scala 에서는
match case문이 존재한다.
특이한 것은 match case와 case class문을 조합하면,
object의 class에 대해서도 switch case를 할 수 있다.

예를 들어보자.
어떤 수학 수식은 숫자(number)와 연산자(operator)로 구성된다.
숫자와 연산자를 포함하는 Expression이라는 abstract class(trait)을 정의하면,
수식을 Expression의 List로 나타낼 수 있다.

triat Expression
case class Number(val: Int) extends Expression {...}
case class Operator(name: String) extends Expression {...}
// Expression을 상속하는 class Number, class Operator 정의


object O {

    def expr(x : Base) = x match {
        case Number(val: Int) => println("x is in class Number")
        case Operator(name: String) => println("x is in class Operator")
    }

    // x가 Number의 instance 이면 "x is in class Name!"를 출력하고
    // x가 Operator의 instance 이면 "x is in class Operator!"를 출력한다.
}

 


Singleton object

Singleton은 class의 instance가 하나만 존재하는 object이다.
class의 instance가 하나만 존재해야하는 경우가 있다.
이 경우에는 class 대신 object를 이용한다.

class Person {...} 대신
object Person {...}


그리고 singleton object는 다음과 같은 특징을 갖는다.
1) 유일하다.
2) 생성자를 정의하지 않는다.
3) new를 이용해서 생성하지 않는다.


Companion Object

class와 이름이 같은 object이다.
static 변수와 method를 정의할 때 companion object를 이용한다.

class Pizza() {...}

Object Pizza {
    val size = 26
    def alertAllergy = println("It contains cheese")
}

standard class

constructor

primary constructor
scala는 java와 달리 class 이름 옆에 parameter를 명시함으로써 primary constructor를 정의한다.

auxilary constructor
추가적인 constructor는 this를 이용해서 아래와 같이 정의한다.

class Person(first_name:String, last_name:String){
	var age = -1
	
    def this(first_name, last_name:String, age:Int) {
    	this(first_name, last_name)this.age = age
    }
}

 

private constructor : private 생성자

class Person private(name: String ){ ... }


default value : 기본값 지정

class Person private(name: String = "hello" ){ ... }



extends를 이용할 때 어떤 생성자를 이용할지 명시할 수 있다.

class Employee(name:String) extends Person (name: String){ ... }

 

 

instance 생성(new)

new를 통해 생성한다.

parameter가 없는 생성자인 경우, ()를 생략할 수 있다.

function의 경우에도 parameter가 없는 경우 ()를 생략할 수 있다.

class AA {
  var vv = 0
  def kk = println("without ()")
  def kk2() = println("with ()")
}


object O {
  def main(args: Array[String]):Unit = {
    val a = new AA
    val b = new AA()
    
    println("a", a)
    println("b", b)
    
    //실행 결과
    //(a,AA@7112f2df)
    //(b,AA@f8ec7ed)
    
    a.kk
    b.kk() // compile error
    
    실행 결과
    //without ()
    
    a.kk2
    b.kk2()
    
    실행 결과
    //with ()
    //with ()
    
    }
}

 

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을 잘 읽자.....

+ Recent posts