주요글: 도커 시작하기

마틴 오더스키 교수님이 코세라에서 진행중인 Functional Programming Principles in Scala 강의(https://www.coursera.org/learn/progfun1)의 4주차 요약.


* 함수형을 잘 모르는 상태에서 요약한 것이므로 내용에 오류가 존재할 수 있음


순수 객체 지향


순수 객체 지향 언어에서 모든 값은 객체이다. 언어가 클래스를 기반으로 한다면 모든 값의 타입이 클래스라는 것을 뜻한다. 스칼라 역시 기본 타입과 함수와 같은 모든 것을 객체로 표현한다.


자바에서 기본 타입인 int, boolean을 스칼라에서는 특별한 타입이 아닌 클래스로 제공한다. 이들 클래스는 scala 패키지에 Int, Boolean으로 정의되어 있다. 정수값 1은 Int 타입이다.


Int를 포함 이들 타입은 자바에서 +, *, ==와 같은 연산자와 동일한 이름을 갖는 메서드를 제공한다. 예를 들어, 1 + 10은 실제로는 Int 타입의 + 메서드를 호출하고 첫 번째 인자로 10을 전달하는 1.+(10) 코드와 동일하다.


스칼라 컴파일러는 성능을 높이기 위해 Int 타입 값은 32비트 정수로 Boolean 타입은 자바의 boolean으로 표현한다.


함수도 객체로


함수 값도 객체로 처리한다. A=>B 타입의 함수는 Function1[A, B] 클래스를 축약한 것이다.  Function2, Function3 등 파라미터 개수별로 그에 해당하는 함수 타입이 존재한다.


함수는 apply() 메서드를 가진 객체로서 f(x)는 f.apply(x)와 같다. 예를 들어, 


var f = (x: Int) => x + x

f(7)


이 코드는 다음의 OO 코드로 해석할 수 있다. 


val f = new Function1[Int, Int] {

    def apply(x: Int) = x + x

}

f.apply(7)


제네릭과 타입 경계


다형은 하위 타입과 제네릭(generic)의 두 가지 형식이 존재하는데, 여기서는 이 두 가지와 관련된 주제인 타입 경계(type bound)와  가변성(variance)에 대해 설명한다.  


IntSet이 다음과 같이 두 개의 하위 타입을 갖는다고 하자.


class NonEmpty extends IntSet { ... }

object Empty extends IntSet { ... }


정수 집합을 의미하는 IntSet의 모든 정수가 양수면 자신을 리턴하고 아니면 익셉션을 발생하는 assertAllPos() 메서드가 있다고 하자. 이 메서드는 NonEmpty와 Empty의 두 타입을 인자로 받을 수 있다. 이 두 타입은 IntSet의 하위 타입이므로 assertAllPos에 전달할 수 있는 타입은 IntSet 타입의 하위 타입이 된다. 이를 제네릭을 사용하면 다음과 같이 메서드를 정의할 수 있다.


def assertAllPos[S <: IntSet](s: S): IntSet


여기서 "<: IntSet"은 타입 파라미터 S의 상위 경계로서, 이는 IntSet에 맞는 타입만 S에 올 수 있다는 것을 의미한다. 상위 경계와 하위 경계는 각각 다음과 같이 지정한다.

  • S <: T 는 S가 T의 하위 타입임을 의미한다
  • S >: T 는 S가 T의 상위 타입임을 의미한다
[S >: NonEmpty]는 S에는 NonEmpty의 상위 타입만 올 수 있다는 것을 의미하므로 S에는 NonEmpty, IntSet, AnyRef, Any 중 하나가 올 수 있다.(AnyRef, Any는 스칼라의 타입 계층에 출현)


[S >: NonEmpty <: IntSet]과 같이 상위 경계와 하위 경계를 함께 제한할 수 있다.


가변성(variance)


제네릭과 관련해서 처음에 잘 이해하기 힘든 게 가변성인데 이 강의에서도 제네릭과 가변성에 대해 다룬다.(여기서 <: 표시는 하위 타입과 상위 타입을 의미한다.)


A <: B일 때 제네릭 타입(즉, 파라미터화 타입) C[T]에 대해 다음의 세 가지 관계가 가능하다.

  • C[A] <: C[B] : C는 공변(convariant)이다
  • C[A] >: C[B] : C는 반공변(contravariant)이다
  • 서로 하위 타입이 아님 : C는 무공변(nonvariant)이다

얼핏 생각하면 A <: B일 때 C[A] <: C[B]일 것 같지만 꼭 그렇지는 않다. 예를 들어, 자바에서 배열은 공변이다. 즉, NonEmpty <: IntSet이면 NonEmpty[] <: IntSet[] 이다. 따라서 다음과 같이 할당이 가능하다.


NonEmpty[] a = new NonEmpty[] { ... }

IntSet[] b = a // NonEmpty[]는 IntSet[]의 하위 타입


그런데 공변 배열은 문제를 일으킨다. 다음 코드를 보자. b는 애초에 NonEmpty 타입의 배열이다. 그런데 a를 할당한 b는 IntSet[] 배열이다. 따라서 b 배열에는 Empty 타입의 값을 할당할 수 있다.


NonEmpty[] a = new NonEmpty[] { ... }

IntSet[] b = a // NonEmpty[]는 IntSet[]의 하위 타입

b[0] = Empty // NonEmpty 배열에 Empty 할당 시도

NonEmpty s = a[0]


Empty와 NonEmpty는 서로 상위-하위 타입 관계가 아니므로 NonEmpty 타입을 요구하는 곳에 Empty 값을 할당할 수 없고, 위 코드는 익셉션을 발생한다.


[박스]

하위 타입인지 여부는 다음 LSP(Liskov Substitution Principle)로 확인해 볼 수 있다.

A <: B이면 B 타입의 값을 사용하는 모든 것은 A 타입 값을 사용할 수 있어야 한다.


스칼라에서 공변, 반공변, 불공변은 다음과 같이 정의한다.

  • 공변: class C[+A] { ... }
  • 반공변: class C[-A] { ... }
  • 무공변: class C[A] { ... }

구성요소를 변경할 수 있는 타입은 (거의) 공변일 수 없다. 반대로 불변 타입은 특종 조건을 충족하면 공변일 수 있다. 보통 메서드에서 공변 타입 파라미터는 메서드 결과에만 위치할 수 있고, 반공변 타입 파라미터는 메서드 파라미터에만 올 수 있다.


데이터 구조에서 값을 가져오면 공변, 데이터 구조에 값을 넣으면 반공변 타입을 사용한다. 이펙티브 자바에 보면 PECS(Producer Extends, Consumer Super)란 규칙도 이에 대해 설명한다. 


분해(decomposition)와 패턴 매칭


1 + 2와 같은 수식을 표현하기 위해 다음의 세 타입을 사용한다고 하자.


trait Expr {

  def isNumber: Boolean

  def isSum: Boolean

  def numValue: Int

  def leftOp: Expr

  def rightOp: Expr

}

class Number(n: Int) extends Expr { ... }

class Sum(e1: Expr, e2: Expr) extends Expr { ... }


Expr 타입을 받아서 값을 구하는 eval() 함수는 다음과 같이 Expr에 정의된 메서드를 이용해서 타입을 구분하고 값을 구하고 필요한 계산을 수행할 수 있다.


def eval(e: Expr): Int = {

  if (e.isNumber) e.numValue

  else if (e.isSum) eval(e.leftOp) + eval(e.rightOp)

  else ...(익셉션발생코드)

}


이 방식에서 새로운 연산인 Prod를 추가하면, Expr과 각 하위 타입에 isProd 메서드를 추가해야 하고 eval() 메서드에 isProd를 이용하는 if 문을 추가해야 한다. 변수 표현을 위한 Var가 추가되어도 동일하게 각 타입마다 Var인지 여부를 판단하기 위한 isVar 메서드를 추가하고 eval에 관련 if 문을 추가하게 된다.


이 방식은 새로운 타입을 추가할 때마다 기존 타입의 코드를 수정해야 하는 문제가 발생한다.  isNumber 메서드나 isSum 메서드를 사용하지 않고 타입을 검사하는 방법을 사용할 수는 있지만 이는 깨지기 쉬운 코드를 만들곤 한다.


이를 객체 지향 방식으로 풀어보면 다음과 같이 Expr에 eval() 메서드를 추가하는 것이다.


trait Expr {

  def eval: Int

}

class Number(n: Int) extends Expr {

  def eval: Int = n

}

class Sum(e1: Expr, e2: Expr) extends Expr {

  def eval: Int = e1.eval + e2.eval

}


더 나아지긴 했지만 여전히 다음의 단점이 있다.

  • 식을 문자열로 구해주는 toStr()과 같은 새로운 기능을 구현하려면 모든 타입에 메서드를 정의해야 한다
  • a * b + a * c를  a * (b + c)로 축약하는 기능은 한 객체의 메서드로 캡슐화할 수 없다.
이런 문제를 해소할 수 있는 방법이 있는데 그것은 바로 패턴 매칭을 사용하는 것이다. 패턴 매칭을 사용하면 eval() 메서드를 다음과 같이 구현할 수 있다.

def eval(e: Expr): Int = e match {
  case Number(n) => n
  case Sum(e1, e2) => eval(e1) + eval(e2)
}


케이스 클래스를 패턴 매칭에 사용할 수 있다. 케이스 클래스를 패턴 매칭에서 사용하는 자세한 내용은 스칼라 관련 서적에 자세히 나와 있다.


케이스 클래스와 패턴 매칭을 사용하면 새로운 기능을 추가하더라도 Expr과 그 하위 타입을 바꿀 필요가 없다. 또한 단일 객체의 메서드로는 구현할 수 없는 기능도 구현할 수 있다.

마틴 오더스키 교수님이 코세라에서 진행중인 Functional Programming Principles in Scala 강의(https://www.coursera.org/learn/progfun1)의 3주차 요약.


* 함수형을 잘 모르는 상태에서 요약한 것이므로 내용에 오류가 존재할 수 있음

* 3주차는 주로 클래스와 객체에 대한 내용으로 비교적 익숙했음


클래스 계층


추상 클래스는 구현이 없는 멤버를 포함할 수 있다. 구현이 없으므로 new로 추상 클래스의 인스턴스를 생성할 수 없다.


abstract class IntSet {

  def incl(x: Int): IntSet

  def contains(x: Int): Boolean

}


IntSet 클래스를 확장(extend)한 두 클래스-Empty, NonEmpty-는 다음과 같이 구현 가능하다.


class Empty extends IntSet {

  def contains(x: Int): Boolean = false

  def incl(x: Int): IntSet = new NonEmpty(x, new Empty, new Empty)

}

class NonEmpty(elem: Int, left: IntSet, right: IntSet) extends IntSet {

  def contains(x: Int): Boolean = 

    if (x < elem) left contains x else if (x > elem) right contains x else true

 

  def incl(x:Int): IntSet = 

    if (x < elem) new NonEmpty(elem, left incl x, right)

    else if (x > elem) new NonEmpty(elem, left, right incl x)

    else this

}


여기서 Empty와 NonEmpty는 IntSet 타입을 따르며(conform), IntSet 타입이 필요한 곳에 이 두 타입의 객체를 사용할 수 있다.


IntSet을 Empty와 NonEmpty의 상위클래스(superclass)라고 하며, Empty와 NonEmpty를 IntSet의 하위클래스(subclass)라고 한다.


상위 클래스를 지정하지 않으면 java.lang.Object 클래스를 상위 클래스로 가정한다. C 클래스의 직/간접 상위클래스를 베이스 클래스(base class)라고 한다. NonEmpty 클래스의 경우 IntSet과 Object가 베이스 클래스가 된다.


오버라이드를 사용하면 상위 클래스에 정의된 비추상 멤버를 재정의할 수 있다. 다음 코드에서 Sub 클래스의 foo 멤버는 Base 클래스의 foo 멤버를 재정의하고 있다.


abstract class Base {

  def foo = 1

  def bar: Int

}

class Sub extends Base {

  override def foo = 2

  def bar = 3

}


오브젝트


스칼라는 한 개의 인스턴스만 갖는 타입인 오브젝트를 지원한다. 예를 들어, NonEmpty는 한 개 인스턴스가 존재하면 되는데 다음과 같이 오브젝트 정의를 사용해서 NonEmpty를 싱글톤으로 정의할 수 있다.


object Empty extends IntSet {

  def contains(x: Int): Boolean = false

  def incl(x: Int): IntSet = new NonEmpty(x, Empty, Empty)

}


동적 바인딩(dynamic binding)


스칼라를 포함한 객체 지향 언어는 동적 바인딩을 구현한다. 동적 바인딩은 메서드를 호출할 때 메서드를 포함한 객체의 런타임 타입에 의존한다는 것을 의미한다.


치환 모델로 동적 바인딩의 계산 과정을 풀면 다음과 같다.


(new NonEmpty(7, Empty, Empty)) contains 7

-> [7/elem][7/x][new NonEmpty(7, Empty, Empty)/this] 

      if (x < elem) this.left contains x 

      else if (x > elem) this.right contains x else true

-> if (7 < elem) new NonEmpty(7, Empty, Empty).left contains 

    else if (7 > 7) new NonEmpty(7, Empty, Empty).right contains 7 else true

-> true


패키지와 임포트


자바처럼 패키지를 이용해서 클래스와 오브젝트를 구성한다. 패키지를 포함한 fully qualified 이름으로 클래스나 오브젝트를 참조할 수 있다.


import를 이용해서 단순 이름으로 사용할 수 있다. scala, java.lang, scala.Predef 오브젝트의 모든 멤버는 자동으로 임포트한다.


트레잇(trait)


스칼라의 클래스는 한 개의 상위클래스만 가질 수 있다. 다중 상속이 필요한 경우 트레잇을 사용할 수 있다. 트레잇은 추상 클래스처럼 추상 멤버와 구현을 가진 멤버를 선언할 수 있다. 


trait Planar {

  def height: Int

  def width: Int

  def surfce = height * width

}


클래스, 오브젝트, 트레잇은 최대 한 개의 클래스를 상속할 수 있지만, 트레잇은 여러 개를 상속할 수 있다.


class Square extends Shape with Planar with Movable ...


트레잇은 필드와 메서드 구현을 가질 수 있기 때문에 자바 인터페이스보다 더 강력하다. 클래스가 파라미터를 가질 수 있는 것과 달리 트레잇은 가질 수 없다.


스칼라 클래스 계층

  • Any: 모든 타입의 베이스 타입이다. ==, !=, equals, hashCode, toString 메서드를 정의한다.
  • AnyRef: 모든 레퍼런스 타입의 기반 타입이다. java.lang.Object에 해당한다. scala.List, scala.Seq 등이 속한다.
  • AnyVal: 모든 원시 타입의 기반 타입니다. (scala.Int, scala.Boolean 등이 속한다.)
  • Null: 레퍼런스 타입은 값으로 null을 가질 수 있다. null은 Null 타입 값으로 Null 타입은 Object를 상속한 모든 클래스의 하위타입이다.
  • Nothing: 모든 타입의 하위 타입으로 스칼라 타입 계층의 최하단에 위치한다.

다형(polymorphism)


다형은 함수 타입이 여러 폼(form)이 된다는 것을 의미한다. 이는 프로그래밍에서 다음을 뜻한다.

  • 함수를 여러 타입의 인자에 적용할 수 있거나 -> 지네릭: 함수나 클래스의 인스턴스를 타입 파라미터로 생성
  • 타입이 많은 타입의 인스턴스를 가질 수 있다 -> 하위타입: 하위클래스의 인스턴스를 베이스 클래스(형태)로 전달
예)
trait List[T]
class Cons[T](val head: T, val tail: List[T]) extends List[T]
class Nil[T] extends List[T]


상속을 통한 다형 : Nil 인스턴스를 List가 필요한 곳에 전달할 수 있다 -> new Cons(10, Nil)

타입파라미터를 통한 다형 : Cons를 여러 타입에 적용할 수 있다. Cons[User](u1, Nil), Cons[Member](m1, Nil)


마틴 오더스키 교수님이 코세라에서 진행중인 Functional Programming Principles in Scala 강의(https://www.coursera.org/learn/progfun1)의 2주차 요약.


* 함수형을 잘 모르는 상태에서 요약한 것이므로 내용에 오류가 존재할 수 있음


고차 함수(Higher-Order Function)


함수형 언어는 함수를 일급 값(first-class value)으로 처리한다. 다른 값 처럼 함수를 파라미터로 전달하거나 결과로 리턴할 수 있다. 이는 프로그램을 조합하는 유연한 방법을 제공한다.


파라미터로 다른 함수를 전달받거나 함수를 결과로 리턴하는 함수를 고차 함수(higher order function)라고 부른다.


고차 함수를 사용하면 여러 기능에 출현하는 공통 패턴을 도출할 수 있다. 다음 두 기능은 공통 패턴이 있는데,


def sumInts(a: Int, b: Int): Int = if (a > b) 0 else a + sumInt(a + 1, b)

def sumCubes(a: Int, b: Int): Int = if (a > b) 0 else (a * a * a) + sumCubes(a + 1, b)


고차 함수를 이용해서 구현하면 다음과 같이 공통 패턴을 뽑아낼 수 있다.


def sum(f: Int => Int, a: Int, b: Int): Int = if (a > b) 0 else f(a) + sum(f, a+1, b)


이 고차 함수를 이용해서 sumInt, sumCubes를 구현한 코드는 다음과 같다.


def id(x: Int): Int = x

def cube(x:Int): Int = x * x * x


def sumInts(a: Int, b: Int): Int = sum(id, a, b) // id 함수를 전달

def sumCubes(a: Int, b: Int): Int = sum(cube, a, b) // cube 함수를 전달


함수는 기본적인 추상인데, 그 이유는 이름을 부여한 명시적인 요소를 사용해서 연산을 수행하기 위한 일반적인 수단을 만들 수 있도록 해주기 때문이다.


임의 함수(anonymous function)


작은 함수를 매번 새로 정의하는 것은 성가신 일인데 임의 함수를 사용해서 이를 해소할 수 있다.


def sumInts(a: Int, b: Int): Int = sum( (x: Int) => x, a, b) // id 함수를 전달

def sumCubes(a: Int, b: Int): Int = sum(x => x*x*x, a, b) // cube 함수를 전달


임의 함수의 파라미터 타입은 컴파일러가 추론가능하면 생략가능하다.


커링(currying)


다음 sum 함수는 (Int => Int) 타입의 함수를 인자로 받아 (Int, Int) => Int 타입의 함수를 리턴한다.


def sum(f: Int => Int): (Int, Int) => Int = {

 def sumf(a,: Int, b: Int): Int = if (a > b) 0 else f(a) + sumf(a+1, b)


  sumf // sumf 함수를 리턴

}


다음은 함수를 리턴하는 sum 함수를 사용해서 구현한 sumCubes이다.


def cubes(x: Int):Int = x*x*x

def sumCubes = sum(cubes)


sum 함수가 cubes를 사용하는 sumf를 리턴하므로, sumCubes(1, 3)은 1 + 8 + 27을 리턴한다. sumCubes 없이 다음과같이 표현할 수도 있을 것이다.


sum(cubes)(1, 3)


함수는 왼쪽에서 오른쪽으로 적용하므로 다음이 성립한다.


sum(cubes)(1,3) == (sum (cubes)) (1,3)


스칼라는 함수를 리턴하는 함수를 위한 다중 파라미터 목록 문법을 제공한다. 앞서 함수를 리턴하는 sum을 다음과 같이 구현할 수 있다.


def sum (f: Int => Int) (a: Int, b: Int): Int =

  if (a > b) 0 else f(a) + sum(f)(a+1, b)


위 함수를 사용하면 sum(cubes) (1, 3)과 같이 함수를 실행할 수 있다.


일반적으로 다중 파라미터 목록을 가진 함수 정의가 있을 때,


def f(args1)...(argsn) = E ( n > 1)


이는 다음과 동등하다.


def f = (args1 => (args2 => ... (argsn => E ) )  )


이런 방식으로 함수를 정의하고 적용하는 것을 커링이라고 부른다. (커링은 Haskell Brooks Cury의 이름에서 딴 것이다.)


함수와 데이터


스칼라는 클래스를 이용해서 데이터를 추상화한다. 클래스 타입의 요소를 객체라고 부르며, 클래스 생성자의 적용(application) 앞에 new 연산자를 위치시켜 객체를 생성한다.


예) new Rational(1, 2)


객체의 멤버는 자바처럼 중위 연산자 "."를 사용해서 선택한다.


데이터를 다루는 함수를 클래스 자체에 넣을 수 있는데 이런 함수를 메서드라고 부른다.


데이터 추상화


다음 세 개의 Rational 클래스를 보자. 클라이언트 입장에서 다음의 두  Rational 클래스는 동일한 데이터를 제공한다.


class Rational(x: Int, y: Int) {

    private def gcd(a: Int, b: Int): Int = ...생략

    private val g = gcd(x, y)

    def numer = x  / g

    def denom = y / g

}


class Rational(x: Int, y:Int) {

    private def gcd(a: Int, b:Int): Int = ....

    val numer = x / gcd(x, y)

    val denom = y / gcd(x, y)

}


두 Rational의 numer와 denom은  클라이언트 입장에서 동일하게 동작한다. 이렇게 클라이언트에 영향없이 데이터의 다른 구현을 선택할 수 있는 것을 데이터 추상화라고 한다.


클래스와 치환 모델


함수를 적용할 때 치환에 기반한 계산 모델을 사용한 것처럼, 클래스와 객체에도 이 모델을 적용한다. 예를 들어, 클래스 다음의 클래스 정의가 있다고 하자.


class C(x1, ..., xn) { def f(y1, ..., yn) = b ..}


이 때 new C(v1, ..., vn).f(w1, ..., wn) 식은 다음과 같이 치환한다.


[w1/y1, ...., wn/yn][v1/x1, ... , vn/xn][new C(v1, ..., v1)/this]b


연산자


파라미터가 한 개인 메서드는 다음과 같이 중위 연산자처럼 사용할 수 있다.


r.add(s) ---> r add s


스칼라의 연산자는 실제로 메서드이며 연산자를 식별자로 사용할 수 있다. 식별자는 다음이 될 수 있다.

  • alphanumeric 식별자: 글자로 시작하고 글자나 숫자가 뒤에 온다.
  • symbolic 식별자: 연산자 심볼로 시작할 수 있고, 다른 연산자 심볼이 올 수 있다.
  • 밑줄('_')은 글자로 처리한다.
  • 밑줄로 끝나는 alphanumeric 식별자 뒤에 연산자 심볼이 위치할 수 있다.

다음은 식별자의 예이다.


xn3   *   ?  list_++   <== ==>


마틴 오더스키 교수님이 코세라에서 진행중인 Functional Programming Principles in Scala 강의(https://www.coursera.org/learn/progfun1)의 1주차 요약.


프로그래밍 패러다임


과학의 경우 패러다임은, 어떤 학문 분야에서 구분되는 개념이나 사고 패턴을 설명한다.


프로그래밍 패러다임: 명령형(Imperative) 프로그래밍, 함수형(Functional) 프로그래밍, 로직(Logic) 프로그래밍

(객체 지향 프로그래밍은 이들 패러다임에 orthogonal하다)


명령형 프로그래밍과 한계


명령형 프로그래밍은 다음에 관한 것이다.
  • 변경가능한 변수를 수정하고 할당을 사용하고 (메모리, 로드/저장 명령)
  • 조건문, 루프, 브레이크, 리턴과 같은 제어 구조를 사용 (점프)

순수 명령형 프로그래밍은 명령어를 순차적으로 실행하는 구조로, 단어별로(word-by-word???) 데이터 구조의 개념을 도출하는 경향이 있다.


다항, 콜렉션, 문자열, 문서와 같은 고수준 추상화를 정의할 수 있는 다른 기법이 필요하다.


이론(Theory)이란?


이론은 다음으로 구성된다.

  • 데이터 타입
  • 타입에 대한 연산(오퍼레이션)
  • 값과 연산 간의 관계를 기술하는 규칙

보통, 이론은 변경(mutation)에 대해 묘사하지 않는다. 


변경 없는 이론 예1) 다항식 이론

(a*x + b) + (c*x + d) = (a+c)*x + (b+d) 계수를 변경하는 연산을 정의하지 않음


변경 없는 이론 예2) 문자열 이론의 문자열 연결 연산자

(a ++ b) ++ c = a ++ (b ++ c) 시퀀스 원소 변경 연산자를 정의하지 않음


프로그래밍을 위한 결론

  • 연산자를 위한 이론을 정의하는데 집중
  • 상태 변경을 최소화
  • 연산자를 함수로 여기고, 종종 단순한 함수의 조합으로 처리

함수형 프로그래밍과 언어


 -

 함수형 프로그래밍

함수형 프로그래밍 언어 

 좁은(restricted) 의미

변경가능한 변수, 할당, 루프, 다른 명령형 제어 구조 없는 프로그래밍

변경 가능 변수, 할당, 명령형 제어 구조가 없는 언어 

 넓은(wider) 의미

함수에 집중하는 것 

함수를 이용해서 elegant한 프로그램 을 구성하는 것 


함수형 프로그래밍 언어에서 함수는 일급(first-class)이다.

  • 모든 곳에 정의 가능함 (다른 함수 내부 포함)
  • 다른 값처럼 함수를 파라미터로 전달 가능하고 결과로 리턴될 수 있음
  • 함수를 조합하는 집합 연산자 존재

요즘 함수형이 뜨는 이유


멅티코어와 클라우드 컴퓨티 환경에서 병렬을 활용하는 좋은 방법을 제공하기 때문이다.



프로그래밍의 요소(Elements of Programming)


언어는 다음을 제공한다.

  • 단순한 요소를 표현하는 프리미티브(원시) 식(primitive expression)
  • 식을 묶는 방법
  • 식을 추상화하는 방법(식에 이름을 붙이고 이를으로 참조)

식의 계산(evaluation)


원시 식이 아닌 연산자를 포함한 식은 다음 과정을 따라 계산한다.

  1. 가장 왼쪽 연산자를 선택
  2. 선택한 연산자의 피연산자의 값을 구함(왼쪽 피연산자의 값을 먼저 구함)
  3. 두 피연산자에 연산자를 적용
식에 포함된 이름은 그 이름 정의의 오른쪽으로 대치한다.(예 def name = body 이면, name을 body로 대치)
최종 결과가 값이 될 때까지 과정을 반복한다.

과정 예) pi = 3.14159 이고 radius = 10
(2 * pi) * radious (가장 왼쪽 연산자 선택)
-> (2 * 3.14159) * radius (연산자의 피연산자 값 구함, pi를 3.14159로 대치 후 연산자 적용)
-> 6.28318 * radius (가장 왼쪽 연산자 선택)
-> 6.28318 * 10 (연산자의 피연산자 값 구함, radius를 10으로 대치 후 연산자 적용)
-> 62.8318 (최종 결과로 값 구함)

함수 적용(function application) 계산

파라미터를 가진 함수의 계산 과정은 다음과 같다.
  1. 모든 함수 인자를 왼쪽부터 오른쪽으로 차례대로 계산
  2. 함수 적용을 함수의 우측(함수의 몸체)으로 대치하고, 동시에
  3. 함수의 formal 파라미터를 실제 인자로 대치함
적용 예) 
def square(x: Double) = x * x
def sumOfSquares(x: Double, y: Double) = square(x) + square(y)

sumOfSquares(3, 2+2)
-> sumOfSquares(3, 4) (인자 값 구함)
-> square(3) + square(4) (sumOfSquares 함수의 우측으로 대치 및 파라미터를 인자로 대치)
-> 3 * 3 + square(4) (square 함수의 우측으로 대치)
-> 9 + square(4) (식의 계산에 의해 첫 번째 연산자 식을 계산)
----> 9 + 16 (비슷한 과정 생략)
-> 25

치환 모델(Substitution model)

앞의 식을 계산하는 방식을 치환 모델이라고 부른다. 이 모델의 아이디어는 모든 계산은 식을 한 개의 값으로 reduce한다. 식에 side effect가 없으면 이 모델을 모든 식에 적용할 수 있다.

계산의 끝(termination)

모든 식의 계산이 끝나는 것은 아니다.

예)
def loop: Int = loop
val i = loop()  // loop 식 계산은 끝나지 않음


값 계산의 두 가지 전략

  • call-by-value(이하 CBV): 식을 값으로 바로 계산해서 전달, 모든 함수 인자를 한 번만 계산하는 장점.
  • call-by-name(이하 CBN): 식의 값이 실제로 필요할 때까지 이름으로 전달, 함수에서 사용하지 않는 인자는 함수 몸체를 적용하는 과정에서 계산하지 않는 장점

두 방식은 다음 조건을 충족하는 한 두 방식은 동일한 값으로 reduce된다.

  • reduce한 식이 순수 함수로 구성
  • 둘 다 계산이 끝남

CBV 식이 끝나면 동일한 CBN 식도 끝나지만, 반대는 성립하지 않는다. 아래 코드에서 CBN의 경우 first(1, loop)는 1로 계산되지만 CBV의 경우 두 번째 인자인 loop의 값 계산이 끝나지 않아 식이 끝나지 않는다.


def first(x: Int, y: Int) = x

first(1, loop)


조건 식


스칼라의 if-else은 식이다. if의 predicate은 Boolean 값이다. &&와 ||는 왼쪽 피연산자의 값에 따라 오른쪽 피연산자를 계산하지 않는다. 예를 들어, true || e의 경우 왼쪽이 true이므로 오른쪽의 e 값을 구할 필요가 없으므로 결과는 true가 된다. 이런 식을 "short-circuit evaluation"을 사용한다고 말한다.


중첩 함수


스칼라는 함수 안에서만 접근할 수 있는 중첩 함수를 정의할 수 있다. 특정 함수 안에서만 사용하는 함수를 중첩함으로써 "name-space pollution"을 피할 수 있다.


블록


스칼라의 블록은 중괄호로 구분하며, 정의나 식을 포함한다. 블록 그 자체는 식이므로 식이 올 수 있는 모든 자리에 블록이 올 수 있다.


블록 안에 위치한 정의는 블록 안에서만 보이며, 블록 밖의 같은 이름을 가진 정의를 감춘다. 블록 밖의 정의는 블록 안에서 접근할 수 있다.


val x = 0

val y = 1

def f(y: Int) y + 1

val result = {

    val x = f(3) // 블록 밖의 x를 감춤

    x * x + y // 블록 안의 x임, 블록 밖의 정의에 접근 가능

} + x // 블록 밖의 x


꼬리 재귀(tail recursion)


함수의 마지막 액션이 자신을 호출하면, 함수의 스택 프레임을 재사용할 수 있다. 이를 꼬리 재귀라 한다. 스칼라는 꼬리 재귀를 최적화한다.


def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b) // 마지막 액션이 꼬리 재귀, 최적화함


def factorial(n: Int): Int = if (n == 0) 1 else n * factorial(n-1) // 마지막 액션이 꼬리 재귀 아님



+ Recent posts