← Back to list

Crash Course: Behavioral Programming in Clojure with core.async

Leveraging core.async for Modular and Incremental Development in Clojure

Eugenii Shevchenko · 2024-09-27 21:06 · 105 claps · 6.2 min read
#clojure #core-async #behavioral-programming #concurrency #event-driven-architecture
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval 💻 · Programming 🏛️ · Architecture

Crash Course: Behavioral Programming in Clojure with core.async

Photo by Diane Picchiottino on Unsplash

Photo by Diane Picchiottino on Unsplash

Leveraging core.async for Modular and Incremental Development in Clojure

Behavioral Programming (BP) is a programming paradigm that enables developers to build complex systems incrementally by specifying independent behaviors that are composed at runtime. In Clojure, the core.async library provides powerful constructs for implementing BP concepts, leveraging channels and lightweight processes for asynchronous communication.

This crash course will guide you through:

  1. Understanding Behavioral Programming concepts.
  2. Implementing BP constructs using core.async.
  3. Building a detailed example application to illustrate BP in action.

Introduction to Behavioral Programming

Behavioral Programming allows developers to define system behaviors incrementally by specifying independent behavioral threads (b-threads). Each b-thread:

  • Requests events: Signals events it wants to occur.
  • Waits for events: Pauses execution until certain events happen.
  • Blocks events: Prevents certain events from occurring.

At runtime, an event selection mechanism coordinates these b-threads, deciding which events to execute based on their requests and blocks.

Benefits of BP:

  • Incremental Development: Add new behaviors without altering existing ones.
  • Separation of Concerns: Each behavior is specified independently.
  • Natural Mapping: Aligns with human reasoning about system behavior.

Implementing Behavioral Programming Constructs

In Clojure, we can model BP constructs using core.async by defining:

  • Events: Represented as immutable data structures.
  • b-threads: Implemented as go blocks that interact via channels.
  • Event Selection Mechanism: Coordinates event execution based on b-thread interactions.

Events

Events are fundamental units in BP, representing actions or occurrences. We’ll define events as maps containing a :type and optional :data.

(defn create-event [type & [data]]
  {:type type :data data})

Example Events:

(def events
  {:green-light       (create-event :green-light)
   :yellow-light      (create-event :yellow-light)
   :red-light         (create-event :red-light)
   :pedestrian-wait   (create-event :pedestrian-wait)
   :pedestrian-cross  (create-event :pedestrian-cross)
   :emergency-vehicle (create-event :emergency-vehicle)})

Behavioral Threads (b-threads)

Each b-thread encapsulates a specific behavior and interacts with the system via channels.

Key Components:

  • Request Channel: b-threads send event requests here.
  • Event Channel: b-threads receive selected events here.

b-thread Structure:

(defn b-thread [name request-ch event-ch]
  (async/go-loop []
    ;; Behavior implementation
    (recur)))

Event Selection Mechanism

The event selection mechanism orchestrates event execution by:

  • Collecting event requests and blocks from all b-threads.
  • Determining allowable events (requested but not blocked).
  • Selecting an event to execute.
  • Broadcasting the selected event to all b-threads.

Implementation Overview:

  • Coordinator Loop: Continuously processes requests and selects events.
  • State Management: Maintains requested and blocked events.

Example Application: Traffic Light Controller

We’ll build a traffic light controller that manages:

  • Standard traffic light cycles.
  • Pedestrian crossing requests.
  • Emergency vehicle overrides.

Defining Events

Using the create-event function, define all necessary events:

(def events
  {:green-light       (create-event :green-light)
   :yellow-light      (create-event :yellow-light)
   :red-light         (create-event :red-light)
   :pedestrian-wait   (create-event :pedestrian-wait)
   :pedestrian-cross  (create-event :pedestrian-cross)
   :emergency-vehicle (create-event :emergency-vehicle)})

Implementing b-threads

We’ll implement several b-threads, each responsible for a specific aspect of the traffic light system.

3.1 Traffic Light Sequence

This b-thread handles the normal cycling of traffic lights.

(defn traffic-light-sequence [request-ch event-ch]
  (async/go-loop []
    ;; Request green light
    (async/>! request-ch {:request #{(:green-light events)}
                          :block #{(:pedestrian-cross events)
                                   (:emergency-vehicle events)}})
    ;; Wait for green light event
    (let [event (async/<! event-ch)]
      (when (= (:type event) :green-light)
        (println "Traffic Light: Green")
        (async/<! (async/timeout 5000))  ; Green light duration

        ;; Request yellow light
        (async/>! request-ch {:request #{(:yellow-light events)}
                              :block #{}})
        (let [event (async/<! event-ch)]
          (when (= (:type event) :yellow-light)
            (println "Traffic Light: Yellow")
            (async/<! (async/timeout 2000))  ; Yellow light duration

            ;; Request red light
            (async/>! request-ch {:request #{(:red-light events)}
                                  :block #{}})
            (let [event (async/<! event-ch)]
              (when (= (:type event) :red-light)
                (println "Traffic Light: Red")
                (async/<! (async/timeout 5000))  ; Red light duration
                (recur)))))))))

Explanation:

  • Requests: The b-thread requests the next traffic light color.
  • Blocks: It may block events like :pedestrian-cross or :emergency-vehicle depending on the current state.
  • Timing: Uses async/timeout to simulate the duration of each light.

3.2 Pedestrian Crossing Request

This b-thread handles pedestrian requests and manages the crossing sequence.

(defn pedestrian-crossing [request-ch event-ch]
  (async/go-loop []
    ;; Simulate pedestrian wait button press
    (async/<! (async/timeout (rand-int 10000)))
    (println "Pedestrian: Wait button pressed")
    (async/>! request-ch {:request #{(:pedestrian-wait events)}
                          :block #{}})
    (let [event (async/<! event-ch)]
      (when (= (:type event) :pedestrian-wait)
        ;; Block green light and request pedestrian cross
        (async/>! request-ch {:request #{(:pedestrian-cross events)}
                              :block #{(:green-light events)}})
        (let [event (async/<! event-ch)]
          (when (= (:type event) :pedestrian-cross)
            (println "Pedestrian: Crossing")
            (async/<! (async/timeout 5000))  ; Crossing duration
            ;; Unblock green light
            (async/>! request-ch {:request #{}
                                  :block #{}})
            (println "Pedestrian: Crossed")
            (recur)))))))

Explanation:

  • Requests: Upon button press, requests :pedestrian-wait and then :pedestrian-cross.
  • Blocks: Blocks :green-light during pedestrian crossing.
  • Simulated Timing: Random delay simulates unpredictable pedestrian requests.

3.3 Emergency Vehicle Override

This b-thread simulates emergency vehicles that need immediate passage.

(defn emergency-vehicle-override [request-ch event-ch]
  (async/go-loop []
    ;; Simulate emergency vehicle detection
    (async/<! (async/timeout (+ 15000 (rand-int 10000))))
    (println "Emergency Vehicle: Detected")
    (async/>! request-ch {:request #{(:emergency-vehicle events)}
                          :block #{}})
    (let [event (async/<! event-ch)]
      (when (= (:type event) :emergency-vehicle)
        ;; Force green light, block other events
        (async/>! request-ch {:request #{(:green-light events)}
                              :block (disj (set (vals events)) (:green-light events))})
        (let [event (async/<! event-ch)]
          (when (= (:type event) :green-light)
            (println "Emergency Vehicle: Passing through")
            (async/<! (async/timeout 5000))  ; Emergency passage duration
            ;; Unblock events
            (async/>! request-ch {:request #{}
                                  :block #{}})
            (println "Emergency Vehicle: Passed")
            (recur)))))))

Explanation:

  • Requests: Signals :emergency-vehicle event, then forces :green-light.
  • Blocks: Blocks all other events except :green-light during emergency.
  • Timing: Simulates emergencies occurring at random intervals.

3.4 Event Logger

This b-thread logs all events for monitoring purposes.

(defn event-logger [event-ch]
  (async/go-loop []
    (let [event (async/<! event-ch)]
      (println "Event occurred:" (:type event))
      (recur))))

Event Selection and Coordination

The event selection mechanism coordinates the execution of events based on b-thread interactions.

(defn event-selection-mechanism [b-threads]
  (let [request-ch (async/chan)
        event-ch (async/chan)
        event-mult (async/mult event-ch)]  ;; Create a mult for event broadcasting
    ;; Start all b-threads, connecting each to a tap of the mult
    (doseq [bt b-threads]
      (let [bt-event-ch (async/chan)]  ;; Create a new channel for each b-thread
        (async/tap event-mult bt-event-ch)  ;; Connect the b-thread to the mult
        (bt request-ch bt-event-ch)))  ;; Pass individual channels to b-threads

    (async/go-loop [requested-events #{}
                    blocked-events #{}]
      ;; Collect requests from b-threads
      (let [request (async/<! request-ch)]
        (let [new-requested-events (into requested-events (:request request))
              new-blocked-events   (into blocked-events (:block request))]
          ;; Determine allowable events
          (let [allowable-events (clojure.set/difference new-requested-events new-blocked-events)]
            (if (empty? allowable-events)
              ;; No events can occur; wait for more requests
              (recur new-requested-events new-blocked-events)
              (let [selected-event (first allowable-events)]
                ;; Broadcast selected event to all b-threads via the mult
                (async/>! event-ch selected-event)
                ;; Reset requests and blocks
                (recur #{} #{})))))))))

The *event-ch channel is"multiplied" using `async/mult`*. This allows us to broadcast events to all b-threads.

We use *async/tap to create a separate event channel (`bt-event-ch`*) for each b-thread, ensuring that every b-thread gets the event when it occurs.

Explanation:

  • Request Handling: Collects :request and :block sets from b-threads.
  • Event Selection: Chooses an event that’s requested and not blocked.
  • Broadcasting: Sends the selected event to all b-threads via event-ch.
  • State Reset: After event execution, resets the requested and blocked events.

Running the Application

Integrate all components in the -main function:

(defn -main []
  (let [b-threads [traffic-light-sequence
                   pedestrian-crossing
                   emergency-vehicle-override
                   event-logger]]
    (event-selection-mechanism b-threads)
    ;; Keep the main thread alive
    (async/<!! (async/timeout 60000))  ; Run for 60 seconds
    (println "Simulation ended")))

Execution Steps:

  1. Initialize b-threads: Start all behavioral threads.
  2. Start Event Selection: Begin coordinating events.
  3. Run Simulation: Let the system run for a specified duration.
  4. Termination: Cleanly exit after the simulation time elapses.

The program will output event occurrences and state changes, such as:

Traffic Light: Green
Event occurred: :green-light
Event occurred: :green-light
Traffic Light: Yellow
Event occurred: :yellow-light
Traffic Light: Red
Event occurred: :red-light
Pedestrian: Wait button pressed
Event occurred: :pedestrian-wait
Pedestrian: Crossing
Event occurred: :pedestrian-cross
Pedestrian: Crossed
Emergency Vehicle: Detected
Event occurred: :emergency-vehicle
Emergency Vehicle: Passing through
Event occurred: :green-light
Emergency Vehicle: Passed
...
Simulation ended

4. Conclusion

In this crash course, we’ve:

  • Explored BP Concepts: Understood how Behavioral Programming works.
  • Implemented BP Constructs: Used core.async to model events, b-threads, and the event selection mechanism.
  • Built a Traffic Light Controller: Created a detailed example handling multiple behaviors.

By leveraging core.async, we can implement BP patterns that allow for modular, incremental development of complex systems. Each b-thread operates independently, yet the system behavior emerges from their coordination.

5. Further Reading

Note: This example is simplified for educational purposes. In a production environment, consider:

  • Error Handling: Implement robust error and exception management.
  • Performance Optimization: Optimize channel usage and state management.
  • Scalability: Ensure the system scales with additional behaviors and increased complexity.
  • Testing: Write comprehensive tests for individual b-threads and the system as a whole.

By mastering these concepts, you can harness the full potential of Behavioral Programming in Clojure to build sophisticated, maintainable systems.


메타데이터
post_id
07ed06ddd760
slug
crash-course-behavioral-programming-in-clojure-with-core-async-07ed06ddd760
url
https://medium.com/@eugenesh4work/crash-course-behavioral-programming-in-clojure-with-core-async-07ed06ddd760
canonical_url
https://medium.com/@eugenesh4work/crash-course-behavioral-programming-in-clojure-with-core-async-07ed06ddd760
author_url
https://medium.com/@eugenesh4work
status
ok
fetched_at
2026-08-28 17:21:34