[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 recentrly 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 ()}}]