core.typed 클로저 정적 타입 맛보기

클로저는 core.typed 라이브러리를 써서 정적 타입으로 프로그래밍 할 수 있습니다. 이 글에서는 간단한 예제로 클로저 정적 타입 맛 만 살짝 보겠습니다. :) 먼저 leiningen으로 calc라는 프로젝트를 만들어봅시다.

lein new calc

그리고 hello.core 네임스페이스에 숫자 두 개를 더하는 add 함수와 프로그램을 실행하는 -main 함수를 만들어 봅시다.

(ns calc.core)

(defn add [x y]
  (+ x y))

(defn -main []
  (println (add 3 4)))

lein run으로 프로그램을 실행해보기 위해서 project.clj :main에 다음과 같이 적고 실행해봅니다.

(defproject hello "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.8.0"]]
  :main hello.core)
lein run
7

잘 실행되네요. 이번에는 일부러 타입 에러를 만들어 보겠습니다. add 함수는 숫자 두 개를 받아야 하는데 숫자 하나와 문자열 하나를 넘겨 에러를 만들어 봅시다.

(ns calc.core)

(defn add [x y]
  (+ x y))

(defn -main []
  (println (add 3 "사")))

저장하고 실행해 봅시다.

lein run
...
Caused by: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
...

에러가 발생했네요. 실행 중에 에러가 난 걸까요? 혹시 컴파일 중에 에러가 난 걸까요? 패키징 해서 실행해보죠. project.clj uberjar :profiles에 다음과 같이 추가하고 calc.core에도 (:gen-class)를 추가해서 lein uberjar로 패키징 해봅시다.

(defproject calc "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.8.0"]]
  :main calc.core
  :profiles {:uberjar {:aot :all}})
(ns calc.core
  (:gen-class))

(defn add [x y]
  (+ x y))

(defn -main []
  (println (add 3 "사")))
lein uberjar
Created ~/calc/target/calc-0.1.0-SNAPSHOT.jar
Created ~/calc/target/calc-0.1.0-SNAPSHOT-standalone.jar

패키징이 잘되어 jar 파일이 생겼습니다. 이제 실행해 봅시다.

java -jar ./target/calc-0.1.0-SNAPSHOT-standalone.jar
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
	at clojure.lang.Numbers.add(Numbers.java:128)
	at calc.core$add.invokeStatic(core.clj:5)
	at calc.core$add.invoke(core.clj:4)
	at calc.core$_main.invokeStatic(core.clj:8)
	at calc.core$_main.invoke(core.clj:7)
	at clojure.lang.AFn.applyToHelper(AFn.java:152)
	at clojure.lang.AFn.applyTo(AFn.java:144)
	at calc.core.main(Unknown Source)

실행 중(런타임)에 에러가 발생하네요. 클로저는 기본적으로 동적 타입 언어기 때문에 컴파일할 때 타입 체크를 하지 않습니다. 보통 정적 언어들은 컴파일 타임에 타입 체크를 하는데요. 앞서 이야기한 것처럼 core.typed 라이브러리를 사용하면 클로저도 정적 타입을 쓸 수 있습니다.

project.clj core.typed 의존성을 추가하고 leiningen :injections 기능을 사용해서 컴파일 시 core.typed이 타입 체크를 하도록 설정해 봅시다.

(defproject calc "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/core.typed "0.3.32"]]
  :main calc.core
  :injections [(require 'clojure.core.typed)
               (clojure.core.typed/install)]
  :profiles {:uberjar {:aot :all}})

다음은 calc.core 네임스페이스가 타입 체크를 사용하도록 네임 스페이스에 메타 정보를 추가합니다. core.typed 라이브러리를 추가한다고 모든 네임스페이스가 정적 타입이 되는 것은 아닙니다. 필요한 네임스페이스만 정적 타입을 사용하도록 지정할 수 있습니다.

(ns calc.core
  {:lang :core.typed}
  (:gen-class))

(defn add [x y]
  (+ x y))

(defn -main []
  (println (add 3 "사")))

add 함수에 타입 정보를 적기 전에 패키징 해봅시다.

lein uberjar
Initializing core.typed ...
Building core.typed base environments ...
Finished building base environments
"Elapsed time: 3749.811981 msecs"
core.typed initialized.
Compiling calc.core
Type Error (calc/core.clj:6:3) Static method clojure.lang.Numbers/add could not be applied to arguments:


Domains:
	Long Long
	Double Double
	clojure.core.typed/AnyInteger clojure.core.typed/AnyInteger
	Number Number

Arguments:
	clojure.core.typed/Any clojure.core.typed/Any

Ranges:
	Long
	Double
	clojure.core.typed/AnyInteger
	Number


in: (clojure.lang.Numbers/add x y)


Type Checker: Found 1 error
...

에러가 발생했습니다. 아직 add에 대한 타입 적어 주지 않았는데 똑똑한 core.typed가 타입을 유추해서 에러를 낸 걸까요? 아닙니다. :) 클로저 코어 함수인 +는 숫자를 받을 수 있게 되어 있는데 xy의 타입이 지정되지 않았기 때문에 +에 넘길 수 없는 것입니다. (“사”를 4로 바꿔도 같은 에러가 발생합니다.)

그럼 에러가 나지 않도록 add 함수 x, y 인자 타입을 숫자로 지정해 봅시다.

(ns calc.core
  {:lang :core.typed}
  (:gen-class)
  (:require [clojure.core.typed :as t]))

(t/ann add [Number Number -> Number])
(defn add [x y]
  (+ x y))

(defn -main []
  (println (add 3 "사")))

core.typed/ann 구문으로 NumberNumber를 인자로 받아 Number를 리턴하는 함수라는 타입 정보를 add 함수에 지정했습니다.

다시 패키징 해보죠.

lein uberjar
Initializing core.typed ...
Building core.typed base environments ...
Finished building base environments
"Elapsed time: 3965.819169 msecs"
core.typed initialized.
Compiling calc.core
Type Error (calc/core.clj:11:12) Function calc.core/add could not be applied to arguments:


Domains:
	Number Number

Arguments:
	(t/Val 3) (t/Val "사")

Ranges:
	Number


in: (calc.core/add 3 "사")


Type Checker: Found 1 error
...

다른 에러가 발생했습니다. add 함수는 NumberNumber를 넘기게 되어 있는데 잘못된 타입을 사용했다는 에러가 나고 패키징이 실패했습니다. 그럼 "사"를 원래 대로 4로 고쳐서 다시 패키징 해봅시다.

(ns calc.core
  {:lang :core.typed}
  (:gen-class)
  (:require [clojure.core.typed :as t]))

(t/ann add [Number Number -> Number])
(defn add [x y]
  (+ x y))

(defn -main []
  (println (add 3 4)))
lein uberjar
Initializing core.typed ...
Building core.typed base environments ...
Finished building base environments
"Elapsed time: 4124.558431 msecs"
core.typed initialized.
Compiling calc.core
Created ~/calc/target/calc-0.1.0-SNAPSHOT.jar
Created ~/calc/target/calc-0.1.0-SNAPSHOT-standalone.jar

패키징이 잘 되네요. 실행도 해볼까요?

java -jar ./target/calc-0.1.0-SNAPSHOT-standalone.jar
7

잘 되네요. 간단히 알아본 것처럼 클로저는 core.typed로 선택적 타입 시스템을 사용할 수 있습니다. 더 자세한 기능은 core.typed 위키를 보세요.


Eunmin Kim

clojure, emacs