Tumgik
#consoleio
studiaries-blog · 7 years
Text
if i take a nap right now i can do my consoleio super quick when i wake up and start on graphic design.
2 notes · View notes
strujillo-blog1 · 7 years
Text
Understanding Clojure Protocols through UI
The last two weeks I've been learning Clojure, and spent some time the last few days trying to create a UI protocol without success.
In Clojure, a protocol is a set of method signatures (name, arguments) without implementation. By defining a protocol with defprotocol, a corresponding interface is automatically generated with matching methods. This interface allows for use of the protocol method signatures for instances of the interface. Instances of the interface provide the implementation, ultimately resulting in a polymorphic set of methods.
The Clojure documents provide the following protocol example:
(defprotocol P (foo [x]) (bar-me [x] [x y])) (deftype Foo [a b c] P (foo [x] a) (bar-me [x] b) (bar-me [x y] (+ c y))) (bar-me (Foo. 1 2 3) 42) = > 45
It seems simple enough, yet I couldn't get the protocol I wanted working. After some toiling, I came up with working code.
Below is not the exact code, but an isolated version of it. In complexity, I think it falls somewhere between the above code and the very specific examples I seem to find on Stack Overflow. (Perhaps my Clojure Google skills are not up to par.)
I created the code below in three stages to highlight basic behavior one might want from a protocol.
Level 1: Let’s build a console UI
The goal of this level is to create a user interface protocol and a console-specific implementation.
First, I created an app with Leiningen.
lein new app ui-protocol
The code I added to the core.clj shows what we eventually want:
Access to a user interface namespace,
A means to create our example-ui, and
A way to use example-ui to print "Hi." to the console.
Notice that ui/display-prompt takes the instance example-ui as an argument.
core.clj:
(ns ui-protocol.core (:gen-class) (:require [ui-protocol.user-interface :as ui])) (defn -main [] (let [example-ui (ui/create)] (ui/display-prompt example-ui "Hi.")))
From here, we start creating our protocol in the user interface namespace. First step, set up your defprotocol. In defprotocol, we'll create the footprint of methods we want. No implementation is provided.
Notice the x argument. That is the instance argument mentioned above. Each method will need this x argument, followed by any additional arguments you need for your eventual implementation. For example, in display-prompt we will need x and a message to display.
(I went ahead and also added get-input to the UI.)
With our protocol set up, we define a defrecord. This will contain the implementation we use for protocol methods. After setting up our arguments (currently none), we tell our defrecord to use the defprotocol we defined: UI.
As an aside, I've seen defrecord described as like a hashmap, but with its own class. According to the Clojure docs, defrecord generates compiled bytecode for the class with the given name, implementation, etc. defrecord has hash semantics like get, count. In addition to this though, defrecord allows for type-based polymorphism using the protocol/interface it implements. So, in the instance of our UI, you could have typed-based polymorphism through a two defrecords, ConsoleUI and BrowserUI, which both use the footprint of our defprotocol.
Our display-prompt and get-input methods will match the footprint in the protocol, except we also provide the desired implementation. In display-prompt, for example, we add (println message).
Finally, we add a method to create our instance of ConsoleUI: create. Now, we can create example-ui in our core.clj and use its methods.
ui_protocol.clj:
(ns ui-protocol.user-interface) (defprotocol UI (display-prompt [x message]) (get-input [x])) (defrecord ConsoleUI [] UI (display-prompt [x message] (println message)) (get-input [x] (read-line))) (defn create [] (map->ConsoleUI {}))
(map->ConsoleUI {}) is one way to create an instance of ConsoleUI with the information in {}. As we'll see later, {} may contain information from arguments. Note: I've since experimented and found (ConsoleUI.) in place of (map->ConsoleUI {}) works just as well here, and I think I'd pick that option going forward. If ConsoleUI had any arguments at this point, we'd use (ConsoleUI. arg), replacing arg with whatever the actual name is.
Level 1 code on GitHub.
Level 2: Let's use UI methods within the UI
So, we have two basic UI methods. Perhaps we'd like another method which combines the two. Our -main method will use ui/prompt-for-input instead of ui/display-prompt.
core.clj
(ns ui-protocol.core (:gen-class) (:require [ui-protocol.user-interface :as ui])) (defn -main [] (let [example-ui (ui/create)] (ui/prompt-for-input example-ui "Please enter a number: ")))
We add this method to our UI protocol and start to add it to our ConsoleUI, but now what? I updated x to this. In our prompt-for-input implementation, we can refer to internal methods with a dot (.display-prompt and .get-input) and pass this as an argument to the method.
Now we have a method that uses other internal methods to display a message to the user and retrieve input.
ui_protocol.clj
(ns ui-protocol.user-interface) (defprotocol UI (display-prompt [this message]) (get-input [this]) (prompt-for-input [this message])) (defrecord ConsoleUI [] UI (display-prompt [this message] (println message)) (get-input [this] (read-line)) (prompt-for-input [this message] (.display-prompt this message) (.get-input this))) (defn create [] (map->ConsoleUI {}))
Level 2 code on GitHub.
Level 3: Abstract that IO
We can go a little futher and abstract dependencies on println and read-line. In -main, we plan to use this abstraction with example-io. Note that we'll pass example-io to example-ui as an argument.
core.clj
(ns ui-protocol.core (:gen-class) (:require [ui-protocol.input-output :as io] [ui-protocol.user-interface :as ui])) (defn -main [] (let [example-io (io/create-console-io) example-ui (ui/create-console-ui example-io)] (ui/prompt-for-input example-ui "Please enter a number: ")))
We create a new defprotocol for input and output: IO. Our implementation in ConsoleIO will house the dependency on println and read-line.
input_output.clj
(ns ui-protocol.input-output) (defprotocol IO (display [this message]) (input [this])) (defrecord ConsoleIO [] IO (display [this message] (println message)) (input [this] (read-line))) (defn create-console-io [] (map->ConsoleIO {}))
Our UI, specifically, our ConsoleUI is modified to accept console-io as an argument (both the defrecord and creation method). Our input/output can now be used in ConsoleUI implementation.
user_input.clj
(ns ui-protocol.user-interface (:require [ui-protocol.input-output :as io])) (defprotocol UI (display-prompt [this message]) (get-input [this]) (prompt-for-input [this message])) (defrecord ConsoleUI [console-io] UI (display-prompt [this message] (io/display console-io message)) (get-input [this] (io/input console-io)) (prompt-for-input [this message] (.display-prompt this message) (.get-input this))) (defn create-console-ui [console-io] (map->ConsoleUI {:console-io console-io}))
Level 3 code on GitHub.
This code is very basic, but writing it helped my understanding of protocols. Hopefully it might help someone else who wants an example aside from the Clojure docs.
Note: lein run can be used to run the code.
0 notes