FP in Elm의 week 1-2 Intro to FRP in Elm 정리 포스트다.


Introduction to FRP in Elm

JS 이벤트 리스너 코드 예제를 보여주면서 IsDown같은 변수를 만들어 상태를 저장해야 하는 부분, 콜백으로 지정하는 복잡성 등에 얘기하며 FRP에서는 더 쉽게(?) 할 수 있다고 이야기.

Signal이란

“시그널은 시간에 따라 변동되는 값” 원시적 시그널은 Signal Bool을 반환.

함수형 프로그래밍 블럭을 만들어서 시그널을 추상화하거나 합성할 수 있음.

Signal.map : (a -> b) -> Signal a -> Signal b
isUp = Signal.map (fun curValIsDown -> not curValIsDown) Mouse.isDown
-- or,
isUp = Signal.map not Mouse.isDown

이제 Mouse.isDown 시그널이 업데이트 되면 자동으로 반영됨.

Signal.map과 같이 함수를 각각에 맵핑하는 것은 다른 데이터에서도 많이 사용. List.map, String.map, Maybe.map, Dict.map, etc.

HTML로 렌더링하기

main에 정의하는 것으로 HTML을 렌더링 할 수 있음. Graphics.Element

elm-repl을 사용할 수 없으므로 elm-make, 온라인 에디터를 활용, 또는 로컬 환경을 활용.

Text.plainText는 최근 버전에서 삭제되서 구버전을 설치하거나 changelog를 참고해야 한다. 아래는 [Text](http://package.elm-lang.org/packages/elm-lang/core/2.1.0/Text) 를 참고했다.

import Text exposing (fromString)
import Graphics.Element (Element, leftAligned)

plainText = leftAligned << fromString

main : Element
main = plainText "Hello World!"

Text.fromStringStringText로 변환하고 Graphics.Element.leftAlignedTextElement로 변환해준다. 코드에서 필요한 함수를 import 한 것도 확인할 수 있다.

실제로 mainSignalElement로 정의되어 있다. 위와 같이 다른 Element는 Signal.constant를 이용해 Signal로 변환된다.

Signal.constant : a -> Signal a

위 예제를 더 명시적으로 작성하면 다음과 같다.

main : Signal Element
main = Signal.constant(plainText "Hello World!")

위 내용으로 작성한 예제.

import Text exposing (fromString)
import Graphics.Element exposing (Element, leftAligned)
import Mouse
import Signal

plainText = leftAligned << fromString
isUp = Signal.map not Mouse.isDown

main : Signal Element
main = Signal.map (\b -> plainText(toString b)) isUp

import에 대해

매번 쓸 때마다 앞 코드처럼 map하지 말고 다른 모듈로 만들어서 재활용하라는 이야기.

함수 합성에 대해

다음은 함수를 합성하는 여러 방법. 취향에 맞게 선택을 하라는데 가장 마지막 방식이 많이 쓰는 모양.

(\b -> b |> toString |> plainText)
(toString >> plainText)
(\b -> plainText <| toString <| b)
(plainText << toString)

Folding From the Past

클릭 횟수를 보여주는 페이지를 만들려고 함. 이런 signal은 내장되어 있지 않은데 다음과 같이 모조 유닛 ()으로 된 이벤트를 정의할 수 있음.

Mouse.clicks : Signal ()

기본 MVC 아키텍쳐

-- Model
type alias State = Int

initState : State
initState = 0

alias는 새 타입 정의 없이 Int와 동일한 역할을 하는 State를 만들어줌. abbr 만들 때도 쓸 수 있고.

-- View
view : State -> Element
view s = asText s
-- 더 간결하게
view = asText

asTextTextasText에서 Graphics.Elementshow변경됨.

컨트롤러는 시그널이 갱신되었을 때, 상태를 변형하거나 렌더링하는 함수를 시그널과 연결해주는 역할을 한다. 타입 a는 전체 값, 타입 b는 초기값, 마지막 타입 b는 최종 상태값이 된다.

Signal.foldp : (a -> b -> b) -> b -> Signal a -> Signal b

List.foldl : (a -> b -> b) -> b -> List a -> b
List.foldr : (a -> b -> b) -> b -> List a -> b

folding from the past 는 from the left라는 말이고 from the future는 from the right이 된다. 시그널은 folding from the past로 호출한다.

step : a -> State -> State
step _ i = i + 1

main : Signal Element
main =
  Signal.map view (Signal.foldp step initState Mouse.clicks)

다른 예제는 여기.

main = Signal.map VIEW (Signal.foldp UPDATE INIT_STATE BASE_SIGNAL)
-- so,
main = Signal.map view (Signal.foldp step initState (Time.every Time.second))

잠깐만

맨 처음 예로 든 JS와 달리 부수적인 부분은 다 컴파일러가 알아서 함.

JavaScript로의 컴파일링

원초적인 이벤트는 앞서 구현한 JS에서와 같이 다뤄짐. 하지만 elm이 Signal Graph로 처리해 순수 함수 형태로 정의된 Signal을 사용할 수 있게 함.

각각의 기능이 순수한 함수 형태로 다른 함수에 영향을 주지 않아 전체를 컴파일 할 필요가 없어짐. 이런 접근 방식은 함수형 언어 컴파일러 최적화와 관련되어 중요한 개념 중 하나.

2D 그래픽

Graphics.Element, Graphics.Collage 공부할 것.

읽을 거리

필수

  • 라이브러리 Signal, Graphics.Element, Graphics.Collage

Signal에 정의된 (<~)는 위에서 정의한 isUp을 간편하게 정의하는데 유용함.

isUp = not <~ Mouse.isDown
-- `Mouse.isDown` 시그널을 `not` 함수를 통해 전달

추천

심화

Redis를 리눅스 박스에 직접 설치해본 적이 한번도 없었다. Ubuntu에 redis를 설치하려니 빌드가 생각처럼 진행되질 않아서 계속 검색을 하게 되었는데 기록 삼아 블로그에 적어둔다.

$ apt-get update
$ apt-get install build-essential
$ wget http://download.redis.io/releases/redis-3.0.3.tar.gz # 버전은 달라질 수 있으니 사이트를 확인
$ tar xzf redis-3.0.3.tar.gz
$ cd redis-3.0.3
$ cd ./deps
$ make hiredis jemalloc linenoise lua
$ cd ..
$ make
$ ./src/redis-server
$ make test # 얘네들이 권장하는데 tcl 설치해야 함
$ make install # 취향에 따라
$ redis-server

의존성 라이브러리 때문에 에러가 계속나서 라이브러리를 한참 찾았는데 deps 디렉토리가 있는걸 나중에야 알았다. 라이브러리가 없으면 자동으로 make을 하는 것 같은데 어중간하게 라이브러리 직접 설치한, 나같은 사람은 수동으로 make 해줘야 한다. 안그러면 다음 에러가 계속 난다. 뭔가 꼬인 것 같으면 make clean을 사용한 후, 다시 make을 진행한다.

root@koala:~/redis-3.0.3# make
cd src && make all
make[1]: Entering directory `/root/redis-3.0.3/src'
    LINK redis-server
cc: error: ../deps/hiredis/libhiredis.a: No such file or directory
cc: error: ../deps/lua/src/liblua.a: No such file or directory
cc: error: ../deps/jemalloc/lib/libjemalloc.a: No such file or directory
make[1]: *** [redis-server] Error 1
make[1]: Leaving directory `/root/redis-3.0.3/src'
make: *** [all] Error 2

이런 삽질 하지 말라고 docker가 나왔는데 아무래도 익숙해지지 않아 걱정이다.

FP in Elm의 week 1-1-2 Intro to ML in Elm 정리 포스트다.


Introduction to ML in Elm

Elm은 웹사이트에서 받아 설치한다. REPL로 진행한다.

$ elm-repl
Elm REPL 0.4.2 (Elm Platform 0.15.1)
  See usage examples at <https://github.com/elm-lang/elm-repl>
  Type :help for help, :exit to exit
> True
True : Bool
> False
False : Bool
> 'a'
'a' : Char
> "abc"
"abc" : String
> 3.0
3 : Float

numberIntFloat 모두를 의미. 타입 검사기가 알아서 선택.

> 3
3 : number
> truncate 3
3 : Int
> truncate 3.0
3 : Int

number가 하스켈에서 type 클래스로 미리 정의한 것처럼 보이지만 Elm엔 일반적으로 타입 클래스 지원이 없음. 이 number는 몇가지 특별한 용도로 사용할 수 있는 클래스 중 하나.

Tuples

두번째 튜플에서 ‘가 여러개 붙는 이유는 모르겠다. 타입이 달라질 수 있어서 그런 것 같기도. 튜플에 컴포넌트가 하나면 튜플이 아닌 것으로 취급.

> (True, False)
(True,False) : ( Bool, Bool )
> (1,2,3,4.0)
(1,2,3,4) : ( number, number', number'', Float )

> ("Leave me alone!")
"Leave me alone!" : String
> (("Leave me alone!"))
"Leave me alone!" : String

Functions

인자 하나, 반환값 하나를 갖는 함수:

> exclaim = \s -> s ++ "!"
<function> : String -> String
> exclaim s = s ++ "!"
<function> : String -> String
> exclaim "HI"
"HI!" : String

uncurried/curried 스타일 다인자 함수:

> plus (x, y) = x + y
<function> : ( number, number ) -> number
> plus = \(x, y) -> x + y
<function> : ( number, number ) -> number
> plus xy = fst xy + snd xy
<function> : ( number, number ) -> number

> plus x y = x + y
<function> : number -> number -> number
> plus x = \y -> x + y
<function> : number -> number -> number
> plus = \x -> \y -> x + y
<function> : number -> number -> number

curried 함수를 활용한 부분 애플리케이션:

> plus3 = plus 3
<function> : number -> number
> plus3 5
8 : number
> plus3 3.0
6 : Float

number 타입 캐스팅을 어떻게 할 것인가. number -> Int는 만들 수 없지만 어짜피 number는 Int가 필요할 때 자동으로 변하니 그냥 작성.

> toInt n = n // 1
<function> : Int -> Int
> plusInt x y = (toInt x) + y
<function> : Int -> Int -> Int
> plusInt x y = (toInt x  + y)
<function> : Int -> Int -> Int

타입 어노테이션 Type Annotations

대부분의 ML 방언과 같이 자동으로 처리하지만 최상위 레벨에서 수동으로 지정해야 좋은 경우도 있음. 예제 IntroML.elm 참조.

plus : number -> number -> number
plus x y = x + y

plusInt : Int -> Int -> Int
plusInt x y = x + y

plusInt : Int -> Int -> Int
plusInt = plus

앞에서 toInt 쓴 것과 달리 plusInt에서 명시적으로 어노테이션을 지정하고 위처럼 쓸 수 있음. 클라이언트에게 공개되는 API보다 더 범용적인 코드를 구현하는 것을 강조.

모듈 불러오기 Importing Modules

앞서 IntroML.elm을 받는다. exposing을 사용하게 변경되었다. (..)은 모듈 내 모든 함수를 노출하게 된다. 모듈을 약어로 부를 때는 as 키워드를 사용한다.

> import IntroML
> IntroML.plusInt 2 3
5 : Int
> import IntroML exposing (plusInt)
> plusInt 2 3
5 : Int
> import IntroML exposing (..)
> exclaim "Awesome"
"Awesome!" : String
> import IntroML as M
> m.plusInt 2 3
5 : Int

Basics, Maybe 등이 포함된 일반 라이브러리는 기본으로 불러오게 됨.

조건문

> if 1 == 1 then "Yes" else "No"
"Yes" : String
> if False then 1.0 else 1
1 : Float
> if | 1 == 1 -> 1.0 \
|    | 1 == 2 -> 1
1 : Float
> if | 1 == 1     -> 1.0 \
|    | otherwise  -> 1
1 : Float

otherwise는 True : Bool. 다중조건문 multi-way-if는 조건 중 참으로 평가되는 경우가 없으면 런타임 에러가 발생. 실행되지 않을 조건이 있다면? 닿지 않는 코드는 평가도 하지 않음. 다중조건문에서 줄 맞추는 것 잊지 말 것.

다형성 타입 Polymorphic Types

타입변수는 소문자로 시작하거나 한글자로 지정되는 경우가 많음.

> choose b x y = if b then x else y
<function> : Bool -> a -> a -> a
> choose True True False
True : Bool
> choose True "a" "b"
"a" : String
> choose True 1.0 2.0
1 : Float
> choose True 1 2
1 : number
> choose True 1 2.0
1 : Float

만약 다음과 같이 타입 어노테이션을 지정한다면 다형성 타입이지만 타입을 강제할 수 있음.

choose : Bool -> number -> number -> number

Basics에서 작성된 비교 연산자에는 comparable 이라는 특수 목적의 타입 변수가 존재함.

> (<)
<function> : comparable -> comparable -> Bool
> 1 < 2
True : Bool
> 1 < 2.0
True : Bool
> "a" < "ab"
True : Bool
> (2, 1) < (1, 2)
False : Bool
> (1 // 1) < 2.0 -- 타입 불일치
> True < False -- 타입 불일치

딴짓: 버그잡기

버그가 이미 잡혀서 내용 결과가 다름. 딴짓 실패.

버그를 찾으면 메일링 리스트에서 검색하고 버그 리포트를 남기자는 이야기.

리스트

cons 연산자는 코스에선 제안된 기능인데 이미 반영되서 import 할 필요 없음. 제안 기능은 불러와서 쓸 수 있는 모양. (::)는 OCaml 문법 , 는 하스켈 문법이라고.

> 1::2::3::4::[]
[1,2,3,4] : List number
> [1,2,3,4]
[1,2,3,4] : List number
> [1..6]
[1,2,3,4,5,6] : List number
> [1.0..6.0]
[1,2,3,4,5,6] : List Float

하스켈은 String이 List Char인데 여긴 아니라고.

> ['a','b','c']
['a','b','c'] : List Char
> "abc"
"abc" : String
> ['a','b','c'] == "abc" -- 타입 불일치

case 한줄로 쓰기. (갑자기 난이도 점프를 시도한 느낌.)

> len xs = case xs of {_::xs -> 1 + len xs; [] -> 0}
> len [1..10]
10 : number
> len ['a', 'b', 'c']
3 : number

글자 수를 세는 len이라는 함수를 정의했다. 리스트를 xs로 받아서 리스트 가장 앞에 녀석을 하나 빼 글자수 1을 더하고 나머지 xs를 다시 len 함수에 보내 계속 글자 수를 알아낸다. 계속 반복해서 빈 리스트 []가 되면 0을 반환해 전체 글자 수를 얻게 된다!

case가 모든 결과를 처리하지 못하면 완전하지 않은 패턴이 발견되었다고 런타임 에러가 발생한다. 다중 case문에서는 두번째 줄부터 맨 앞에 공백을 넣어야 한다.

> hd xs = case xs of \
|   x::_ -> x
<function> : List a -> a
> hd [2]
2 : number
> hd []
Error: Runtime error in module Repl (between lines 4 and 5)
Non-exhaustive pattern match in case-expression.
Make sure your patterns cover every case!

고차함수

> import List exposing (filter, map, foldr, foldl)
> filter
<function> : (a -> Bool) -> List a -> List a
> filter (\x -> x `rem` 2 == 0) [1..10]
[2,4,6,8,10] : List Int
> map
<function> : (a -> b) -> List a -> List b
> map(\x -> x ^ 2) [1..10]
[1,4,9,16,25,36,49,64,81,100] : List number
> foldr
<function: foldr> : (a -> b -> b) -> b -> List a -> b
> foldr (\x acc -> x :: acc) [] [1..10]
[1,2,3,4,5,6,7,8,9,10] : List number
> foldl
<function: foldl> : (a -> b -> b) -> b -> List a -> b
> foldl (\x acc -> x :: acc) [] [1..10]
[10,9,8,7,6,5,4,3,2,1] : List number

(::)도 함수라서 다음처럼 가능.

> (::)
<function> : a -> List a -> List a
> foldl (\x acc -> (::) x acc) [] [1..10] -- eta-expanded version
[10,9,8,7,6,5,4,3,2,1] : List number
> foldl (::) [] [1..10] -- eta-reduced version
[10,9,8,7,6,5,4,3,2,1] : List number

데이터타입

리스트는 귀납형 데이터 타입이라 직접 데이터 타입을 정의 할 수 있음. (enum 같은 느낌.) 값을 가지지 않을 수도, 가질 수도 있음.

> type Diet = Herb | Carn | Omni | Other String
> Carn
Carn : Repl.Diet
> Omni
Omni : Repl.Diet
> Other "Lactose"
Other "Lactose" : Repl.Diet
> Other -- Non-nullary data constructor는 그 자체로 함수
<function> : String -> Repl.Diet
> diets = [Herb, Omni, Omni, Other "Lactose"]
[Herb,Omni,Omni,Other "Lactose"] : List Repl.Diet

패턴매칭에 유용. 결과가 나오지 않는 case를 작성하지 않도록 주의.

> isHerb d = case d of \
|   Herb -> True \
|   _    -> False
<function> : Repl.Diet -> Bool
> List.map isHerb diets
[True,False,False,False] : List Bool

에러를 위한 타입

앞에서 작성한 hd 함수는 빈 리스트를 넣었을 때 런타임 에러가 발생하고 실패함. 에러를 위한 타입을 만들어서 의미있는 결과를 받도록 처리.

> type MaybeInt = YesInt Int | NoInt
> hd xs = case xs of \
|   x::xs' -> YesInt x \
|   []     -> NoInt
<function> : List Int -> Repl.MaybeInt
> hd [1..4]
YesInt 1 : Repl.MaybeInt
> hd []
NoInt : Repl.MaybeInt

다형성 타입으로 바꾸면,

> type MaybeData a = YesData a | NoData
> hd xs = case xs of \
|   x::_ -> YesData x\
|   []   -> NoData
<function> : List a -> Repl.MaybeData a
> hd [1]
YesData 1 : Repl.MaybeData number
> hd ['a','b','c']
YesData 'a' : Repl.MaybeData Char
> hd []
NoData : Repl.MaybeData a

이 방식은 MaybeData 패턴으로 엄청 일반적이고 Maybe라는 라이브러리도 포함되어 있음.

> type Maybe a = Just a | Nothing
> hd xs = case xs of \
|   x::_ -> Just x \
|   []   -> Nothing
<function> : List a -> Repl.Maybe a

Maybe 패턴을 활용한 Result 가 제공되고 있음. 다음은 예시에서의 코드.

errHead : List a -> Result String a
errHead xs = case xs of
  x::_ -> Ok x
  []   -> Err "errHead: expects non-empty list"

저장해서 불러오면 다음과 같은 결과.

> import MaybeStudy exposing (errHead)
> errHead ["What", "WHhooo"]
Ok "What" : Result.Result String String
> errHead []
Err ("errHead: expects non-empty list") : Result.Result String a

이항 연산자 infix Operators

(<|), (|>), (<<), (>>)를 Basics 문서에서 찾아보라고.

(<|)는 괄호를 입력해야 하는 번거로움을 줄여준다.

leftAligned (monospace (fromString "code"))
leftAligned << monospace <| fromString "code"

(|>)도 괄호를 줄여주고 역순으로 입력하기 때문에 이해하기 더 쉬운 코드가 된다.

scale 2 (move (10,10) (filled blue (ngon 5 30)))
ngon 5 30
  |> filled blue
  |> move (10,10)
  |> scale 2

위 둘은 괄호를 회피하는 것 이외에도 앞서 함수의 치역과 뒤따라오는 함수의 정의역을 일치시키기 위해서 결괏값을 먼저 처리하는 것 같다.

(<<)와 (>>)는 함수 합성에 사용한다.

(g << f) == (\x -> g (f x))
(g >> f) == (\x -> f (g x))

Let 표현식

지역 스코프 한정 변수를 let으로 지정해 선언한다. 앞 공백이 중요함. 그 밑은 같은 결과, 쉬운 표현.

plus3 a =
  let b = a + 1 in
  let c = b + 1 in
  let d = c + 1 in
    d

plus3 a =
  let b = a + 1
      c = b + 1
      d = c + 1
  in
    d

plus3 a = a |> plus 1 |> plus 1 |> plus 1

plus3 = plus 1 << plus 1 << plus 1

읽을 거리

필수

추천

그외

seoh님의 Elm Resources 글에서 [Functional Programming: Purely Functional Data Structures in Elm

]3 강의를 알게 되었다. 개요를 읽고 흥미가 생겨 강의 노트를 읽기 시작했고 나중에 쉽게 찾아보려고 짤막하게라도 정리하기로 했다. 강의에서 사용된 elm이 이미 구버전이라서 최신 버전과 다른 부분이 있어 그 부분은 별도로 적었다. 조금 지나면 내 노트도 금방 구버전이 될 것 같은 기분이지만.

강의 노트 정리 페이지 목록보기


Course Overview

이 코스는 다음 두가지 테마로 진행된다.

  • 효과적인 데이터 구조를 구현하고 분석, 특히 함수형 프로그래밍 언어의 맥락에서
  • 인터렉티브 프로그래밍을 위한 함수형 리액티브 프로그래밍(FRP) 배우기

여기서 사용한 언어는 ML의 방언인 Elm. ML은 1970년대 개발되어 많은 영향을 주고 있다.

  • 평가 전략 Evaluation Strategy: (이른 평가 문법 위에서 동작하는) 지연 평가
  • 타입 Types: 정적 타입, 자동 타입 인터페이스 등 쿨한 기능이 있어 하스켈과는 조금 다르다고.
  • 부작용 Side Effects: 함수형 프로그래밍에서는 가변변수 등 절차형에서 제공하는 기능을 사용할 수 있지만 작게, 지역적으로 사용하길 권장. ML은 타입 시스템 바깥에서 처리(?)하고 하스켈은 타입으로 기록하는 방식으로 차이가 있음.

Elm은 Standard ML, OCaml, F#에 비해 적은 기능만 추가돼 작은 방언이라 얘기한다.

이 코스도 처음으로 진행되고 Elm도 작은 언어에 작은 커뮤니티라 삽질을 각오하고 코스에 임할 것.

Elm 강의를 보고 있는데 멱집합을 생성하는 함수가 과제로 나왔다. 한참을 고민하다가 결국 검색을 해보게 되었다. 반성하는 차원에서 How is this powerset generating function working?를 짧게 번역했다. 수학공부 부지런히 해야겠다.


이 멱집합 생성 함수는 어떻게 동작하는거죠?

리스트를 넣어 멱집합을 생성하는 함수를 만들려고 하는데 다음과 같은 코드를 인터넷에서 찾게 되었다. 설명은 없었지만 테스트 해보니 정상적으로 동작했다. 이 함수가 어떻게 동작하는지 이해할 수가 없었다. 설명을 해주면 감사하겠다.

generateSubset [] = [[]]
generateSubset (x:xs) = let p = generateSubset xs in p ++ map (x:) p

Daniel Wagner의 답변

멱집합의 특성으로 쉽게 증명할 수 있다. P(A ∪ B) = {a ∪ b | a ∈ P(A) 또는 b ∈ P(B)}. 특히 집합 S와 그 집합에 포함된 원소인 s로 분해하고 S’의 모든 원소는 s가 아닐 때,

P(S) = P({s} ∪ S')
     = {a ∪ b | a ∈ P({s}), b ∈ P(S')}

여기서 P({s})는 직접 계산하기 좋을 정도로 충분히 작다. P({s}) = {{}, {s}}. 이 사실로 다음과 같이 유도한다.

P(S) = {a ∪ b | a ∈ {{}, {s}}, b ∈ P(S')}
     = {b | b ∈ P(S')} ∪ {{s} ∪ b | b ∈ P(S')}
     = P(S') ∪ {{s} ∪ b | b ∈ P(S')}
     = let p = P(S') in p ∪ {{s} ∪ b | b ∈ p}

이 방법은 멱집합이 빈 원소를 갖는 집합이기 때문에 사용 가능한 계산 방법이다. 먼저 원소를 고른 후, 나머지로 멱집합을 연산한다. 그 후 각각의 부분집합에 추가하거나 더하지 않는다. 질문한 함수는 이 부분을 코드로 전환한 것으로 리스트를 집합으로 사용했다.

-- P         ({s} ∪ S') = let p = P(S')             in p  ∪ {{s} ∪ b | b ∈ p}
generateSubset (x:xs)   = let p = generateSubset xs in p ++     map (x:) p

재귀를 위해 기본값을 넣어주는 일만 남았다. 멱집합의 정의에 따라 다음과 같이 추가한다.

-- P          ({}) = {{}}
generateSubset []  = [[]]

1년 반 만에 세번째 이사를 하게 되었다. 그간 Justin님 댁에서 감사하게도 정말 편하게 하숙 생활을 하며 걱정없이 지낼 수 있었다. 몸이 편하면 게을러지는 타입인 나란 사람은 좀 더 부지런히 지내기 위해 주변 환경을 바꿔야겠다는 생각이 들어 이사를 결정하게 되었다.

처음으로 직접 집을 빌리고 사용할 가구도 구입하는 등 이사 자체가 이전과는 전혀 다른 경험이기도 했고 처음으로 혼자 살게 되서 기대만큼 걱정도 컸다. 이제 이사온 지 거의 한달이 되었는데 얼마 전 인터넷까지 설치가 완료되서 지금까지의 과정을 기록해보기로 했다.

이사갈 집 찾기

이사의 순서는 다음과 같다.

  1. 부동산 메타 사이트(realestate.com.au, domain.com.au, etc.)에서 집 검색
  2. 인스펙션 약속을 잡아 집을 살펴봄
  3. 마음에 들면 어플리케이션 제출, 안들면 1번으로
  4. 합격(?)하면 부동산에서 연락이 와서 보증비 bond를 먼저 입금
  5. 짐 꾸리기
  6. 이사가기 전에 렌트비 입금
  7. 이사 당일 열쇠를 수령한 후 이사

메타사이트를 보면 베드룸 몇, 주차 몇, 화장실 몇으로 표시되고, 부동산에서 작성한 설명과 사진을 볼 수 있다. 사이트를 보다보면 쇼핑하는 기분이 들 정도로 화려한 사진도 많고 거창한 설명도 많은데 막상 가보면 실제와 다른 경우도 있었다. 한군데만 보고 결정할 수 없으니 결정을 쉽게 하기 위해 어떤 집을 고를까 목록을 먼저 만들었다.

  • 출퇴근 30분 내외 거리, 트램 정거장 가까운 곳, 기차역 있으면 +a
  • 10분 내외 거리에 장 볼 수 있는 곳
  • 1 or 2 베드룸
  • 카펫보다는 마루바닥
  • 전기렌지는 비싸고 조리음식하기 불편하므로 가스렌지 있는 곳
  • 북쪽으로 창문이 있어 채광이 잘되고 습하지 않은 곳
  • 2층 이상이면 +a
  • 녹물 나오지 않는 곳, 물이 콸콸 나옴, 냉온수 잘나옴
  • 샤워부스 있고, 세탁기 설치할 수 있는 곳
  • 조금 비싸더라도 살면서 불평하지 않을 집으로

위 목록 기준으로 메타 사이트를 검색했다. 일단 회사를 트램으로 통근할 수 있는 위치를 찾았다. 처음엔 72번 트램과 Glen waverly 트레인 라인이 교차하는 Glen Iris 인근에 알아보려고 했는데 주변 편의시설이 없어 장보려면 트램을 이용해야만 하는 불편함이 있었다.

게다가 그 동네에 나온 집도 별로 많지 않아서 괜찮아 보이는 곳은 두 군데 정도밖에 없었다. 점심시간을 짬 내 한 곳을 다녀왔는데 중개인이 시간을 안 지켜서 보지도 못하고 오고 그래서 이상하게 정이 가지 않는 동네였다.

그렇게 트램 라인을 따라 검색하던 중 Armadale 인근에 집이 많이 나와 있어 가장 많은 집이 인스펙션 하는 날짜에 휴가를 내고 여섯 군데 집을 돌아봤다. 다행스럽게도 그중에서 위 조건에 가장 충실한 한 곳을 찾을 수 있었다.

신청서는 부동산 업체마다 양식이 조금씩 다르긴 하지만 일종의 보증인을 적게 되어 있는데 각각 보증인에게 직접 전화해서 신원을 확인하는 절차가 있다. 전에 같이 살던 사람, 회사 상사, 동료, 친구 등을 적게 되어 있다. 그 외에는 안정적인 수입이 있는지 확인할 수 있도록 은행 명세서, 신원확인을 위한 신원 증명 관련 서류를 첨부하게 되어 있다. 특히 신원 확인의 경우, 점수제로 100점 이상 넘겨야 하는데 내 경우에는 여권, 우체국에서 발급해주는 Photo ID, 은행 서류로 점수를 넘길 수 있었다.

신청서를 제출한 다음 날, 부동산 에이전시에서 연락이 와서 서류가 통과되었다고 보증비를 입금하라는 연락이 왔다. 입금한 후 에이전시에 방문에 추가적인 설명을 듣고 서류에 서명해 모든 절차를 완료했다.

그러고 집에 와서 짐을 꾸렸는데 어느 사이에 이렇게 짐이 많이 늘어났는지 한참 걸렸다.

이사 짐싸기

이사하는 날

침대, 책상, 의자와 같은 가구가 하나도 없어서 IKEA Richmond에 가서 주문했다. 이전에 IKEA에 사전에 다녀와 어떤 가구를 살지 보고 왔었어 구매하려고 하는 목록을 빠르게 픽업할 수 있었다. IKEA도 무서운 게 조금만 질이 좋아져도 가격이 수배로 뛰어버리는 통에 다른 곳에서도 골라서 사고 싶었지만, 배송비가 워낙에 비싸 한 번에 주문할 수 있는 곳에서 다 주문했다. 감사하게 Justin님 댁에서 이사하면 필요할 도구들도 많이 주셔서 자잘한 물건들 사는 걸 많이 줄일 수 있었다.

가구 구입

원래는 배달하는 사람을 쓰기로 하다가 시간이 맞질 않아서 IKEA에서 제공하는 배송 서비스를 이용했다. 3시 이전까지 배송을 신청하면 당일에 배송해준다길래 열심히 가구를 구매해서 배송을 신청했고 집에 와서 기다렸다. 그 사이에 보스도 맥주 사서 놀러와 빈 집 구경을 하고 갔다. 이 때까지는 금방 배달 오리라 믿었는데…

IMG_6799

예정된 시간이 지나도 배송도 오지 않고 연락도 없었다. IKEA는 택배사에 문의하란 얘기만 반복하고 택배사는 전화를 받지 않아 밤 10시까지 기다리다가 그냥 집으로 갔다.

그리고 그 다음 날, 역시나 아침에 배달 온다고 전화가 왔고 급하게 와 물건을 받았다. 전날 밤에 비가 잠깐 왔는데 그래서 그냥 안 오고 갔단다. 가구 던질까 봐 화를 낼 수가 없었다.

배달!

교회 형이 도와주러 와서 같이 침대를 조립했더니 3시간이 지나 저녁 시간이 되어 버렸다. 그래서 같이 저녁 먹고 보내고서 혼자 열심히 성인용 LEGO인 IKEA 가구를 조립하다 잤고 다음 날 나머지 조립하고 청소하고 쓰러졌다.

신청해야 할 것들

우편물 주소 변경 서비스 신청

호주 우체국에서는 이사했을 때 기존 주소로 오는 우편물을 새 주소로 배달해주는 서비스를 제공하고 있다. 1달에 23.05달러, 3달에 39.55달러이므로 상황에 맞게 신청하면 된다. 새 주소가 수취 가능한 주소인지 확인 메일이 발송되고 그 메일에 서명해 우체국으로 보내면 그때부터 변경된 주소로 보내준다.

실제 거주하는지 확인하는 Mail Redirection 서비스

전기/도시가스 신청

전기와 도시가스는 Origin Energy로 신청했고 별문제 없이 연결된…줄 알았지만, 하루 단전을 겪었다. 인터넷을 통해 전기와 가스를 쉽게 신청할 수 있길래 잘 만들었다고 생각했는데 다음 날 전기가 끊겨 깜깜한 집에서 2시간가량 전화 붙들고 겨우야 연결할 수 있었다. 인터넷으로 신청한 신청서는 전산에도 잡히지 않아서 CS 담당자도 의아해했는데 어찌 되었든 전기 연결에 성공했다.

단전 >_<

전기와 가스 연결 시에 여러 가지 물어본다. 개를 키우는지, 태양광 발전기가 설치되어 있는지, 집에 생명 유지장치 같은 게 필요한지 등 질문한 후 약관에 동의하면 원격에서 예정된 날짜에 전기와 가스를 연결해준다. 이 연결이 원격으로 가능하면 연결 비용이 5달러가량 청구되고 원격으로 안되면 100달러 이상이 든다고 한다.

연결 신청을 한 후에도 이전 공급자에게서 자꾸 경고 편지가 왔다. 앞서 전기도 한번 단전된 경험이 있어서 전화해서 연결 상황을 여러 번 확인해야만 했다. 가격에 따라 회사를 선택할 수 있는 것은 좋긴 하지만 공급자끼리 정보가 잘 공유되지 않아 단전의 고통을 겪어야 하는 건 소비자라는 게 씁쓸하다.

아직 전기와 가스 비용이 나와보지 않아서 얼마나 나올지는 잘 모르겠다. 전기는 3달에 한 번씩, 가스는 2달에 한 번씩 고지서가 발송된다고 한다.

인터넷 신청

호주는 일부 지역엔 NBN이 들어와 빠른 인터넷을 사용할 수 있지만, 이 동네에는 아직 ADSL밖에 옵션이 없었다. 인터넷은 Engin으로 연결했는데 ADSL이라서 인터넷을 설치하기 위해서는 전화를 가설해야 했다. 그래서 사용하지 않을 전화까지 설치하게 되었다. (합해서 월 70달러)

가입하고 얼마 지나지 않아 모뎀이 도착해 설치했다. 연결했는데 신호가 안 잡혀 CS에 상담을 했다. 전화기로 먼저 라인을 확인해야 한다고 해서 전화기를 구매해야만 했고 연결을 해보니 역시 되질 않아 기술 지원팀이 집을 방문했다. 라인을 점검해서 건물까지는 라인이 살아있는데 건물 단자함에서 집 안으로 라인이 들어오질 않고 있다는 걸 확인해줬다. 이 경우에는 프로퍼티 매니저에게 연락해서 조처를 해달라고 요청해야 한다고 해서 연락을 했고 또 다른 테크니션과 약속을 잡아 내부 라인을 확인했다.

연결 확인

내부 라인을 확인한 결과, 모두가 라인이라고 생각했던 그 선을 따라 반대로 가보니 아무 곳에도 연결되지 않은 상태로 주방 장판 밑에 숨겨져 있었다. 즉 집에 전화선 자체가 존재하지 않았던 것. 그 사실을 확인한 테크니션은 건물 외부로 선을 만들어 집까지 끌어와야 한다고 했고 집주인 허락을 받아야 한다고 알려줬다. 또다시 프로퍼티 메니저에게 연락했고 테크니션 말대로 하기로 집주인과 합의를 봤다고 연락이 왔다. 또 약속을 잡아 방문한 테크니션은 5시간여 외부 라인 공사 끝에 벽에 구멍을 내 전화선을 연결해줬고 드디어 인터넷이 연결되었다.

벽에 구멍내서 포트 연결

이 모든 과정이 신청에서부터 1달 걸렸다. 인터넷 설치가 가장 오래 걸린다고 빨리 신청하라 해서 신청했었는데 인터넷 없이 한 달 비용을 내게 되었는데 선 끝 모양이 전화선이라고 모두 연결된 전화선은 아니라는 교훈을 얻었다.


호주의 여유로움이 살기 좋은 세상을 만든다고 생각하지만 한편으로는 한국과 같은 말도 안되는 속도가 그리울 때가 있다. 이번 이사로 좀 더 여유로움에 익숙해질 기회가 되었는지 잘 모르겠다. 처음으로 혼자 지내게 되었는데 아이들도 있고 시끌시끌한 곳에 있다가 혼자 지내니 어색하기도 하다. 처음 호주에 도착했을 때 그 기억도 새록새록 나고, 새로이 각오를 다져 열심히 지내야겠다.

Backbone.js를 지금까지 사용해본 적이 없었는데 주말에 깜짝 방문한 jimkimau님과 함께 살펴보게 되었다. 처음 사용해보는데다 아직 이사온 곳에 인터넷이 아직 들어오지 않아 문서 없이 코드만 보고 살펴볼 수 있을지 걱정했다. 컴퓨터를 검색해보니 어디서 사용한지 모르겠지만 backbone.js 파일을 찾을 수 있었다. 게다가 un-zipped 버전의 backbone.js에는 주석으로 모든 함수에 대한 설명과 사용 방식이 상세하게 남겨져 있어 오프라인이 문제가 되지 않았다.

underscore를 개발한 개발자가 만들어서 그런지 underscore에 대한 강한 의존성을 가지고 있다. Underscore도 방대하고 유익한 함수가 많다는 사실을 익히 들어 알고 있었지만 실무에서 사용하고 있지 않아 익숙해질 기회가 별로 없었다. 이번에 backbone.js를 살펴보던 중 이 라이브러리를 사용하는 방식이 상당히 인상적이었다. underscore를 더 재미있게 활용하는데 도움이 될 것 같아 어떤 방식으로 사용하고 있는지 발췌해 포스트로 정리해봤다.

객체 확장하기: _.extend()

Backbone.js에서는 기본적으로 underscore에서 제공하는 _.extend(destination, *sources) 함수를 이용해 확장하는 방식을 채택하고 있다. 이 함수는 sorces로 전달된 모든 객체의 프로퍼티를 복사해 destination 객체에 넣고 그 객체를 반환한다. 만약 동일한 명칭의 프로퍼티가 있다면 sources로 제공된 순서에 따라 덮어쓰게 된다. Backbone.js 전반에 걸쳐 상속 등에 사용되고 있는 함수다.

_.extend(Colletion.prototype, Events, {
    ...
});

Backbone.js에서 제공하는 모든 모듈은 위와 같은 방식으로 Backbone.Events의 함수를 확장하고 있다. 이 Events가 모든 모듈에 포함되어 이벤트 핸들링을 쉽게 만들며 전역 객체인 Backbone도 이 Events를 상속해 전역적인 pubsub을 구현할 수 있도록 돕고 있다.

객체 생성하기: _.defaults()_.uniqueId()

_.defaults()은 객체의 기본값을 설정할 때 많이 사용되는 함수로 Backbone.js에서는 기본값에 사용자 설정을 채워넣을 때 사용하고 있다.

var setOptions = {add: true, remove: true, merge: true};
// ...
_.extend(Collection.prototype, Events, {
  set: function(models, options) {
    options = _.defaults({}, options, setOptions);
    // ...
  }
  // ...
};

앞서 _.extend()와 유사하게 느껴지지만 _.defaults()는 첫번째 인자로 들어온 객체에 프로퍼티가 존재하지 않거나 null인 경우 즉, obj[prop] == null이 참인 경우에 이후 인자의 프로퍼티를 덮어쓰게 된다.

또한 객체를 초기화 하는 과정에서 고유한 id가 필요한 경우가 있는데 backbone.js는 _.uniqueId() 함수를 사용해 객체의 cid를 설정하고 있다. 인자로 제공한 문자열은 prefix가 된다. 이 함수는 호출할 때마다 1씩 증가한다.

this.cid = _.uniqueId('view'); // view1

객체에서 필요한 값 얻기: _.pick(), _.result()

객체를 확장하기 위해서 _.extend() 함수를 사용하는 것을 앞에서 확인했다. 확장에서 모든 객체를 확장하는 것이 아니라 필요로 하는 메소드와 프로퍼티만 확장하고 싶을 수도 있다. 객체의 모든 값이 아니라 일부만 필요하다면 _.pick() 함수를 활용할 수 있다.

var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
_.extend(this, _.pick(options, viewOptions));

위와 같이 배열로 입력해도 되고 필요로 하는 키 이름을 나열해도 반환한다.

var bibimbab = {gosari: 10, egg: 4, rice:3, pepper_paste:10, spinach:3, sesame_oil: 2};
_.pick(bibimbab, 'egg', 'rice', 'sesame_oil');
// Object {egg: 4, rice: 3, sesame_oil: 2}
_.pick(bibimbab, ['gosari', 'spinach']);
// Object {gosari: 10, spinach: 3}

함수가 1급 클래스인 JavaScript의 특성으로 객체 프로퍼티가 값인 경우도 있고 함수인 경우도 있다. 프로퍼티 값을 받는 것과 같이 함수의 경우에는 호출해서 그 반환 값을 바로 받도록 하고 싶다면 직접 _.isFunction() 함수로 검사해서 값을 만들어도 되지만 간편하게 _.result() 함수를 활용할 수 있다.

var attrs = _.extend({}, _.result(this, 'attributes'));

위와 같이 객체에서 사용할 어트리뷰트가 함수의 형태로 저장되어 있다면 함수를 실행해 그 결과를 반환해서 바로 사용할 수 있다.

underscore 메소드 재사용하기

Bacbone.js에서 제공하는 Backbone.Collection에서는 underscore의 다양한 함수를 체이닝으로 사용할 수 있도록 확장되어 있다. 문서에서는 collection에서 언더스코어의 90% 가량 함수를 지원한다고 기술하고 있다.

var methods = ['forEach', 'each', 'map', 'collect', 'reduce', ... ];

_.each(methods, function(method) {
  Collection.prototype[method] = function() {
    var args = slice.call(arguments);
    args.unshift(this.models);
    return _[method].apply(_, args);
  };
});

Backbone.Collection이 테이블이라면 각각의 데이터 행을 Backbone.Model로 제공한다. 위 코드에서 보면 Collection에 존재하는 model의 목록을 unshift()로 인자목록 앞에 넣어 underscore로 넘기는 방식으로 체이닝 메소드를 구현했다.

위와 같은 구현은 컬렉션 외에도 Backbone.Model에서도 유사하게 지원한다. 앞서 언급한 바와 같이 model은 단일 엔티티를 의미하고 있어 keys(), values()와 같은 메소드만 선택적으로 확장하고 있다.

앞서의 methods와 달리 attributeMethods의 경우는 다음과 같이 약간 다른 구현이 필요하다.

var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];

_.each(attributeMethods, function(method) {
    Collection.prototype[method] = function(value, context) {
      var iterator = _.isFunction(value) ? value : function(model) {
        return model.get(value);
      };
      return _[method](this.models, iterator, context);
    };
  });

익명함수나 값을 기준으로 처리하는 attribute 함수는 위와 같은 방식으로, 함수인지 아닌지 판별 과정을 거쳐 함수인 경우 필터에 사용한다. 함수가 아닌 경우 해당 모델에서 값을 받아 사용할 수 있도록 처리되었다.


위에서 언급한 부분 외에도 흥미로운 부분이 많았다. _.once() 함수를 Backbone.Events에서 구현한 방식도 인상적이었고, 함수형 접근에 더 적절한 맥락을 갖도록 몇가지 함수를 제어 역전하는 부분도 살펴보기 좋다. 이벤트 트리거를 위해 내부적으로 사용하고 있는 함수인 triggerEvents()에서는 속도를 위해 switch를 쓰는 재미있는 구석도 있다.

코드를 읽는데도 주석도 잘 달려있고 각각의 함수 이름도 잘 지어진데다 잘게 잘 쪼개져 있어서 배우게 된 부분이 많았다. Backbone.js와 underscore를 관통하는 패러다임을 살펴볼 수 있었고 지금도 많은 개발자가 이 라이브러리를 사용할 만큼 매력적이란 사실을 알 수 있었다. lodash와 함께 나올 underdash도 기대가 된다.

사소한 일에도 고민을 많이하는 편이다. 이미 결정된 일에도 고민하는 편이며 사람과의 관계에도 매사 조심스러워 하는데다 이곳 저곳에 생각을 많이 쓴다. 가끔 이유 없이 아플 때도 이런 잦은 고민과 관련이 있지 않을까 싶다. 최근 몇년은 고민을 줄이고 행동으로 먼저 옮기는 삶을 살려고 노력하고 있고, 그 결과로 지금 여기까지 지내오게 되었다. 여전히 무의식적으로 고민을 하는 편이지만 결정을 빨리 내려 “장고 끝 악수” 같은 일은 만들지 않으려고 하고 있다.

지나치게 고민하는 습관을 고치려고 오래 생각을 했었는데 나에게 있어서 고민은 깊이있게 생각하고 판단하는 과정으로의 고민보다는 단지 결정이 어렵기 떄문에 그 순간을 뒤로 미루고 행동을 지연하는, 일종의 게으름이란 결론을 갖게 되었다. 결국 하기로 결정할 일을 고민에 시간을 많이 쓰면 그 결과가 좋지 않았을 때는 더 큰 손해가 된다. 결과가 좋으면 그나마 다행이지만 좋은 결과 나올 일이면 왜 더 빨리 결정하지 못했을까 하는 아쉬움도 든다. 반대로 빠른 결정에서 실패를 하더라도 고민하는 시간만큼 감정과 결과를 추스릴 시간을 얻을 수 있다. 이 시간이 빠른 결정에서 얻을 수 있는 가장 큰 장점이다.

지금 당장 결정해야 하는 일이 아니라면 계속 생각하거나 고민하지 않고 결정해야 하는 날에 일정으로 등록해두고 잊는다. (최소한 메모를 해둔다.) 고민도 멀티테스킹을 요구하고 고민이 많아지면 정작 눈 앞에 있는 일에 집중하기가 어렵다. 아예 생각하지 않고 지내다가 결정의 순간에서 갖고 있는 데이터만으로 결론을 도출하면 망설임도 적어졌고, 또 단호하게 결정할 수 있었다.

여기까지가 불필요한 고민을 줄이기 위해 내 스스로 세운 원칙이다. 최근 들어서 글처럼 잘 지켜지지는 못하고 있다. 남은 2015년은 이 원칙을 잘 닦아 고민이란 탈을 쓰고서 결정 게으름을 피우지 않도록 경계해야겠다.


이상한모임에서 진행하는, 다양한 주제로 함께 글을 쓰는 글쓰기 소모임입니다. 함께 하고 싶다면 #weird-writing 채널로 오세요!

회사에서 동료나 클라이언트와 메일을 주고 받을 때가 많다. Gmail에도 내장된 철자 검사기가 있긴 하지만 철자만 고쳐주지 문법적인 부분을 고쳐주는 것은 아니라서 몇번이고 읽어보고 보내게 된다. 하지만 여전히 문법적으로 맞지 않거나 익숙한 단어를 계속 반복적으로 쓰게 되는 경향이 있다. 사내에서 주고 받는 메일이야 괜찮지만 클라이언트에게 보내는 메일이나 중요한 내용의 메일은 그렇게 보내게 될까봐 늘 민망해 하다가 grammarly를 사용하기 시작했다.

이 서비스를 결제하기 전에 ChattingCat과 grammarly 중 어느 서비스를 할 지 한참 고민했는데 ChattingCat은 건 당 지불하는 구조에 실시간이 아니기 때문에 불편할 것 같아 조금 퀄리티는 떨어지더라도 grammarly를 쓰기로 결정했다. (사실 이 결정에서 가장 큰 영향을 준 것은 70% 할인 이벤트였다.)

grammarly는 맞춤법 및 문법 검사를 돕는 서비스로 크롬 플러그인이 잘되어 있어 어디든 글을 입력하는 곳이라면 grammarly의 로고와 함께 맞춤법과 문법을 빠르게 검사할 수 있게 해준다. 기본적인 철자 검사는 무료로 사용할 수 있어서 몇 달 무료로 사용했었다. 무료로 사용할 때는 Critical Error와 Advanced 두가지로 첨삭해주고 Advanced 항목은 유료로 결제해야 보여준다. 유료로 결제해서 사용한 것은 최근 한 달 정도 지났는데 결제한 이후로는 Critical 또는 Advanced로 표시하지 않고 일괄로 처리해줘서 어떤 항목이 advanced로 지적하는 것인지 알 수 없다는 미묘한 단점이 있다. 그래도 동어를 반복하면 동의어 추천도 해주고 쉼표나 마침표 위치를 조정해주는 등 쉽게 놓칠 수 있는 부분을 잘 챙겨주는 편이다.

영어 실력이 정말 느는가에 대해서는 아직 잘 모르겠다. 대신 빠르게 점수로 피드백을 받을 수 있어서 겁없이 작성해보기에는 도움이 되는 기분이다. 하지만 기계 검사라서 그런지 가끔 어색한 표현을 맞다고 할 때가 있어서 조심해야 한다. 이런 경우에는 맞는 표현으로 안고쳐도, 고쳐도 100점으로 나온다.

사용하면서 특별하다고 느낀 점은 매주 리포트를 보내주는 부분인데 지난 주에 비해 얼마나 풍부한 어휘를 사용했는지, 잘못된 단어를 몇개 사용했는지 등 수치적인 피드백을 제공한다. 상세한 데이터도, 성장 지표로 삼기에도 아직 부족한 수준이긴 하지만 수치가 높아진다는 것은 그래도 기분이 좋은 일이다. 그로스 해킹으로 사용자 경험을 돕는 예라고 봐야 할 것 같다.

언어와 관련된 서비스를 사용하다보면 자국어만 구사해도 문제 없을 정도로 소통의 저변이 확장되는 것을 느낄 수 있다. 기계학습, 빅데이터 연구로 많은 혜택을 보게 될 분야기도 하고, 구글이 인덱싱한 자료로 타 언어 자료까지 연관 검색을 한다거나, 구글 번역을 제공하는 모습만 봐도 멀지 않은 기분이 든다.

얼마 전 제이펍 출판사 베타리더스 3기에 선정되었다. 선정 되자마자 <함수 프로그래밍 실천 기술>이란 제목의 책을 베타리딩하게 되었는데 함수형 프로그래밍에 대해 전반적인 내용과 세세한 개념을 Haskell로 설명하는 책이었다. 함수형 프로그래밍에 대한 책을 처음 읽어봐서 생소한 개념도 많았지만 다른 언어로의 비교 코드를 많이 제시하고 있어 전체적인 이해에 도움이 많이 되었다. 조만간 출간된다고 하니 관심이 있다면 제목을 적어두는 것도 좋겠다 🙂

함수형 언어를 얘기하면 모나드가 꼭 빠지지 않는다. 이 포스트는 Monads in JavaScript의 번역글이다. 이 글이 모나드에 대해 세세하게 모든 이야기를 다룬 것은 아니지만 모나드의 아이디어를 JavaScript로 구현해서 이 코드에 익숙하다면 좀 더 쉽게 접근할 수 있는 글이라 번역으로 옮겼다. 쉽게 이해하기 어렵지만 이해하면 정말 강력하다는 모나드를 이 글을 통해 조금이나마 쉽게 이해하는데 도움이 되었으면 좋겠다.


모나드는 순서가 있는 연산을 처리하는데 사용하는 디자인 패턴이다. 모나드는 순수 함수형 프로그래밍 언어에서 부작용을 관리하기 위해 광범위하게 사용되며 복합 체계 언어에서도 복잡도를 제어하기 위해 사용된다.

모나드는 타입으로 감싸 빈 값을 자동으로 전파하거나(Maybe 모나드) 또는 비동기 코드를 단순화(Continuation 모나드) 하는 등의 행동을 추가하는 역할을 한다.

모나드를 고려하고 있다면 코드의 구조가 다음 세가지 조건을 만족해야 한다.

  1. 타입 생성자 – 기초 타입을 위한 모나드화된 타입을 생성하는 기능. 예를 들면 기초 타입인 number를 위해 Maybe<number> 타입을 정의하는 것.
  2. unit 함수 – 기초 타입의 값을 감싸 모나드에 넣음. Maybe 모나드가 number 타입인 값 2를 감싸면 타입 Maybe<number>의 값 Maybe(2)가 됨.
  3. bind 함수 – 모나드 값으로 동작을 연결하는 함수.

다음의 TypeScript 코드가 이 함수의 일반적인 표현이다. M은 모나드가 될 타입으로 가정한다.

interface M<T> {

}

function unit<T>(value: T): M<T> {
    // ...
}

function bind<T, U>(instance: M<T>, transform: (value: T) => M<U>): M<U> {
    // ...
}

Note: 여기에서의 bind 함수는 Function.prototype.bind 함수와 다르다. 후자의 bind는 ES5부터 제공하는 네이티브 함수로 부분 적용한 함수를 만들거나 함수에서 this 값을 바꿔 실행할 때 사용하는 함수다.

JavaScript와 같은 객체지향 언어에서는 unit 함수는 생성자와 같이 표현될 수 있고 bind 함수는 인스턴스의 메소드와 같이 표현될 수 있다.

interface MStatic<T> {
    new(value: T): M<T>;
}

interface M<T> {
    bind<U>(transform: (value: T) => M<U>):M<U>;
}

또한 여기에서 다음 3가지 모나드 법칙을 준수해야 한다.

  1. bind(unit(x), f) ≡ f(x)
  2. bind(m, unit) ≡ m
  3. bind(bind(m, f), g) ≡ bind(m, x => bind(f(x), g))

먼저 앞 두가지 법칙은 unit이 중립적인 요소라는 뜻이다. 세번째 법칙은 bind는 결합이 가능해야 한다는 의미로 결합의 순서가 문제가 되서는 안된다는 의미다. 이 법칙은 덧셈에서 확인할 수 있는 법칙과 같다. 즉, (8 + 4) + 2의 결과는 8 + (4 + 2)와 같은 결과를 갖는다.

아래의 예제에서는 화살표 함수 문법을 사용하고 있다. Firefox (version 31)는 네이티브로 지원하고 있지만 Chrome (version 36)은 아직 지원하지 않는다.

Identity 모나드

identity 모나드는 가장 단순한 모나드로 값을 감싼다. Identity 생성자는 앞서 살펴본 unit과 같은 함수를 제공한다.

function Identity(value) {
  this.value = value;
}

Identity.prototype.bind = function(transform) {
  return transform(this.value);
};

Identity.prototype.toString = function() {
  return 'Identity(' + this.value + ')';
};

다음 코드는 덧셈을 Identity 모나드를 활용해 연산하는 예시다.

var result = new Identity(5).bind(value =>
                 new Identity(6).bind(value2 =>
                     new Identity(value + value2)));

Maybe 모나드

Maybe 모나드는 Identity 모나드와 유사하게 값을 저장할 수 있지만 어떤 값도 있지 않은 상태를 표현할 수 있다.

Just 생성자가 값을 감쌀 때 사용된다.

function Just(value) {
  this.value = value;
}

Just.prototype.bind = function(transform) {
  return transform(this.value);
};

Just.prototype.toString = function() {
  return 'Just(' + this.value + ')';
};

Nothing은 빈 값을 표현한다.

var Nothing = {
  bind: function() {
    return this;
  },
  toString: function() {
    return 'Nothing';
  }
};

기본적인 사용법은 identity 모나드와 유사하다.

var result = new Just(5).bind(value =>
                 new Just(6).bind(value2 =>
                      new Just(value + value2)));

Identity 모나드와 주된 차이점은 빈 값의 전파에 있다. 중간 단계에서 Nothing이 반환되면 연관된 모든 연산을 통과하고 Nothing을 결과로 반환하게 된다.

다음 코드에서는 alert가 실행되지 않게 된다. 그 전 단계에서 빈 값을 반환하기 때문이다.

var result = new Just(5).bind(value =>
                 Nothing.bind(value2 =>
                      new Just(value + alert(value2))));

이 동작은 수치 표현에서 나타나는 특별한 값인 NaN(not-a-number)과도 유사하다. 결과 중간에 NaN 값이 있다면 NaN은 전체 연산에 전파된다.

var result = 5 + 6 * NaN;

Maybe 모나드는 null 값에 의한 에러가 발생하는 것을 막아준다. 다음 코드는 로그인 사용자의 아바타를 가져오는 예시다.

function getUser() {
  return {
    getAvatar: function() {
      return null; // 아바타 없음
    }
  }
}

빈 값을 확인하지 않는 상태로 메소드를 연결해 호출하면 객체가 null을 반환할 때 TypeErrors가 발생할 수 있다.

try {
  var url = getUser().getAvatar().url;
  print(url); // 여기는 절대 실행되지 않음
} catch (e) {
  print('Error: ' + e);
}

대안적으로 null인지 확인할 수 있지만 이 방법은 코드를 장황하게 만든다. 코드는 틀리지 않지만 한 줄의 코드가 여러 줄로 나눠지게 된다.

var url;
var user = getUser();
if (user !== null) {
  var avatar = user.getAvatar();
  if (avatar !== null) {
    url = vatar.url;
  }
}

다른 방식으로 작성할 수 있을 것이다. 비어 있는 값을 만날 때 연산이 정지하도록 작성해보자.

function getUser() {
  return new Just({
    getAvatar: function() {
      return Nothing; // 아바타 없음
    }
  });
}

var url = getUser()
            .bind(user => user.getAvatar())
            .bind(avatar => avatar.url);

if(url instanceof Just) {
  print('URL has value: ' + url.value);
} else {
  print('URL is empty');
}

List 모나드

List 모나드는 값의 목록에서 지연된 연산이 가능함을 나타낸다.

이 모나드의 unit 함수는 하나의 값을 받고 그 값을 yield하는 generator를 반환한다. bind 함수는 transform 함수를 목록의 모든 요소에 적용하고 그 모든 요소를 yield 한다.

function* unit(value) {
  yield value;
}

function* bind(list, transform) {
  for (var item of list) {
    yield* transform(item);
  }
}

배열과 generator는 이터레이션이 가능하며 그 반복에서 bind 함수가 동작하게 된다. 다음 예제는 지연을 통해 각각 요소의 합을 만드는 목록을 어떻게 작성하는지 보여준다.

var result = bind([0, 1, 2], function (element) {
  return bind([0, 1, 2], function* (element2) {
    yield element + element2;
  });
});

for (var item of result) {
  print(item);
}

다음 글은 다른 어플리케이션에서 JavaScript의 generator를 어떻게 활용하는지 보여준다.

Continuation 모나드

Continuation 모나드는 비동기 일감에서 사용한다. ES6에서는 다행히 직접 구현할 필요가 없다. Promise 객체가 이 모나드의 구현이기 때문이다.

  1. Promise.resolve(value) 값을 감싸고 pormise를 반환. (unit 함수의 역할)
  2. Promise.prototype.then(onFullfill: value => Promise) 함수를 인자로 받아 값을 다른 promise로 전달하고 promise를 반환. (bind 함수의 역할)

다음 코드에서는 Unit 함수로 Promise.resolve(value)를 활용했고, Bind 함수로 Promise.prototype.then을 활용했다.

var result = Promise.resolve(5).then(function(value) {
  return Promise.resolve(6).then(function(value2) {
      return value + value2;
  });
});

result.then(function(value) {
    print(value);
});

Promise는 기본적인 continuation 모나드에 여러가지 확장을 제공한다. 만약 then이 promise 객체가 아닌 간단한 값을 반환하면 이 값을 Promise 처리가 완료된 값과 같이 감싸 모나드 내에서 사용할 수 있다.

두번째 차이점은 에러 전파에 대해 거짓말을 한다는 점이다. Continuation 모나드는 연산 사이에서 하나의 값만 전달할 수 있다. 반면 Promise는 구별되는 두 값을 전달하는데 하나는 성공 값이고 다른 하나는 에러를 위해 사용한다. (Either 모나드와 유사하다.) 에러는 then 메소드의 두번째 콜백으로 포착할 수 있으며 또는 이를 위해 제공되는 특별한 메소드 .catch를 사용할 수 있다.

Promise 사용과 관련된 기사는 다음과 같다:

Do 표기법

Haskell은 모나드화 된 코드를 작업하는데 도움을 주기 위해 편리 문법(syntax sugar)인 do 표기법을 제공하고 있다. do 키워드와 함께 시작된 구획은 bind 함수를 호출하는 것으로 번역이 된다.

ES6 generator는 do 표기법을 JavaScript에서 간단하고 동기적으로 보이는 코드로 작성할 수 있게 만든다.

전 예제에서는 maybe 모나드가 다음과 같이 직접 bind를 호출했었다.

var result = new Just(5).bind(value =>
                 new Just(6).bind(value2 =>
                     new Just(value + value2)));

다음은 같은 코드지만 generator를 활용했다. 각각의 호출은 yield로 모나드에서 값을 받는다.

var result = doM(function*() {
  var value = yield new Just(5);
  var value2 = yield new Just(6);
  return new Just(value + value2);
});

이 작은 순서를 generator로 감싸고 그 뒤에 bind를 값과 함께 호출해 yield로 넘겨준다.

function doM(gen) {
  function step(value) {
    var result = gen.next(value);
    if (result.done) {
      return result.value;
    }
    return result.value.bind(step);
  }
  return step();
}

이 방식은 다른 Continuation 모나드와 같은 다른 모나드에서도 사용할 수 있다.

Promise.prototype.bind = Promise.prototype.then;

var result = doM(function*() {
  var value = yield Promise.resolve(5);
  var value2 = yield Promise.resolve(11);
  return value + value2;
}());

result.then(print);

다른 모나드와 같은 방식으로 동작하도록 thenbind로 별칭을 붙였다.

promise에서 generator를 사용하는 예는 Easy asynchrony with ES6를 참고하자.

연결된 호출 Chained calls

다른 방식으로 모나드화 된 코드를 쉽게 만드는 방법은 Proxy를 활용하는 것이다.

다음 함수는 모나드 인스턴스를 감싸 proxy 객체를 반환한다. 이 객체는 값이 있는지 없는지 확인되지 않은 프로퍼티라도 안전하게 접근할 수 있게 만들고 모나드 내에 있는 값을 함수에서 활용할 수 있게 돕는다.

function wrap(target, unit) {
  target = unit(target);
  function fix(object, property) {
    var value = object[property];
    if (typeof value === 'function') {
      return value.bind(object);
    }
    return value;
  }
  function continueWith(transform) {
    return wrap(target.bind(transform), unit);
  }
  return new Proxy(function() {}, {
    get: function(_, property) {
      if(property in target) {
        return fix(target, property);
      }
      return continueWith(value => fix(value, property));
    },
    apply: function(_, thisArg, args) {
      return continueWith(value => value.apply(thisArg, args));
    }
  });
}

이 래퍼는 빈 객체를 참조할 가능성이 있는 경우에 안전하게 접근하는 방법을 제공한다. 이 방식은 실존적 연산자(?.) 구현 방식과 동일하다.

function getUser() {
  return new Just({
    getAvatar: function() {
      return Nothing; // 아바타 없음
    }
  });
}

var unit = value => {
  // 값이 있다면 Maybe 모나드를 반환
  if (value === Nothing || value instanceof Just) {
    return value;
  }
  // 없다면 Just를 감싸서 반환
  return new Just(value);
}

var user wrap(getUser(), unit);

print(user.getAvatar().url);

아바타는 존재하지 않지만 url을 호출하는 것은 여전히 가능하며 빈 값을 반환 받을 수 있다.

동일한 래퍼를 continuation 모나드에서 일반적인 함수를 실행할 때에도 활용할 수 있다. 다음 코드는 특정 아바타를 가지고 있는 친구가 몇명이나 있는지 반환한다. 예제는 보이기엔 모든 데이터를 메모리에 올려두고 사용하는 것 같지만 실제로는 비동기적을 데이터를 가져온다.

Promise.prototype.bind = Promise.prototype.then;

function User(avatarUrl) {
  this.avatarUrl = avatarUrl;
  this.getFriends = function() {
    return Promise.resolve([
      new User('url1'),
      new User('url2'),
      new User('url11'),
    ]);
  }
}

var user = wrap(new User('url'), Prommise.resolve);

var avatarUrls = user.getFriends().map(u => u.avatarUrl);

var length = avatarUrls.filter(url => url.contains('1')).length;

length.then(print);

여기서 모든 프로퍼티의 접근과 함수의 호출은 간단하게 값을 반환하는 것이 아니라 모나드 안으로 진입해 Promise를 실행해 결과를 얻게 된다.

ES6의 Proxies에 대한 자세한 내용은 Array slices를 참고하자.


원본 포스트 https://curiosity-driven.org/monads-in-javascript (CC BY 3.0)

색상을 바꿔요

눈에 편한 색상을 골라보세요 :)

Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.