관리 메뉴

HAMA 블로그

스칼라 강의 (46) 리프팅(lifting) 이란? 본문

Scala

스칼라 강의 (46) 리프팅(lifting) 이란?

[하마] 이승현 (wowlsh93@gmail.com) 2017. 11. 9. 17:05


 [경고] 개인적으로 공부한것 정리한 것이라 내용이 두서없이 조악하게 연결되어 있습니다.


스칼라에서 리프팅(lifting) 이란? [번역/요약] 


먼저 스칼라에서 에러처리하는 방식으로 option 을 사용하는데, 사용하다보면 이것을 리턴 받아서 사용 할 때 일일이 확인해서 처리 해주다보면 군더더기 코드가 덕지 덕지 되지 않을까 생각 할 수 있을 것이다. 초보시절에는 if 문이나 case 매칭으로 지져분하게 해결 한 경험도 많을 것인데, 여기서 설명할 리프팅을 알고 나면 그렇게 코딩 했어야 했었던 초보시절의 죄책감을 앞으로는 벗어 던질 수 있을 것이다.


아래 예 (pseudo-code)를 보면

x = someOperation
if !x.nil?
y = someOtherOperation
    if !y.nil?
    doSomething(x,y) return "it worked"
    end
end
return "it failed"

함수를 실행해서 리턴 받은 값이 정상적인지 조사해서, 그 값에 문제가 없을 경우 다음 프로세스를 진행하는 if 로 범벅된 전형적인 코드이다.

아시다시피 자바에서 null은 객체가 아니며 메소드도 가지고 있지 않은데, 정적으로 형식화 된 규칙의 예외라고 볼수 있다. (null은 클래스가 아니지만 클래스에 대한 참조를 null로 설정할 수 있다). null에서 메소드를 호출하면 하나의 결과인 예외가 throw 될 뿐이다. 실제로 null을 발명 한 사람은 10 억 달러의 실수라고 말했다.

Ruby는 null보다 조금 더 나은 nil을가지고 있는데 nil은 실제 싱글톤 객체이다. 즉 전체 시스템에 단 하나의 인스턴스 만 존재하며 메소드도 가지고 있고  Object의 서브 클래스이기도 하다. Object에는 "nil"이라는 메서드가 있으며 이 메서드는 false를 반환하지만 nil 싱글톤은 이 메서드를 재정의하여 true를 반환한다. nil은 Java에서 null과 매우 비슷하게 반환되며,  그것은 "유효한 대답 없음" 이라는 의미이다.

우리의 스칼라는 조금 다른 철학을 가지고 있다.

Option이라고하는 강력한 타입의 추상 클래스가 있으며. Option [T]로 선언 된다. 즉, Option은 모든 타입이 될 수 있지만 일단 타입이 정의되면 변경되지 않는다. Option에는 Some 하위 클래스와 None 하위 클래스가 있으며,  None 은 싱글톤 (nil과 같다) 이고 Some 은 일종의 컨테이너이다. 따라서 다음과 같은 메소드가 있을 수 있다.

def findUser(name: String): Option[User] = {
val query = buildQuery(name)
val resultSet = performQuery(query)
val retVal = if (resultSet.next) Some(createUser(resultSet)) else None
resultSet.close
retVal
}

Some(User) 또는 None을 반환하는 findUser 메서드가 있다. 지금까지는 위의 예제와 많이 다르지 않습니다. 그래서 모든 사람들을 혼란스럽게하기 위해 컬렉션에 대해 잠깐 이야기 할 것입니다.

스칼라에서 정말 좋은 점은 (예, Ruby도 마찬가지) 풍부한 리스트 오퍼레이션들인데, 카운터를 생성하고 리스트(배열) 요소를 하나씩 꺼내는 대신에 작은 함수를 작성하고 해당 함수를 목록에 전달한다. 리스트는 각 요소가 있는 함수를 호출하고 각 호출에서 반환 된 값으로 새 리스트를 반환한다. 아래 코드를 참고하자.

scala> List(1,2,3).map(x => x * 2)
line0: scala.List[scala.Int] = List(2,4,6)

위의 코드는 각 목록 항목에 2를 곱하고 "map"은 결과 목록을 반환한다. 좀 더 간결하게 다음처럼도 가능하다.

scala> List(1,2,3).map(_ * 2)
line2: scala.List[scala.Int] = List(2,4,6)

map 작업을 중첩 할 수 있도 있다.

scala> List(1,2,3).map(x => List(4,5,6).map(y => x * y))
line13: scala.List[scala.List[scala.Int]] = List(List(4,5,6),List(8,10,12),List(12,15,18))

그리고 내부 목록을 "평평하게"할 수도 있다. (flatMap 이용)

scala> List(1,2,3).flatMap(x => List(4,5,6).map(y => x * y))
line14: scala.List[scala.Int] = List(4,5,6,8,10,12,12,15,18)

마지막으로 첫 번째 목록에서 짝수를 "필터링"할 수 있다.

scala> List(1,2,3).filter(_ % 2 == 0). flatMap(x => List(4,5,6).map(y => x * y))
line16: scala.List[scala.Int] = List(8,10,12)

그러나 보시다시피 map / flatMap / filter 항목은 조금 복잡하다고 할 수 있는데, 스칼라는 코드를 보다 쉽게 읽을 수 있도록 "for" comprehension 을 도입했다.  (역주: flatMap 과 for-comprehension 의 유사성에 주목)

scala> for {
x <- List(1,2,3) if x % 2 == 0
y <- List(4,5,6)} yield x * y
res0: List[Int] = List(8, 10, 12)

좋다. 그런데 이것이 Option [T]와 어떤 관련이 있나? 


Option은 map, flatMap 및 filter (스칼라 컴파일러가 for-comprehension 에 사용하는 데 필요한 메서드)를 구현한다. 따라서 이 구조를 매우 효과적으로 사용할 수 있다는 것이다. 첫 번째 예는 간단하다.

scala> for {x <- Some(3); y <- Some(4)} yield x * y
res1: Option[Int] = Some(12)

3과 4 곱하는 엄청난 코드입니다만 여기에 None 이 있으면 어떻게 되는지 봅시다.

scala> val yOpt: Option[Int] = None
yOpt: Option[Int] = None
scala> for {x <- Some(3); y <- yOpt} yield x * y
res3: Option[Int] = None

None 을 되돌려 받았다.

scala> (for {x <- Some(3); y <- yOpt} yield x * y) getOrElse -1
res4: Int = -1
scala> (for {x <- Some(3); y <- Some(4)} yield x * y) getOrElse -1
res5: Int = 12

"getOrElse"코드를 사용하여 None 일 경우 디폴트 값을 돌려 받도록 하였다.


아래와 같은 코드를 이용하여, A,B 타입을 받아서 C 타입을 반환하는 함수를 Option[A] 와 Option[B] 를 받아서 Option[C] 를 받는 함수로 승급(lifting) 시킬 수 있게 된다. 

def lift[A,B] (f: A => B): Option[A] => Option[B] = _ map f 

아래는 math.abs 함수를 승급 시키는 것을 보여준다.

val abs0: Option[Double] => Option[Double] = lift(math.abs) 

두 Option 값을 이항 함수 (binary function) 를 이용해서 결합하는 일반적 함수 map2 이다. 두 Option 값 중 하나라도 None 이면 map2 의 결과 역시 None 이다. 


def map2[A,B,C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] =
a flatMap (aa => b map (bb => f(aa, bb)))

Option 들의 목록을 받고, 그 목록에 있는 모든 Some 값으로 구성된 목록을 담은 Option 을 돌려주는 함수는 아래와 같다.  마찬가지로 목록에 None 이 하나라도 있으면 None 이 된다. 

/*
Here's an explicit recursive version:
*/
def sequence[A](a: List[Option[A]]): Option[List[A]] =
a match {
case Nil => Some(Nil)
case h :: t => h flatMap (hh => sequence(t) map (hh :: _))
}
/*
It can also be implemented using `foldRight` and `map2`. The type annotation on `foldRight` is needed here; otherwise Scala wrongly infers the result type of the fold as `Some[Nil.type]` and reports a type error (try it!). This is an unfortunate consequence of Scala using subtyping to encode algebraic data types.
*/
def sequence_1[A](a: List[Option[A]]): Option[List[A]] =
a.foldRight[Option[List[A]]](Some(Nil))((x,y) => map2(x,y)(_ :: _))





아래 글은 Scala에서 리프팅이 무엇이냐라는 물음에 대한 스택오버플로우 대답니다.


Scala 에서 리프팅은 다양한 의미를 갖습니다.

PartialFunction

PartialFunction[A, B] 가 도메인 A 의 서브셋으로 정의된 함수임을 기억하시구요.  "lift" 를 통해서 PartialFunction[A, B] 를  Function[A, Option[B]]로  리프팅 할 수 있을 것입니다. 이것은 PartialFunction 에 있는 lift 메소드를 통해서 명시적으로 완수 될 것입니다. 예를 보시죠. 

scala> val pf: PartialFunction[Int, Boolean] = { case i if i > 0 => i % 2 == 0}
pf: PartialFunction[Int,Boolean] = <function1>

scala> pf.lift
res1: Int => Option[Boolean] = <function1>

scala> res1(-1)
res2: Option[Boolean] = None

scala> res1(1)
res3: Option[Boolean] = Some(false)

PartialFunction 내의 lift 메소드를 통해서 Boolean 타입이 option[Boolean] 으로 바뀌었습니다. 


Methods

메소드를 함수로 "lift" 할 수 있습니다. 이것을 eta-expansion 이라고 하는데요. 예를 보시죠.

scala> def times2(i: Int) = i * 2
times2: (i: Int)Int

underscore 를 applying 하여 메소드를 함수로 리프팅 하였습니다.

scala> val f = times2 _
f: Int => Int = <function1>

scala> f(4)
res0: Int = 8


Functors

펑터(scalaz에 의해 정의된 의미로)는 어떤 "컨테이너" 로 볼수 있는데, F such that, if we have an F[A] and a function A => B, then we can get our hands on an F[B] (think, for example, F = List and the map method)

이 속성은 다음과 같이 인코딩 할 수 있다.

trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}

이것은 A => B 함수를 "functor"의 도메인으로 "lift" 하는 것과 동형입니다. 

def lift[F[_]: Functor, A, B](f: A => B): F[A] => F[B]

즉, F가 functor이고 우리가 A => B라는 함수를 가지고 있다면, 함수 F [A] => F [B]가 됩니다. 리프트 방법을 시도하고 구현할 수도 있습니다. 


Monad Transformers

As hcoopz says below (and I've just realized that this would have saved me from writing a ton of unnecessary code), the term "lift" also has a meaning within Monad Transformers. Recall that a monad transformers are a way of "stacking" monads on top of each other (monads do not compose).

So for example, suppose you have a function which returns an IO[Stream[A]]. This can be converted to the monad transformer StreamT[IO, A]. Now you may wish to "lift" some other value an IO[B] perhaps to that it is also a StreamT. You could either write this:


StreamT.fromStream(iob map (b => Stream(b)))

Or this:

iob.liftM[StreamT]

this begs the question: why do I want to convert an IO[B] into a StreamT[IO, B]?. The answer would be "to take advantage of composition possibilities". Let's say you have a function f: (A, B) => C


lazy val f: (A, B) => C = ???
val cs =
for {
a <- as //as is a StreamT[IO, A]
b <- bs.liftM[StreamT] //bs was just an IO[B]
}
yield f(a, b)

cs.toStream //is a Stream[IO[C]], cs was a StreamT[IO, C]



번역 레퍼런스:

fpinScala (function programming in scala) 의 4장

https://simply.liftweb.net/index-7.2.html

https://stackoverflow.com/questions/17965059/what-is-lifting-in-scala

Comments