Scala의 variant

Scala의 variant는 낯설고도 헷갈리기 쉬운 개념이다.

개념 정의

A <: B 일때 C[A] <: C[B] 라면, C는 covariant다.
A <: B 일때 C[A] >: C[B] 라면, C는 contravariant다.

역도 성립한다.

C가 covariant라면, A <: B 일때 C[A] <: C[B]이다.
C가 contravariant라면, A <: B 일때 C[A] >: C[B]이다.

선언하는 법

C를 covariant로 선언하려면,

class C[+A] { ... }

C를 contravariant로 선언하려면,

class C[-A] { ... }

아래 코드의 두번째줄에서 타입 에러가 발생하지 않으려면,

object Nil extends List[Nothing] { ... }
val x : List[String] = Nil

List는 아래와 같이 covariant로 선언되어야 한다.

class List[+T] { ... }

이렇게 하면 List가 covariant이므로, String >: Nothing 일때 List[String] >: List[Nothing] 이다. 따라서 에러가 발생하지 않는다.

Advertisements

Scala와 Haskell의 문법

Functional Progrmaming Principles in Scala 수업을 듣고 스터디를 하면서 배운 스칼라의 문법 및 언어 특징들을 Haskell과 비교하면서 적어본다.

Type inference

Scala는 함수를 정의할 때 return type이 무엇인지 생략해도 알아서 추론을 할 수 있다.

scala> def double (x: Int) = x * 2
scala> :type double
(x: Int)Int

하지만 formal parameter의 타입은 생략할 수 없다.

scala> def double (x) = x * 2
<console>:1: error: ':' expected but ')' found.
   def double(x) = x * 2
               ^

Haskell은 둘 다 생략해도 정확하게 추론해준다.

Prelude> let double x = x * 2
Prelude> :t double
double :: Num a => a -> a

currying

Scala는 currying을 쉽게 할 수 있도록, multiple parameter list를 지원한다.

scala> def sum1 (x: Int, y: Int) = x + y
scala> def sum2 (x: Int)(y: Int) = x + y
scala> sum1(3, 4) == sum2(3)(4)
res2: Boolean = true

Haskell도 지원한다.

Prelude> let sum1 (x, y) = x + y
Prelude> let sum2 x y = x + y
Prelude> sum1(3, 4) == sum2 3 4
True

Haskell은 어째 후자가 더 자연스러워보인다.

High order function

Scala에서 function은 first class citizen이다.

scala> def apply (f: Int => Int, x: Int) = f(x)
scala> def double (x: Int) = > x * 2
scala> apply(double, 4)
res1: Int = 8

apply의 첫번째 parameter에 들어갈 함수가 Int가 아닌 다른 타입을 다룰 수 있게 하려면 다음과 같이 타입 파라메터를 명시해야한다.

scala> def apply[T, U](f: T => U, x: T) = f(x)
scala> def double (x: Int) => x * 2
scala> apply(double, 4)
res2: Int = 8
scala> def hello (x: String) => "hello, " + x
scala> apply(hello, "world")
res3: java.lang.String = hello, world

Haskell은 타입 추론이 잘 되니 타입 파라메터가 필요없다.

Prelude> let apply (f, x) = f x
Prelude> let double x = x * 2
Prelude> apply(double, 4)
8
Prelude> let hello x = "hello, " ++ x
Prelude> apply(hello, "world")
"hello, world"

Anonymous function

Scala에서 익명 함수(anonymous function)를 정의하는 문법은 보통 함수을 정의하는 문법과 살짝 다르다.

scala> def sum (x: Int, y: Int) = x + y
scala> sum(3, 4)
res1: Int = 7
scala> ((x: Int, y: Int) => x + y)(3, 4)
res2: Int = 7

솔직히 항상 헷갈린다. 특히 ==>로 바뀌는 부분이.

Haskell도 마찬가지로 익명 함수 정의 문법이 별도로 있다.

Prelude> let sum x y = x + y
Prelude> sum 3 4
7
Prelude> (x y -> x + y) 3 4
7

Haskell도 =->로 바뀐다. 뿐만 아니라 파라메터 목록 앞에 가 더해진다.

Scala나 Haskell이 맨날 쓰는 언어면 익명함수 문법 정도 그냥 외워버리겠지만 어쩌다 한번 쓰는거다보니 매번 까먹고 적어놓은 것을 뒤적거리게 된다.

우분투에서 와이파이가 안될때

나는 맥북에어에 우분투를 사용한다. MacOSX를 쓰다가 수 개월 전에 넘어왔다.

우분투가 MacOSX보다 못한 점 중 하나가 와이파이가 잘 안된다는 점이다. 단순히 늦게 연결되는 정도가 아니라 아예 불통인 경우가 수두룩하다.

우분투에서 인터넷이 안될 때 시도해 볼 수 있는 몇가지 팁을 적어본다.

애플릿 재시작

무선 인터넷이 안되서 와이파이 애플릿에서 AP를 바꿔보려고 하는데 전혀 먹통인 경우가 있다. 아무리 클릭해도 아무런 반응을 하지 않는다.

이럴 땐 그냥 애플릿을 재시작하면 된다. 우분투에서 사용하는 와이파이 애플릿 이름은 nm-applet이다. killall로 죽이고 다시 띄우자.

드라이버 바꿔보기

애플릿에는 아무 문제가 없는데 아무리해도 AP에 연결이 안되는 경우가 있다. 이럴 땐 드라이버를 바꿔보자.

맥북에어 + 우분투에서 쓸 수 있는 드라이버는 wl(broadcom closed driver)과 brcmsmac(opensource driver)의 두 가지가 있는데 wl로는 잘되고 brcmsmac으로는 안 될 때가 있는가 하면 그 반대의 경우도 있다.

wl을 쓰려면 Applications > Additional Drivers에서 Broadcom STA wireless driver를 켜면 된다. 혹시 wl 드라이버를 블랙리스트에 추가한 상태라면 지운다.

$ sudo rm /etc/modprobe.d/blacklist-wl.conf

brcmsmac을 쓰려면 wl을 블랙리스트에 넣고, /etc/modules 파일에 brcmsmac을 추가한다.

$ echo 'blacklist wl' |sudo tee -a /etc/modprobe.d/blacklist-wl.conf
$ echo 'brcmsmac' |sudo tee -a /etc/modules

둘 다 안된다면 업데이트된 버전의 wl을 사용해보자. 여기에 업데이트하는 방법이 나와있다.

그래도 안되면

주위에 쓰는 사람이 많아서 안되는 경우도 있다. 똑같이 쓰는 사람이 많아도 MacOSX에선 잘되고 우분투에서는 안될 수 있다. 사람이 줄어들기를 기다리자.

한달간 매일 블로깅을 해보고

9월 한달동안 하루도 빠지지 않고 이 블로그에 포스팅을 했다. (이 포스팅을 포함해서)

어디선가 좋은 프로그래머가 되기 위해서는 블로그를 성실하게 운영해보는 것이 좋다는 이야기를 들은 것이 계기이다. 성실하게 운영하려면 포스팅하는 습관을 들여야 할 것이고, 습관을 들이기 위해서는 하루도 빼놓지 않고 매일 하는 것이 가장 좋은 방법이라고 생각했다. 이 과정에서 문제점을 발견하기도 했지만, 목표로 한 기간 동안에는 무시하고 계속 진행했다.

한달간의 매일 블로깅에 대해 스스로 분석한 결과를 적어본다.

포스팅 시간 분석

기록된 로그를 통해 나의 포스팅 시간을 분석했다. 여기서 포스팅 시간이란, vim을 열어 포스트를 작성하기 시작해서 vim을 종료하기까지의 시간이다. 따라서 이 작성 시간에는 워드프레스를 통해 웹으로 편집한 시간은 제외되므로 5분 정도 더하는 것이 정확할 것이다. 포스트 작성 도중에 인터넷을 뒤적거리거나 한 시간은 포함되지만, 작성 전에 인터넷으로 자료를 수집한 시간은 포함되지 않는다.

지난 30개의 포스트 중 로그가 잘 남아있는 24개를 대상으로 분석해 본 결과,

  • 평균 포스트 작성 시간은 61분이다.
  • 가장 빨리 작성한 포스트는 “iOS6 사파리의 POST 캐싱 버그에 대해“로 12분이 걸렸다. 사실 이 포스트의 경우 인터넷으로 먼저 자료를 수집한 뒤 작성을 시작한 것도 적게 걸린 이유 중 하나이다.
  • 가장 오래 작성한 포스트는 “개인위키를 10년간 쓰며 배운것“으로 2시간 48분이 걸렸다. 데이터 수집을 위해 내 개인위키의 히스토리를 탐색하거나 하는 데 많은 시간을 썼기 때문이다. 순수하게 포스트를 작성한 시간만 계산하면 68분이다.
  • 도저히 쓸 것이 없으면 내 개인위키를 뒤져서 옛날에 조사했던 것을 토대로 올렸다. (예:
    ECMAScript의 Object와 Property, 3-way merge 알고리즘에 대해)

얻은 것

  • 글을 빨리 쓸 수 있게 되었다. 원래 나는 글을 빨리 쓰지 못해서, 과거(2004-2005년)에 블로그를 운영할 때는 1시간 내로 쓴 포스트는 거의 없었다. 하지만 이번 매일 블로깅 중에는 1/3에 달하는 포스트를 30분 내로 작성했다.

발견된 문제점

  • 다른 사람에게 보여주기 위한 글로서는 부족하다는 생각이 드는 경우도 꽤 많았다. 예를 들어 “3-way merge 알고리즘에 대해“는 지나치게 많은 부분을 생략했기 때문에 그냥 읽고서는 이해하기가 매우 어렵다. 오로지 하루에 한번이라는 꾸준함에 집중했기 때문에 이러한 사실을 알고서도 그냥 포스팅했다.
  • 쓰다가 귀찮아져서 인터넷질을 하느라 많은 시간을 낭비하는 경우도 종종 있었다.
    vim에 능숙해지는 법“가 그런 포스트 대표적인 예이다. 별 내용도 없는 포스트임에도 불구하고 포스트를 쓰기 시작해서 끝내는 데 까지 2시간 36분이 걸렸다. 인터넷 뉴스에서 아무 상관없는 기사를 읽거나 vimgolf에 가서 노느라 많은 시간을 썼다. vimgolf 하는데만 1시간 15분을 썼다. 대개 딱히 쓰고 싶지 않은 주제에 대해 포스팅을 하려고 할 때 이런 현상이 일어난다.
  • 포스팅하는데 1시간 정도 걸린다고 해도, 그 전에 뭘 쓸지 고민하며 빈둥대는 시간도 상당하기 때문에 실제로는 총 1시간 30분 정도는 소모하는 것으로 보는 것이 맞을 것이다. 블로그의 포스팅에 매일 1.5시간씩 쓰는 것은 다소 과한 감이 있다.

개선방향

매일 포스팅하는 것은 과연 쉽지 않은 일이다. 쓸거리는 갈수록 줄어들 것이고 그렇게 되면 포스트를 쓰는 시간보다 뭘 쓸지 고민하느라 낭비하는 시간이 점점 늘어날 것이다. 따라서 둘 중의 하나를 택해야 한다. 별로 영양가 없는 글이라도 꾸준히 포스팅하거나, 아니면 포스팅 횟수를 줄여야 한다.

우선 후자를 시도해보고자 한다. 10월에는 1주일에 두번씩만 포스팅을 할 것이다. 1주일에 두번이라는 횟수는 습관으로 만들기에는 애매하지 않은가 하는 생각이 들긴 한다. 일주일동안 계속 안 쓰다가 토, 일에 한개씩 쓸 가능성이 농후하다. 그러나 9월에서의 실험에서도 그랬듯이, 어떻게 포스팅을 할 것인가에 대해 결정하기 위한 데이터를 얻기 위해 부작용은 감수하고 진행한다.

ps. 내가 로그를 남긴 방법에 대해서는 “내가 맥으로 뭘 하고 있는지 로그 남기기 (애플스크립트)“나, “내가 리눅스로 뭘 하고 있는지 로그 남기기 (파이썬)“를 보라.

3-way merge 알고리즘에 대해

Git에서 두 브랜치를 머지할 때, 기본적으로 3-way merge 알고리즘을 사용하게 된다.

어떤 sequence A, B와 그 둘의 base인 sequence O가 있다고 하자. Git으로 치면 A, B는 머지할 브랜치, O는 base commit이 된다. sequence의 각 item은 소스코드의 한 줄이라고 생각하면 된다.

이렇게 A, O, B가 있을 때,

  1. A, O, B에 대해 [Longest common subsequence](이하 LCS) (( 이름 그대로 가장 긴 공통 sequence이다. 알고리즘은 http://rosettacode.org/wiki/Longest_common_subsequence 의 예제코드들을 보면 쉽게 이해할 수 있을 것이다. )) 를 찾는다. A, O에 대해 LCS를 먼저 찾고, O, B에 대해서도 찾은 다음 이 LCS들을 통해 얻는다.
  2. 위의 LCS를 이용해 모든 chunk에 대해 stable, unstable 여부를 검사한다. A, O, B가 모두 같다면 stable이고 아니면 unstable이다.
  3. unstable chunk에서 A와 O가 같지만 B가 다르다면 B를 택하고, B와 O가 같지만 A가 다르다면 A를 택한다. 셋이 모두 다르다면 충돌로 처리한다.

A Formal Investigation of Diff3에 소개된 예를 통해 위의 알고리즘을 설명하겠다.

다음과 같은 세 sequence가 있다면,

A = [1, 4, 5, 2, 3, 6]
O = [1, 2, 3, 4, 5, 6]
B = [1, 2, 4, 5, 3, 6]

A, O의 LCS는 다음과 같고

A = [[1], [4, 5], [2, 3], [],     [6]]
O = [[1], []      [2, 3], [4, 5], [6]]

O, b의 LCS는 다음과 같다.

O = [[1, 2], [3], [4, 5], [],  [6]]
B = [[1, 2], [],  [4, 5], [3], [6]]

셋의 LCS를 찾으면 다음과 같다.

A = [[1], [4, 5], [2], [3],       [6]]
O = [[1], [],     [2], [3, 4, 5], [6]]
B = [[1], [],     [2], [4, 5, 3], [6]]

위에서 [1], [2], [6]은 셋이 모두 같으므로, stable chunk라고 하며 그 외에는 unstable chunk라고 한다. unstable chunk에 대해서 conflict가 발생헀는지 찾는다. A, O, B가 모두 다른 chunk가 있다면 conflict다.

A' = [[1], [4, 5], [2], [3],       [6]]
O' = [[1], [4, 5], [2], [3, 4, 5], [6]]
B' = [[1], [4, 5], [2], [4, 5, 3], [6]]

2번째 chunk는 A만 [4, 5]이고 O와 B는 같으므로([]) A에서만 변경이 있었음을 의미한다. merge하면 따라서 [4, 5]가 선택된다. 4번째 chunk인 [3], [3, 4, 5], [4, 5, 3]은 A’, O’, B’가 모두 다르므로 conflict다.

위 결과를 출력하면 다음과 같다.

1
4
5
2
<<<<<<< A
3
||||||| O
3
4
5
=======
4
5
3
>>>>>>> B
6

그러나 실제 patch, diff3, Git등으로 merge를 해 보면 이보다 더 쉽게 충돌한다. 주위 문맥을 고려하기 때문이다. 대개 전후 3줄 이내에 변경이 있으면 충돌로 간주한다.

3-way merge에 대해 더 자세히 알고 싶다면 A Formal Investigation of Diff3을 보라. Longest common subsequence를 포함해 3-way merge의 전 과정에 대해 자세히 설명하고 있다.

문자집합과 문자 인코딩 구조

문자집합(character set)은 이름 그대로 문자의 집합이다. ASCII, ISO-8859-1, UCS(Unicode의 문자집합) 등이 문자집합이다. ASCII는 127개, ISO-8859-1는 191개, 가장 최신의 UCS(Unicode 6.2)는 110,182개 문자의 집합으로 정의된다. UCS에 포함되는 문자의 갯수는 Unicode의 버전이 올라가면서 점점 증가하고 있다. 각 문자들은 번호가 매겨진다.

컴퓨터로 이 문자집합을 다루기 위해서는, 각 집합에 속한 문자들을 특정 길이의 정수(코드값)로 표현할 수 있어야 한다. 한 문자집합의 문자들을 코드값 표현하는 방법은 한 가지가 아니다. 예를 들어 UCS의 경우 UTF-8, UTF-16, UTF-32 등으로 표현할 수 있다. 문자 하나를 UTF-8은 8~32 비트, UTF-16은 16*n비트, UTF-32는 32비트 크기의 코드값으로 표현한다. (( 좀 더 엄격하게 표현하자면, 문자에 대응하는 unicode code point를 code unit sequence로 변환한다. )) 이러한 표현방식들을 문자 인코딩 형태(character encoding form)라고 부른다.
이 코드값들로 이루어진 텍스트 데이터를 디스크에 저장하거나 네트워크로 전송하려면 옥텟의 시퀀스로 변환해주어야 한다. UTF-8 같은 경우 아무것도 안 해주어도 되지만, UTF-16 같은 경우엔 엔디안 처리가 필요하다. 이러한 변환방법들을 문자 인코딩 구조(character encoding scheme)라고 부른다.

그러나 컴퓨터 산업에서는 이 개념들을 정확히 잘 구분해서 사용하고 있지 못하다. 예를 들어 MIME의 charset은 마치 문자집합의 줄임말처럼 보이지만 전혀 그렇지 않다. 책 HTTP: The Definitive Guide에 따르면, MIME의 charset은 문자 인코딩 구조(character encoding scheme)와 부호화된 문자 집합(coded character set, 앞에서 말한 문자집합을 좀 더 엄밀하게 정의한 개념)의 의미가 결합된 개념으로 쓰이고 있다. (( HTTP: The Definitive Guide, p377 “Charset Is Poorly Named” )) 따라서 us-ascii, iso-8859-1, utf-8 등이 모두 charset 속성의 값이 될 수 있다. MIME이 이러다보니 HTTP에서도 terminology의 호환을 위해 charset을 MIME에서와 같은 잘못된 의미로 사용하고 있으며, 이 사실에 대해 명시하고 있다. (( RFC 2616 3.4. Character Sets ))

ECMAScript(JavaScript)의 Object와 Property

JavaScript(이하 ECMAScript)를 처음 접하면 프로토타입 개념이 혼란을 일으키곤 하는데, Object가 어떻게 생성되고 프로퍼티를 어떻게 읽고 쓰는지만 확실히 기억해두면 혼란을 피할 수 있다.

Object 생성하기 (( ECMA-262 13.2.2 ))

ECMAScript에서 오브젝트란 프로퍼티들의 정렬되지 않은 컬렉션이다. 오브젝트는 생성자를 통해 만들어진다. Foo()라는 생성자가 있다면 다음의 방법으로 만들 수 있다.

x = new Foo();

이 때 생성자가 prototype이란 이름의 프로퍼티를 갖고 있다면 그 프로퍼티의 값이 생성되는 오브젝트의 프로토타입이 된다.

Foo.prototype = new Number();
x = new Foo();

new Number() 오브젝트는 x의 프로토타입이므로, x 오브젝트에 어떤 property를 추가하지 않았더라도 그 property가 new Number() 오브젝트에 존재한다면 x 오브젝트의 프로퍼티처럼 읽게 된다. 다음 절을 읽어보자.

프로퍼티 읽기/쓰기

ECMASCript에서 오브젝트의 프로퍼티의 읽기/쓰기는 다음과 같이 동작한다.

읽기

프로퍼티를 읽는 내부 메소드를 GET이라고 한다. GET은 다음과 같이 동작한다. (( ECMA-262 8.12.3 ))

어떤 오브젝트에 대해 GET P를 하면,

  1. 이름이 P인 프로퍼티가 있으면 그 프로퍼티의 값을 반환
  2. 없으면 [[Prototype]] 내부 메소드로 프로토타입을 가져오고 null이 아니라면 [[Get]] (P) 를 수행, null이면 undefined 반환.

쓰기

프로젝트를 쓰는 내부 메소드를 PUT이라고 하는데, PUT은 프로퍼티 존재 유무 확인하고 그런 거 없이 그냥 현재 오브젝트에 바로 값을 쓴다. 쓸 수 없다면 예외를 던진다. (( ECMA-262 8.12.5 ))