- 
                Notifications
    You must be signed in to change notification settings 
- Fork 8
2016 08 16 cbk
        baekkyu cho edited this page Aug 23, 2016 
        ·
        14 revisions
      
    <주요내용>
* 오류를 함수적으로 제기하고 처리하는 데 필요한 기본원리들을 배운다.
* 실패상황과 예외를 보통의 값으로 표현할 수 있으며, 일반적인 오류 처리-복구 패턴을 추상화한 고차함수를 작성한다.
* 스칼라 표준라이브러리의 두형식 Option, Either를 직접 만들어본다.
- 예외는 참조투명성(referential transparency) 을 해치고, 문제가 된다.
def failingFn(i: Int): Int = {
  val y: Int = throw new Exception("fail!")   // Exception 발생
  try {
    val x = 42 + 5
    x + y    
  }
  catch { case e: Exception => 43 }    //패턴매칭
}
// --- 예상대로 예외가 발생함
scala> failingFn(5)
java.lang.Exception: fail!
  at .failingFn(<console>:11)
  ... 33 elided- y가 참조에 투명하지 않다.
- x + y부분을 아래 예제처럼 치환하면 다른 결과가 나온다.
def failingFn(i: Int): Int = {
  try {
    val x = 42 + 5
    x + ((throw new Exception("fail!")): Int)
  }
  catch { case e: Exception => 43 }    //패턴매칭
}
// --- catch 문으로 인하여, 정상적으로 값을 반환함
scala> failingFn(5)
res3: Int = 43참조에 투명한 표현식의 의미
- 문맥(context)에 의존하지 않으며, 지역적으로 추론할 수 있다.
- 예) 42 + 5의 의미는 더 큰 표현식에 의존하지 않는다.
참조에 투명하지 않은 표현식의 의미
- 문맥에 의존적이고, 좀 더 전역의 추론이 필요하다.
- 예) throw new Exception("fail") 이라는 표현식의 의미는 문맥에 크게 의존한다.
예외의 주된문제 두가지
- 예외는 참조투명성을 위반하고, 문맥 의존성을 도입한다.
- 치환모형의 간단한 추론이 불가능해지고, 예외에 기초한 혼란스러운 코드가 만들어진다.
- 예외를 오류처리에만 사용하고, 흐름의 제어에는 사용하지 말아야 한다는 속설의 근원이다.
- 예외는 형식에 안전하지 않다.
- failingFn 함수의 형식인 Int => Int만 보고는 이 함수가 예외를 던질 수 있다는 사실을 알 수 없다.
점검된 예외
- java의 점검된 예외(checked exception)는 적어도 오류를 처리할 것인지 다시 발생시킬 것인지의 결정을 강제하나, 결과적으로 호출하는 쪽에 판에박힌 코드가 추가된다. 더욱 중요한 점은, 점검된 예외는 고차 함수에는 통하지 않는다는 점이다.
- 고차함수에서는 인수가 구체적을 어떤 예외를 던질지 미리 알 수 없기 때문이다.
def map[A, B](l: List[A])(f: A => B): List[B]  //목록의 평균(mean)을 계산하는 함수.
  def mean(xs: Seq[Double]): Double =  //Seq는 컬렉션의 공통인터페이스
    if (xs.isEmpty)
      throw new ArithmeticException("mean of empty list")
    else xs.sum / xs.length- 위 예제는 부분함수(partial function)의 예제이다. (일부 입력에 대해서는 정의되지 않는 함수)
- 빈 목록에 대해서 Double.NaN 리턴
- 상황에 따라 null을 리턴
- 특정한 경계 값(sentinel value)을 돌려준다. (-99999)
문제점
- 오류가 소리없이 전파될 수 있다. 호출자가 이런 오류조건의 점검을 실수로 빼먹어도 컴파일러가 경고해주지 않는다.
- 호출하는 쪽에 호출자가 '진짜' 결과를 받았는지 점검하는 명시적 if구문들이 늘어난다.
- 다형적 코드에는 적용할 수 없다. (return 형식에 따라서는 그 형식의 경계값을 결정하는 것이 불가능할 수도 있다.)
- 호출자에게 특별한 방침이나 호출규약을 요구한다. (mean함수를 제대로 사용하려면 호출자가 그냥 mean을 호출해서 그 결과를 사용하는 것 이상의 작업을 수행해야 한다.)
  def mean(xs: Seq[Double], onEmpty: Double): Double =
    if (xs.isEmpty) onEmpty
    else xs.sum / xs.length- mean은 부분함수가 아닌 완전함수(total function)가 된다.
문제점
- 정의되지 않은 경우의 처리바익을 함수의 호출자가 알고 있어야 한다.
- 더 큰 계산과정에서 mean을 호출할 때, mean이 정의되지 않으면 그 계산 자체를 취소하는 등의 유연함을 얻을 수 없다.
해법
- 함수가 항상 답을 내지는 못한다는 점을 반환형식을 통해서 명시적으로 표현한다.(오류 처리 전략을 호출자에게 미룬다)
//Option은 스칼라 표준라이브러리에 존재하나, 학습을 위해 직접 만들어 보았다.
sealed trait Option[+A] //공변성
case class Some[+A](get: A) extends Option[A]
case object None extends Option[Nothing]- Option을 정의할 수 있는 경우에는 Some, 정의할 수 없는 경우에는 None이 된다.
//option을 이용해 재구현
def mean(xs: Seq[Double]): Option[Double] =
  if (xs.isEmpty) None
  else Some(xs.sum / xs.length)- 함수의 결과가 항상 정의되지는 않는다는 사실이 return형식에 반영되어 있음.
- 입력형식의 모든 값에 대해 정확히 하나의 출력형식값을 돌려주므로, 완전함수이다.
- Map에서 주어진 키를 찾는 함수는 Option을 돌려준다.
- 목록과 기타 반복 가능 자료형식에 정의된 headOption과 lastOption은 순차열이 비어있지 않은 경우 첫 요소 또는 마지막 요소를 담은 Option을 돌려준다.
Option이 편리한 이유는, 오류 처리의 공통 패턴을 고차함수들을 이용해서 추출함으로써 예외 처리 코드에 흔히 수반되는 판에 박힌 코드를 작성하지 않아도 된다는 점이다.
- Option은 최대 하나의 원소를 담을 수 있다는 점을 제외하면 List와 비슷하다.
trait Option[+A] {
  def map[B](f: A => B): Option[B]    //Option이 None이 아니면 f를 적용한다.
  def flatMap[B](f: A => Option[B]): Option[B]    //Option이 None이 아니면 f(실패할 수 있음)를 적용한다.
  def getOrElse[B>:A](default: => B): B    // default: => B는 해당인수의 형식이 B이지만 해당인수가 함수에서 실제로 쓰일 때까지는 평가되지 않음을 뜻한다. / 비엄격성(non-strictness)개념은 다음장에서..
  def orElse[B>:A](ob: => Option[B]): Option[B]    // B>:A는 B형식 매개변수가 반드시 A의 상위형식(supertype)이어야 함을 의미한다.
  def filter(f: A => Boolean): Option[A]    // 값이 f를 만족하지 않으면 Some을 None으로 변환한다.
}case class Employee(name: String, department: String)
def lookupByName(name: String): Option[Employee]
//Option안의 결과(가 있다면)를 변환하는데 사용할 수 있다.
val joeDepartment: Option[String] = lookupByName("Joe").map(_.department)
val department: String = 
  lookupByName("Joe").
  map(_.department).
  filter(_ != "Accounting").
  getOrElse("default")- 어떤 호출자가 복구가능한 오류로 처리할 수 있을만한 상황이라면 예외대신 Option을 돌려주어서 호출자에게 유연성을 부여한다.
- 계산의 매 단계마다 None을 점검할 필요가 없다 -> 그냥 일련의 변환을 수행하고, 나중에 원하는 장소에서 None을 점검하고 처리하면 된다.
- Option[A]는 A와는 다른 형식이므로, None일 수 있는 상황의 처리를 명시적으로 수행하지 않으면 컴파일러가 오류를 낸다.
- Option을 사용하기 시작하면 코드기반 전체에 Option이 번지게 될 것이라는 생각을 할 수도 있다.
- 하지만 그런 부담을 질 필요는 없다. 실제로는 보통의 함수를 Option에 작용하는 함수로 승급시킬(lift) 수 있다.
//승급함수
def lift[A, B](f: A => B): Option[A] => Option[B] = _ map f
//절대값 함수에 대한 승급처리
val abs0: Option[Double] => Option[Double] = lift(math.abs)보험료율 견적 관련 예제
- 온라인에서 나이와 속도위반딱지 발급수를 입력받아 보험료율을 계산해야 한다.
- 웹기반으로 입력된 값은 기본적으로 문자열로 되어있으므로, 문자열을 파싱해야한다.
- 문자 파싱에 관련된 부분은 예외가 발생할 수도 있다라는 가정으로 Option으로 감싸도록 프로그래밍 한다.
//문자열 파싱 - 잘못된 값이 입력되면 에러가 발생한다.
scala> "112".toInt
res0: Int = 112
//Try함수를 이용
def parseInsuranceRateQuote(
    age: String, 
    numberOfSppedingTickets: String): Option[Double] = {
  val optAge: Option[Int] = Try(age.toInt) 
  val optTickets: Option[Int] = Try(numberOfSppedingTickets.toInt)
  insuranceRateQuote(optAge, optTickets)  //보험료율계산
}
//parameter a:는 엄격하지 않은 방식(= lazy) (자세한건 다음장에서 설명됨) 
//Try(age.toInt)에서 age.toInt구문이 laza로 실행됨.
def Try[A](a: => A): Option[A] = 
  try Some(a)
  catch { case e: Exception => None }
// 두 가지 핵심요인으로 연간자동차 보혐료를 계산하는 비밀공식.
def insuranceRateQuote(age: Int, numberOfSpeedingTickets: Int): Double- 위 함수에는 문제가 있다. 보험료율을 계산하는 함수는 parameter로 Option[Int]가 아닌 Int를 받는다.
- 이와같은 문제는 아래 예제처럼 해결이 가능하다.
//기존에 나왔던 map과 flatMap signature
def map[B](f: A => B): Option[B]
def flatMap[B](f: A => Option[B]): Option[B]
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)
    }
  }
//보험료율 계산하는 부분을 아래처럼 수정한다.
insuranceRateQuote(optAge, optTickets)  //보험료율계산 (Int, Int) 에러
map2(optAge, optTickets)(insuranceRateQuote)  //map2함수를 이용하여 Option에 대응하게 한다.- 위와 같은 예제는 parameter가 두개인 어떤 함수라도 아무 수정 없이 Option에 대응하게만들 수 있음을 의미한다.
- map3, map4와 같은 형태로도 만들 수 있다.
for-함축 관련
- 스칼라에서는 승급함수들이 흔히 쓰이기 때문에, for-함축(for-comprehension)이라고 하는 특별한 구문 구조를 제공한다.
- 해당 구문은 자동으로 flatMap, map호출들로 전개된다.
// map2 함수
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)
    }
  }
// for함축으로 재구현
def map2[A,B,C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] =
  for {
    aa <- a
    bb <- b
  } yield f(aa, bb)- yield키워드 다음 표현식에서는 임의의 <- 묶음 좌변에 있는 값을 사용할 수 있다.
- 컴파일러단에서 이러한 묶음들을 flatMap 호출로 전개하고, 마지막 묶음과 yield는 map호출로 변환해준다.
- Option은 예외적인 조건이 발생했을 때 무엇이 잘못되었는지에 대한 정보는 제공하지 못한다는 단점이 있다.
- 실패에 관해 알고 싶은 정보가 있을 경우에는 Either자료형식을 이용한다.
// Either자료형식에 대한 signature
sealed trait Either[+E, +A]
case class Left[+E](value: E) extends Either[E, Nothing]
case class Right[+A](value: A) extends Either[Nothing, A]- 
Option과의 본질적인 차이는 두 경우 모두 값을 가진다는 것이다. 
- 
Right은 성공을 나타낸다 (옳은(right)쪽이라는 말장난에서 비롯) 
- 
Left는 실패를 나타낸다 
- 
Either로 풀어본 mean 예제 
//실패하게되면 string값을 돌려준다.
def mean(xs: IndexedSeq[Double]): Either[String, Double] = 
  if (xs.isEmpty) 
    Left("mean of empty list!")
  else 
    Right(xs.sum / xs.length)
//필요하다면 예외를 돌려줘도 된다.
def safeDiv(x: Int, y: Int): Either[Exception, Int] = 
  try Right(x / y)
  catch { case e: Exception => Left(e) }- 던져진 예외를 값으로 변환한다는 공통의 패턴을 추출한 함수 Try작성
def Try[A](a: => A): Either[Exception, A] = 
  try Right(a)
  catch { case e: Exception => Left(e) }- Right값에 대해 작용하는 map, flatMap구현
def map[B](f: A => B): Either[E, B] = 
 this match {
   case Right(a) => Right(f(a))
   case Left(e) => Left(e)
 }
// 우변에 대한 사상에서, +E 공변 주해를 만족하기 위해서는 반드시 왼쪽 매개변수를 적절한 상위형식으로 승격시킬(promote)필요가 있다.
// 하위바운드(lower type bound), EE는 E의 supertype
def flatMap[EE >: E, B](f: A => Either[EE, B]): Either[EE, B] =
 this match {
   case Left(e) => Left(e)
   case Right(a) => f(a)
 }- 위와 같은 정의들이 있으면 Either를 for-함축에 사용할 수 있다.
def parseInsuranceRateQuote(
    age: String,
    numberOfSpeedingTickets: String): Either[Exception, Double] = 
  for {
    a <- Try { age.toInt }
    tickets <- Try { numberOfSpeedingTickets.toInt }
  } yield insuranceRateQuote(a, tickets)
//- map2 함수를 구현해보고, 유효성값을 검증하는 예제에 활용해본다.
//for-함축을 이용하여 map2함수를 구현 (Either class 내부에 선언됨)
def map2[EE >: E, B, C](b: Either[EE, B])(f: (A, B) => C): Either[EE, C] = 
  for { a <- this; b1 <- b } yield f(a,b1)
// 참고
this flatMap { a =>
  b map { b1 =>
    f(a, b1)
  }
}
// 유효값을 검증하는 예제
case class Person(name: Name, age: Age)
sealed class Name(val value: String)
sealed class Age(val value: Int)
def mkName(name: String): Either[String, Name] = 
  if (name == "" || name == null) Left("name is empty")
  else Right(new Name(name))
def mkAge(age: Int): Either[String, Age] = 
  if (age < 1) Left("age is out of range.")
  else Right(new Age(age))
def mkPerson(name: String, age: Int): Either[String, Person] = 
  mkName(name).map2(mkAge(age))(Person(_, _))
//실행예제.
scala> mkPerson("", 34)
Left(name is empty)
scala> mkPerson("cho", 0)
Left(age is out of range.)