m.render

m.render

m.render는 주어진 DOM 요소 내부에 DOM 트리를 생성한다. 만약 한 번 렌더링된 요소에 render를 재실행하면 m은 효율적으로 변경된 부위의 트리만을 재생성한다. 흥미로운 점은 m의 스마트diff는 커서위치라든지 포커스와 같은 속성을 변경하지 않기 때문에 사용자 인터렉션이 발생하는 중간에 일어나도 안전하다. 하지만, 때로는 이 알고리듬은 몇가지 키 속성을 수동으로 지정받아야 할 때가 있다.

links = 
    * title: 'item 1'
      url: '/item1'
    ...
m.render document.body,
  m \ul.nav, m \li, do
    link <~ links.map
    m \a do
      href: link.url
      config: m.route
    , link.title

포커스 딜링

가상 DOM 구분 알고리듬은 약점을 가지는데, 나이브한 비교는 각각의 DOM의 동일성을 인식하지 못한다는 것이다. 실제로 이는 리스트에서 맨 첫 아이템을 한칸 이동하는 것만으로 미스릴의 렌더링 엔진은 리스트 내의 모든 요소를 새로 생성하게 된다. (성능상의 이슈가 있을 수 있다.) 그외에도 리스트 내 특정 아이템을 삭제했을 때 포커스나 제3자 라이브러리의 특정 지정자가 엉뚱한 요소로 바뀔 수도 있다. 대신 미스릴은 특정 DOM에 속성값을 부여하면서 key라는 속성을 줄 수 있다. 이 키는 문서 내에서 유니크할 필요는 없고, 그 자신이 그의 다른 형제들과 구분되기만 하면 된다.

예를 들어 다음의 코드는, 일련의 input 박스를 만드는데, 각각의 DOM은 고유의 키를 가지고 있어서 입력중에라도 정렬이나 뒤집기가 발생하는 경우에 포커스를 올바로 유지할 수 있게 된다.

m \ul, items.map (item) -> m \li, key: item.id, m \inpu

서브트리 디렉티브

m.render는 가상 DOM 트리내의 노드로서 저수준의 서브트리 디렉티브를 채용한다. 만약 트리 노드가 아래와 같은 객체를 속성을 갖고 있다면, 미스릴은 해당 노드에 대해서는 diff 알고리듬을 더 이상 적용하지 않는다. 따라서 캐시된 카운터파트에 대비하여 가상 DOM을 재생성하지 않아서 개발자로 하여금 최적화를 수행하도록 한다.

{ subtree: 'retain' }

정리

  1. m.renderm.mount와 달리 자동으로 뷰 업데이트를 실행하지 않으며, 따라서 1회적인 뷰를 구성하기에 적합하다.
  2. 수동으로 리렌더링을 하는 경우 포커스 등과 같은 DOM 요소의 속성을 건드리지 않으므로 안전하다.
  3. 최적화를 위해서 특정 요소들은 그 형제 요소(siblings)와 구분할 수 있는 키를 제공할 수 있다.

project euler 38

오일러 프로젝트 38 번

숫자 192에 1, 2, 3을 각각 곱합니다.

    192 × 1 = 192
    192 × 2 = 384
    192 × 3 = 576

곱한 결과를 모두 이어보면 192384576 이고, 이것은 1 ~ 9 팬디지털(pandigital)인 숫자입니다. 이런 과정을 편의상 '곱해서 이어붙이기'라고 부르기로 합니다.

같은 식으로 9와 (1, 2, 3, 4, 5)를 곱해서 이어붙이면 918273645 라는 1 ~ 9 팬디지털 숫자를 얻습니다.

어떤 정수와 (1, 2, ... , n)을 곱해서 이어붙였을 때 얻을 수 있는 가장 큰 아홉자리의 1 ~ 9 팬디지털 숫자는 무엇입니까? (단 n > 1)

http://euler.synap.co.kr/prob_detail.php?id=38

n 을 증가시켜가면서 1, 2, 3 .. 9 와 곱한 결과들을 모아서 팬디지털이 되는지 검사한다. 즉 문제의 내용 그대로를 코딩하면 된다.

def test(n):
    s = ""
    for i in range(1, 10):
        s += str(i * n)
        if len(s) >= 9:
            break
    if "".join(sorted(s)) == "123456789":
        return s
    return None

def e038():
    a = []
    print(max(filter(lambda x:x is not None, (test(x) for x in range(1, 100000)))))

%time e038()
# 932718654
# Wall time: 645 ms

m.mount

m.mount

마운팅은 미스릴 컴포넌트를 DOM 트리로 렌더링하는 과정을 말한다. 하지만 m.mountm.render와 달리 이벤트 핸들러가 트리거될 때 자동으로 렌더링을 갱신하는 매커니즘을 포함한다. 만약 특정 요소가 URL에 따라서 언로드/재마운팅되게 하려면 m.route를 이용한다.

my-comp = 
  controller: -> greeting: \hello
  view: (ctrl) -> m \h1 ctrl.greeting
m.mount document.body, my-comp

m.mount()의 첫 인자는 컴포넌트가 마운트될 DOM요소이고, 두 번째 인자는 컴포넌트 객체이다. 실행이되면 먼저 컴포넌트의 컨트롤러 함수를 호출 한 후, (컴포넌트 함수의 리턴값이나 컴포넌트 함수로 생성한 객체를 이용해서) view 함수를 호출한다. 뷰 함수의 호출결과가 DOM을 치환하게 된다.

m.component

본격 mithril 탐구. 가독성을 위해서 본 문서에서는 LiveScript로 예제를 작성합니다.

m.component

미쓰릴의 컴포넌트는 보다 앱보다도 더 작은 단위의 요소를 작성할 수 있게 해준다. 실질적으로 DOM에 마운트되는 앱 역시 기본적으로는 컴포넌트에 해당한다.

my-component = do
  controller: (data) -> greeting: \Hello
  view: (ctrl) ->
    m \h1, ctrl.greeting

m.mount document.body, my-component

기본적으로 컴포넌트내의 컨트롤러는 view에서 사용하는 헬퍼 함수를 모은 객체를 생성해내는 함수라 보면 된다. (따라서 기본적으로는 앱의 컨트롤러는 완전히 숨겨지게된다.) 뷰는 기본적으로 컨트롤러(정확히는 컨트롤러 객체를 만드는 컨스트럭터)를 받을 수 있고, 컨트롤러 인스턴스가 노출하는 메소드나 객체를 사용할 수 있다. 만약 컨트롤러가 앱 외부의 데이터를 조작할 수 있다면, view 내에서도 이를 호출할 수 있다.

model = count: 0
my-component = do
  controller: (data) -> do
    increment: !-> model.count += 1
  view: (ctrl) ->
    m "a[href=javacript:;]", 
      onclick: ctrl.increment
    , "count: #{model.count}"
m.mount document.body, my-component
# 링크를 클릭할 때마다 카운트 값이 오른다.

컨트롤러와 뷰를 반드시 타이트하게 묶어야 하는 규칙은 사실 없다. 이들은 각각 작성되고, 마운트 시점에 말아서 넣어도 된다.

controller = (data) -> greeting: \hello
view = (ctrl) -> m \h1, ctrl.greeting
m.mount document.body, controller: controller, view: view

사실 뷰에서 참조할 동적 요소가 없다면, 컨트롤러는 없어도 된다.

my-component = view: -> m \h1, \hello
m.mount document.body, my-component

컨트롤러는 클래스 컨스트럭터일 수 있다. (보통은 이렇게 작성하지 않던가?)

my-component = do
  * controller: (data) !->
      @greeting = \hello
  view: (ctrl) -> m \h1, ctrl.greeting
m.mount document.body, my-component

view는 그외의 인자도 받을 수 있다. 컴포넌트 문법을 사용하면 된다.

my-component = do
  * controller: -> greeting: \hello
  view: (ctrl, args) -> m \h1, ctrl.greeting + ' ' + args.data

m.render document.body,
  * m my-component, data: \hello
  m.component my-component, data: \world

혹은

컴포넌트를 생성할 때 두 번째 이후의 인자가 있으면 이는 view, controller에 각각 바운드되어 전달되기도 한다.

my-component = do
  controller: (args, extras) ->
    console.log args.name, extras
    greeting: \hello
  view: (ctrl, args, extras) ->
    m \h1, "#{ctrl.greeting} #{args.name} #{extras}"

component = m.component my-component, {name: \world}, "this is a test"

ctrl = new component.controller!
# logs "world", "this is a test"

m.render document.body, component.view(ctrl)
# <body><h1>Hello world this is a test</h1></body>

이 때, controller 함수는 단 한번만 호출되며, view 함수는 바인딩된 데이터에 변경이 발생하여 뷰 업데이트가 되는 시점마다 호출된다.

정리하면

  1. 컴포넌트는 controller, view 두 개의 함수를 가지는 객체이다.
  2. 컨트롤러는 1)없어도 되며, 2) 데이터를 컨트롤하는 객체를 리턴하거나, 3)데이터 컨트롤러의 컨스트럭터이다. (이 경우는 리턴이 없다)
  3. view의 첫번째 인자는 반드시 컨트롤러 객체이다. (controller 함수의 리턴값이거나, 해당 함수로 생성한 객체)
  4. controllerview는 추가적인 인자를 받을 수 있으며, 해당 인자는 각각의 함수 호출시에 바인드되어 들어간다.

컴포넌트 네스팅

m.component() 를 이용하면 외부에 작성한 컴포넌트 객체를 특정 컴포넌트 내로 삽입할 수 있다. (반복적으로 쓰이는 요소라면 이 패턴을 사용하는 편이 편리하다.)

app = do
  view: ->
    m ".app",
      * m \h1, "My App"
      m.component my-component, {message: \hello}

my-component = do
  controller: (args) -> greeting: args.message
  view: (ctrl) -> m \h2, ctrl.greeting

m.mount document.body, app

이해가는지? 좀 더 복잡한 모양을 도전해보자.

app = do
  controller: -> data: [1 2 3]
  view: (ctrl) ->
    m \.app,
      * m "button[type=button]",
          onclick: !-> ctrl.data.reverse!
        , \MyApp
      item <~ ctrl.data.map
      m.component my-component, do
        message: "hello #{item}"
        key: item

my-component = do
  controller: (args) -> greeting: args.message
  view: (ctrl) -> m \h2, ctrl.greeting

m.mount document.body, app

가장 간단하게 앱을 구성하는 것은 하나의 객체에 컨트롤러와 뷰를 넣고 이 객체(이렇게 구성한 객체는 컴포넌트가 된다)를 특정 DOM에 마운트하는 것이다. 만약 앱의 크기가 커지거나 앱을 쪼개서 관리하고 싶다면 각각의 영역을 다시 컴포넌트로 분리한 다음, 한 컴포넌트에서 다른 컴포넌트를 m.component 혹은 m()을 이용해서 네스팅할 수 있다.

언로딩

만약 컴포넌트의 컨트롤러가 onunload라는 함수를 가지고 있다면, 아래 조건 중 하나에서 해당 함수가 호출된다.

  1. 컴포넌트가 가리키고 있는 DOM의 루트가 새로 마운트 될 때
  2. 라우팅이 변경될 때

이를 통해서 컴포넌트가 언로드되기 직전에 필요한 처리를 할 수 있다. 다음의 코드는 다른 주소로 라우트 될 때 변경사항을 저장하라는 경고를 표시할 수 있다.

comp = 
  controller: -> do
    @unsaved = m.prop no
    unsaved: @unsaved
    onunload: (e) !->
      if unsaved! then e.prevent-default!

이 케이스에서 미스릴은 주소가 변경되는 시점에 언로딩을 직접 후크하지 않기 때문에 언로드 시도를 통해서 이를 감지하도록 이벤트 핸들럴르 추가해주어야 한다.

window.onbeforeunload = -> if not (m.mount root-element, null) then "Are you sure want to leave?"

project euler 37

오일러 프로젝트 37 번

소수 3797에는 왼쪽부터 자리수를 하나씩 없애거나 (3797, 797, 97, 7) 오른쪽부터 없애도 (3797, 379, 37, 3) 모두 소수가 되는 성질이 있습니다.

이런 성질을 가진 소수는 단 11개만이 존재합니다. 이것을 모두 찾아서 합을 구하세요.

(참고: 2, 3, 5, 7은 제외합니다)

http://euler.synap.co.kr/prob_detail.php?id=37

조건에 맞는 소수들을 세면서 11개가 될때까지 검사를 수행해야 한다. 왼쪽에서 한자리씩 없애거나 오른쪽에서 한자리씩 없애는 것은 간단한 편인데 (상용로그를 이용하면 쉽다) 시간이 적지 않게 소모된다. 성능을 최적화하는 방법 몇 가지를 살펴보자.

  1. 숫자를 하나씩 제거해나가면 원래 값보다 계속 작아진다. 따라서 소수 판별함수는 메모이제이션한다.
  2. 문제의 조건에 의해 1의 자리는 3, 7 중 하나의 숫자만 올 수 있다. 이걸 미리 검사하면 수행시간이 절반으로 줄어든다.
  3. 마찬가지로 첫자리에는 2가 올 수 있지만, 그외 나머지 자리에는 짝수 숫자가 올 수 없다. 이 부분이 엄청나게 많은 경우를 제거한다.

2, 3의 제약 조건이 없을 때 약 6~7초 가량 걸리던 것이 저 제약 조건만으로 500ms대로 단축됐다.

from math import log10

def rFront(n):
    a = [n]
    while n > 0:
        l = 10**int(log10(n))
        n = n % l
        a.append(n)
    return a[1:-1]

def rRear(n):
    a = [n]
    while n > 0:
        n = n // 10
        a.append(n)
    return a[1:-1]

def memoize(f):
    cache = {}
    def wrapper(a):
        if a in cache:
            return cache[a]
        r = f(a)
        cache[a] = r
        return r
    return wrapper

@memoize
def is_prime(n):
    if n < 2:
        return False
    if n is 2 or n is 3:
        return True
    if n % 2 == 0 or n % 3 == 0:
        return False
    if n < 9:
        return True
    k = 5
    l = n ** 0.5
    while k <= l:
        if n % k == 0 or n % (k+2) == 0:
            return False
        k += 6
    return True


def check(n):

    if n % 10 not in (3, 7):
        return False

    s = str(n)[1:]
    for c in "02468":
        if c in s:
            return False

    if is_prime(n):
        for i in rFront(n):
            if not is_prime(i):
                return False
        for i in rRear(n):
            if not is_prime(i):
                return False
    else:
        return False
    return True

def e37():
    res = []
    a = 11
    c = 0
    while len(res) < 11:
        if check(a):
            res.append(a)
        a += [2, 4][c]
        c = (c + 1) % 2
    print(sum(res))

%time e37()
# 748317
# Wall time: 565 ms