본문 바로가기
행위 돌아보기

함수형 사고 - [5] 진화하라.

by simplify-len 2019. 7. 26.

"100개의 함수를 하나의 자료구조에 적용하는 것이 10개의 함수를 10개의 자료구조를 적용하는 것보다 낫다."
- 앨런 펄리스

객체지향적인 명령형 프로그래밍 언어에서 재사용의 단위는 클래스와 그것들이 주고받는 메시지이다. 이것들은 클래스도표로 표시된다.

1. 적은 수의 자료구조, 많은 연산자

- 복잡한 자료구조를 직접 만들어야 하는 객체지향에 비해, 함수형은 주요 자료구조(map, set, list)와 거기에 따른 최적화된 연산들을 선호.

- 그래서 XML을 파싱 할 때도, 함수형프로그래밍은 자료구조를 따로 만들 필요가 없다.

- 스칼라에서 크롤링하는 코드를 보면 신기할 따름

-PlayJson

)

2. 문제를 향하여 언어를 구부리기

대부분의 개발자들은 복잡한 비지니스 문제를 자바와 같은 언어로 번역하는 것이 그들의 할 이라는 차각 속에서 일을 한다. 자바가 언어로서 유연하지 못하기 때문에, 아이디어를 기존의 고정된 구조에 맞게 주물러야 하기 때문이다. 그런 개발자가 유연한 언어를 접하면 문제를 언어에 맞게 구부리는 대신 언어를 문제에 어울리게 구부릴 수 있다는 것을 깨달음.

import scala.xml._
import java.net._
import scala.io.Source

val theUrl = "https://query.yahoo... []"

val xmlString = SOurce.fromURL(new URL(theUrl)).mkString
val xml = XML.loadString(xmlString)
val city = xml \\ "location" \\ "@city"
val state = xml \\ "location" \\ "@region"
val temperature = xml \\ "condition" \\ "@temp"
  • 스칼라는 가단성? 을 위해 설계되었다. 때문에 연산자 오버로딩이나 묵시적 자료형 같은 확장이 가능하다.

3. 디스패치? -> 디스패치란 넓은 의미로 언어가 작동방식을 동적으로 선택하는 것

자바에서 조건부 실행은 특별한 경우의 switch 문을 제외하고는 if문을 사용

일반적으로 if문을 길게 쓰면 가독성이 떨어지므로, 주로 팩토리나, 추상 팩토리 패턴을 사용한다.

class LetterGrade {
  def gradeFromScore(score){
    switch(score){
      case 90..100 : return "A"
      case 80..<90 : return "B"
      case 70..<80 : return "C"
      case 60..<70 : return "D"
      case 0..<60 : return "E"      
      case ~"[ABCEDFabcdf]": return score.toUpperCase()
    }
  }
}

자바의 경우 switch에 받을 수 있는 것은 인티저만 가능한것에 반해 그루비는 동적 자료형을 받을 수 있다.

4. 연산자 오버로딩

함수형 언어의 공통적인 기능은 연산자 오버로딩이다. 이것은 +, -, * 와 같은 연산자를 새로 정의하여 새로운 자료형에 적용하고 새로운 행동을 하게 하는 기능이다. 자바가 처음 만들어질 때는 의도적으로 연산자 오버로딩이 제외되었지만, 자바의 후계 언어들을 비롯해 거의 모든 현대 언어들에는 이 기능이 들어 있다.

스칼라는 연산자와 메서드의 차이점을 없애는 방법으로 연산자 오버로딩을 허용. 즉, 연산자는 특별한 이름을 가진 메서드에 불과하다. 따라서 곱셈 연산자를 스칼렝서 오버라이드할려면 *메서드를 오버라이드하면 된다.

final class Complex(val real: Int, val imaginary: Int) extends Ordered[Complex] {

  def +(operand: Complex) =
      new Complex(real + operand.real, imaginary + operand.imaginary)

  def +(operand: Int) =
    new Complex(real + operand, imaginary)

  def -(operand: Complex) =
    new Complex(real - operand.real, imaginary - operand.imaginary)

  def -(operand: Int) =
    new Complex(real - operand, imaginary)

  def *(operand: Complex) =
      new Complex(real * operand.real - imaginary * operand.imaginary,
          real * operand.imaginary + imaginary * operand.real)

  override def toString() =
      real + (if (imaginary < 0) "" else "+") + imaginary + "i"

  override def equals(that: Any) = that match {
    case other : Complex => (real == other.real) && (imaginary == other.imaginary)
    case other : Int => (real == other) && (imaginary == 0)
    case _ => false
  }

  override def hashCode(): Int =
    41 * ((41 + real) + imaginary)

  def compare(that: Complex) : Int = {
    def myMagnitude = Math.sqrt(real ^ 2 + imaginary ^ 2)
    def thatMagnitude = Math.sqrt(that.real ^ 2 + that.imaginary ^ 2)
    (myMagnitude - thatMagnitude).round.toInt
  }
}
import org.scalatest.FunSuite

class ComplexTest extends FunSuite {

  def fixture =
    new {
      val a = new Complex(1, 2)
      val b = new Complex(30, 40)
    }

  test("plus") {
    val f = fixture
    val z = f.a + f.b
    assert(1 + 30 == z.real)
  }

  test("comparison") {
    val f = fixture
    assert(f.a < f.b)
    assert(new Complex(1, 2) >= new Complex(3, 4))
    assert(new Complex(1, 1) < new Complex(2,2))
    assert(new Complex(-10, -10) > new Complex(1, 1))
    assert(new Complex(1, 2) >= new Complex(1, 2))
    assert(new Complex(1, 2) <= new Complex(1, 2))
  }

}

5. 함수형 자료구조

자바는 예외 생성 및 전파 기능을 사용하여 전통적인 방법으로 오류를 처리한다 그러나 대부분의 함수형 언어들은 예외 패러다임을 지원하지 않기 때문에 개발자는 다른 방법으로 오류 조건을 표현한다.

예외는 많은 함수형 언어가 준수하는 전제 몇 가지를 깨뜨린다. 첫째, 함수형 언어는 부수효과가 없는 순수함수를 선호.그러나 예외를 발생시키는 것은 예외적인 프로그램 흐름을 야기하는 부수효과. 함수형 언어들은 주로 값을 처리하기 때문에 프로그램의 흐름을 막기보다는 오류를 나타내는 리턴 값에 반응하는 것을 선호한다.

함수형 프로그램이 선호하는 또 하나의 특성은 참조 투명성이다. 호출하는 입장에서는 단순한 값 하나를 사용하든, 하나의 값을 리턴하는 함수를 사용하든 다를 바가 없어야 한다. 만약 호출된 함수에서 예외가 발생할 수 있다면, 호출하는 입장에서는 안전하게 값을 함수로 대체할 수 없을 것이다.

함수형 오류 처리?

자바에서 예외를 사용하지 않고 오류를 처리하기 위해 해결해야 할 근본적인 문제는 메서드가 하나의 값만 리턴할 수 있다는 제약. 물론 메서드는 여러 개의 값을 지닌 하나의 object나 그 서브클래스의 참조를 리턴할 수 있다. 딸서 Map을 사용하여 다수의 리턴 값을 지원하게 할 수 있다.

public static Map<String, Object> divide(int xm int y){
  Map<String, Object> result = new HashMap<String, Object>();
  if(y == 0)
      result.put("exception", new Exception("div by zero"));
  else
      result.put("answer", (double) x / y);
  return result
}

위 코드의 단점은 타입이 세이프 하지 않다는 것.그래서 컴파일러가 오류를 잡아내기 힘들다는 것.

메서드 호출자는 리턴 값을 가능한 결과들과 비교해보기 전에는 성패를 알 수 없다.

두 가지 결과가 모두 리턴 Map에 존재할 수가 있으므로, 그 경우에는 결과가 모호해진다.

그래서 스칼라에서는 Either 클래스

왼쪽 또는 오른쪽 값 중 하나만 가질 수 있게 설계되었다. 이런 자료구조를 분리합집합(disjoin union)이라고 한다. C에서 파생된 어떤 언어들은 여러 자료형의 인스턴스를 지닐 수 있는 union자료형을 제공한다. 분리합집합은 두 자료형이 들어갈 자리가 있지만, 둘 중 하나만 가질 수 있다.

type Error = String
type Success = String
def call(url:String):Either[Error, Success]={
  val response = WS.url(url).get.value.get
  if(valid(response))
      Right(response.body)
  else Left("Invalid response")
}

Either는 오류 처리에서 주로 사용된다. Either는 스칼라의 전체 생태계에 자연스럽게 녹여든다. 자주 사용하는 경우 중의 하나는 패턴매칭과 함께 자주 사용된다.

getContent(new URL("https://...")) match {
  case Left(msg) => println(msg)
  case Right(source) => source.getLines.foreach(println)
}

그리고 Option Class

Either는 두 부분을 가진 값을 간편하게 리턴할 수 있는 개념이다 .이것은 이 장의 뒤에서 볼 수 있듯이, 트리 모양 자료구조와 같은 일반적인 자료구조를 만드는데 유용. 여러 언어나 프레임워크에는 함수에서 리턴할 때 사용할 수 있는 Either와 유사한 Option이란 클래스가 있다.

이것은 적당한 값이 존재하지 않을 경우를 의미하는 none, 성공적인 리턴을 의미하는 some을 사용하여 예외 조건을 더 쉽게 표현한다. 함수형 자바의 Option클래스는 다음과 같다.

public static Option<Double> divide(double x, double y){
  if(y == 0)
    return Option.none();
  return Option.some(x/y);
}

댓글