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

Date: 2023-08-11 Fri 00:00

Author: Younghwan Nam

Created: 2024-05-02 Thu 03:16

Emacs 27.2 (Org mode 9.4.4)

Validate