[230811] core.async - 02 - based on youtube video
Table of Contents
1 주의
이번 장은 아래 영상을 기반으로 제작했으며, 코드나 그런 것들은 모두 영상의 예시가 기반이다. 이번 core.async 챕터는 정리가 아니라 영상에 대해 본 것들에 대한 기록에 가깝다. 이 영상은 현재 9년이 되어간다. 코드의 출력값은 지금과 다르다.
2 iocmacro
core.async를 공부하게 된 이유가 이곳에 있다.
iocmacro는 clojure macro의 정점을 보여준다.
iocmacro는 go 매크로를 필두로 go 매크로 안에 있는 표현식을 상태머신으로 바꿔서 수행시킨다.
즉, 좀 더 간단하게 이야기 자나면 go 매크로 안에 <! >! 는 put! take! 로 바뀌면서 저절로 콜백형식의 코드를 지원하게 하기 위해서 많은 일을 한다.
나는 매크로를 제대로 이해하기 위해 core.match를 만들어보는 시간을 가졌었는데 이번에 core.async만 제대로 내부를 이해하게 된다면 좋겠다.
3 상태머신 인덱스
;; state machine function (def ^:const FN-IDX 0) ;; maybe next(or current) block (to run) (def ^:const STATE-IDX 1) ;; return value (def ^:const VALUE-IDX 2) (def ^:const BINDINGS-IDX 3) (def ^:const EXCEPTION-FRAMES 4) (def ^:const CURRENT-EXCEPTION 5) (def ^:const USER-START-IDX 6)
3.1 FN-IDX
state machine function 자체를 저장함. (그래야 재귀를 돌거라서)
3.2 STATE-IDX
next state. start with 1
이후 다른 숫로 계속 바뀐다.
block id 와 같고 어디로 jmp 할지 결정한다.
3.3 VALUE-IDX
result of recently run expression
3.4 BINDINGS-IDX
var-binding 값이 있는 곳.
3.5 EXCEPTION-FRAMES & CURRENT-EXCEPTION
예전 방식은 EXCEPTION을 받아서 그냥 스택을 버리고 예외를 핸들링.
변경된 방식은 예외를 다시 state machine으로 만들어서 state machine 안에 state machine을 넣음.
3.6 USER-START-IDX
you can define when you create state machine. how many slot available in your implementaion.
(ioc/state-machine body 0 &env terminators)
이곳은 유저가 정의할 수 있는 영역이다. go 매크로는 이 슬롯에 1을 세팅하며 리턴 채널을 저장한다. 그리고 go는 이 슬롯에 이동하여 :
[f# ~(ioc/state-machine `(do ~@body) 1 [crossing-env &env] ioc/async-custom-terminators)]
4 deep walking macro
(defmulti parse-item (fn [form ctx]
(cond
(seq? form) :seq
(integer? form) :int
(symbol? form) :symbol
(nil? form) :nil)))
(defmulti parse-sexpr (fn [[sym & rest] ctx]
sym))
(defmethod parse-sexpr 'if
[[_ test then else] ctx]
{:type :if
:test (parse-item test ctx)
:then (parse-item then ctx)
:else (parse-item else ctx)})
(defmethod parse-sexpr 'do
[[_ & body] ctx]
{:type :do
:body (doall (map (fn [x] (parse-item x ctx))
body))})
(defmethod parse-sexpr :default
[[f & body] ctx]
{:type :call
:fn (parse-item f ctx)
:args (doall (map (fn [x] (parse-item x ctx))
body))})
(defmethod parse-item :seq
[form ctx]
(let [form (macroexpand form)]
(parse-sexpr form ctx)))
(defmethod parse-item :int
[form ctx]
(swap! ctx inc)
{:type :int
:value form})
(defmethod parse-item :symbol
[form ctx]
{:type :symbol
:value form})
(defmethod parse-item :nil
[form ctx]
{:type :nil})
(defmacro to-ast [form]
(pr-str (parse-item form (atom 0))))
(defn r-assoc [k v m]
(assoc m k v))
(def add-me
(partial r-assoc :name "ME"))
(add-me {})
;; {:name "ME"}
(defn thread-it [& fns]
(fn [initial]
(reduce
(fn [acc f]
(f acc))
initial
fns)))
(defn add-personal-info []
(thread-it (partial r-assoc :name "ME")
(partial r-assoc :last-name "LAST")
(partial r-assoc :age 30)))
(defn add-job-info []
(thread-it (partial r-assoc :job "DEV")))
;; functional pure way with having worring about perhaps mutating state
((thread-it (add-personal-info)
(add-job-info))
{})
(defn assoc-in-plan
"Same as assoc-in, but for state hash map"
[path val]
(fn [plan]
[val (assoc-in plan path val)]))
(defmacro gen-plan
"Allows a user to define a state monad binding plan.
(gen-plan
[_ (assoc-in-plan [:foo :bar] 42)
val (get-in-plan [:foo :bar])]
val)"
[binds id-expr]
(let [binds (partition 2 binds)
psym (gensym "plan_")
forms (reduce
(fn [acc [id expr]]
(concat acc `[[~id ~psym] (~expr ~psym)]))
[]
binds)]
`(fn [~psym]
(let [~@forms]
[~id-expr ~psym]))))
(defn get-plan
"Returns the final [id state] from a plan. "
[f]
(f {}))
(defn all
"Assumes that itms is a list of state monad function results, threads the state map
through all of them. Returns a vector of all the results."
[itms]
(fn [plan]
(reduce
(fn [[ids plan] f]
(let [[id plan] (f plan)]
[(conj ids id) plan]))
[[] plan]
itms)))
((assoc-in-plan [:test] "Hello") {})
;; => ["Hello" {:test "Hello"}]
((gen-plan
;; associate into the context {:test "HELLO"}
;; assoc-v (vlaue) => "HELLO"
[assoc-v (assoc-in-plan [:test] "HELLO")]
(str "value is " assoc-v))
{})
;; => ["value is HELLO" {:test "HELLO"}]
#_((gen-plan
;; all -> seq of function -> single function
[assoc-v (for [x (range 5)]
(assoc-in-plan [:test]
"HELLO"))]
(str "value is " assoc-v))
{})
;; LazySeq cannot be cast to clojure.lang.IFn
((gen-plan
;; all -> seq of function -> single function
[assoc-v (all (for [x (range 5)]
(assoc-in-plan [:test] "HELLO")))]
(str "value is " assoc-v))
{})
;; => ["value is [\"HELLO\" \"HELLO\" \"HELLO\" \"HELLO\" \"HELLO\"]" {:test "HELLO"}]
이렇게 state-machine을 만드는 함수가 있다.
(defn parse-to-state-machine
"Takes an sexpr and returns a hashmap that describes the execution flow of the sexpr as
a series of SSA style blocks."
[body terminators]
(-> (gen-plan
[_ (push-binding :terminators terminators)
blk (add-block)
_ (set-block blk)
id (item-to-ssa body)
term-id (add-instruction (->Return id))
_ (pop-binding :terminators)]
term-id)
get-plan))
(parse-to-state-machine '[(if (= x 1) :true :false)] {})
[inst_2537
{:current-block 4,
:start-block 1,
:block-catches {4 nil, 3 nil, 2 nil, 1 nil},
:blocks
{4
[{:value :clojure.core.async.impl.ioc-macro/value, :id inst_2536}
{:value inst_2536, :id inst_2537}],
3 [{:value :false, :block 4, :id inst_2535}],
2 [{:value :true, :block 4 :id inst_2534}],
1 [{:refs [= x 1], :id inst_2532}
{:test inst_2532, :then-block 2, :else-block 3, :id inst_2533}]},
:block-id 4
:bindings {:terminators (), :locals ()}}]
;; state machine 하나의 함수처럼 동작할 것이고, 아래의 형태를 상상하면 쉽다.
;; 이런식으로 만들어진다.
;; 아주 비효율적인 코드지만 대부분의 경우 JVM최적화는 환상적이고,
;; 자주사용한다면 JIT이 동작하면서 엄청 빨라질 거라고 말한다.
(loop []
(let [result
(case (:block-id state)
1 (do (aset state : x)
(aset staet :block-id2)
:recur)
2 (...block 2...))]
(if (identical? result :recur)
(recur)
result)))
하지만 corea.sync 버전이 올라가면서 코드가 변경되었다.
[inst_16977
{:bindings {:terminators ()},
:block-id 1,
:blocks
{1
[{:ast [(if (= x 1) :true :false)],
:locals nil,
:id inst_16976}
{:value inst_16976,
:id inst_16977}]},
:block-catches {1 nil},
:start-block 1,
:current-block 1}]
다른 좀 더 복잡한 코드를 보자
(-> (parse-to-state-machine '[(loop [x 0]
(if (= x 5)
:done
(do (>! c x)
(recur (inc)))))] {} {})
(clojure.pprint/pprint))
;; =>
[inst_2746
{:current-block 3
:start-block 1
:block-catches {1 nil 2 nil 3 nil 4 nil 5 nil 6 nil}
:blocks {6
[{:values :clojure.core.async.impl.ioc-macro/value, :id inst_2743}
{:value inst_2743 :block 3 :id inst_2744}]
5
;; 3.1 inst_2734값을 c에 넣음. 리턴값은 inst_2739
;; 3.2 리턴값 inst_2739를 inc
;; 3.3 inc값을 inst_2740에 저장.
;; 3.4 재귀수행(recur) recur-nodes=inst_2734는 x를 말함.
;; 3.5 ids 는 다음 재귀를 돌 때 가자가야하는 데이터로 보임.
;; 3.6 이후 block 2로 감.
[{:refs [>! c inst_2734] :id inst_2739}
{:refs [inc] :id inst_2740}
{:recur-nodes [inst_2734] :ids [inst_2740] :id inst_2741}
{:value nil :block 2 :id inst_2738}]
4 [{:value :done :block 6 :id inst_2738}]
3 [{:value :clojure.core.async.impl.ioc-macros/value :id inst_2745}
{:value inst_2745 :id inst_2746}]
;; 2.1 (= x 5) 값을 inst_2736에 저장.
;; 2.2 test inst_2736 -> then-block 4번, else-block 5번 중에 감.
2 [{:refs [= inst_2734 5] :id inst_2736}
{:test inst_2736 :then-block 4 :else-block 5 :id inst_2737}]
;; 1. 여기서 시작.
;; 1.1 inst_2734(x) = 0
;; 1.2 jmp 2
1 [{:value 0 :id inst_2734}
{:value nil :block 2 :id inst_2735}]}
:block-id 6
:bindings {:recur-nodes (), :recur-point () :terminators () :locals ()}}]