일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 파이썬 동시성
- 파이썬 데이터분석
- 파이썬
- play2 강좌
- akka 강좌
- 파이썬 강좌
- Akka
- 하이퍼레저 패브릭
- 블록체인
- Play2
- Golang
- Adapter 패턴
- 이더리움
- 플레이프레임워크
- 스칼라
- 하이브리드앱
- CORDA
- Hyperledger fabric gossip protocol
- 파이썬 머신러닝
- 스칼라 동시성
- 스위프트
- play 강좌
- 스칼라 강좌
- 엔터프라이즈 블록체인
- Play2 로 웹 개발
- 그라파나
- Actor
- 주키퍼
- 안드로이드 웹뷰
- hyperledger fabric
- Today
- Total
HAMA 블로그
스칼라 강의 (46) 리프팅(lifting) 이란? 본문
[경고] 개인적으로 공부한것 정리한 것이라 내용이 두서없이 조악하게 연결되어 있습니다.
스칼라에서 리프팅(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
'Scala' 카테고리의 다른 글
스칼라 강의 (47) ADT (Algebraic Data Types) 이란? (feat. 타입클래스) (0) | 2017.11.15 |
---|---|
스칼라 vs 코틀린 : 더 나아진 자바를 목표로 경쟁하는 2개의 언어. [번역] (0) | 2017.11.10 |
스칼라 강의 (45) 빌드(build) & 디플로이 (deploy) 하기 [번역] (0) | 2017.11.06 |
스칼라 강의 (44) - 리플렉션 (0) | 2017.11.06 |
스칼라 강의 (43) - 고급 타입관련 주제들 (0) | 2017.03.26 |