What you need to know about the Gilded Rose interview problem

A few years ago, I interviewed for a Dev Ops position at a pretty prominent CI shop in San Francisco. Part of the interview process involved a take-home code-assignment (of course). This particular assignment is known as the Gilded Rose.

The Gilded Rose isn’t a fizzbuzz. It’s not “Implement a Trie” in whatever language you want. The Gilded Rose is a problem that involves adding some functionality into the worst code that a developer could ever write. I don’t mean any offense to whoever created the Gilded Rose problem, bad code is entirely the intention! They want to see how you work through a terrible codebase to add new functionality.

This example will be in Clojure, as thats what I did this exercise with at the time, but it’s worth mentioning that the Gilded Rose exists for almost any programming language. If you find yourself facing this question, you’ll be left with usually two files.

The README

The README for this project is quite long. It’ll list stipulations for this problem that you have to follow for it to be successful. Some are downright stupid (more on that in a moment), but that’s the reality of actually working in real code. You’ll find those there, too. Anyways, here’s an example of what you might find in the Gilded Rose README (also, if you’ve ever played WoW, you might notice some references here).


Hi and welcome to team Gilded Rose.

As you know, we are a small inn with a prime location in a prominent city ran by a friendly innkeeper named Allison. We also buy and sell only the finest goods. Unfortunately, our goods are constantly degrading in quality as they approach their sell by date.

We have a system in place that updates our inventory for us. It was developed by a no-nonsense type named Leeroy, who has moved on to new adventures. Your task is to add the new feature to our system so that we can begin selling a new category of items.

First an introduction to our system:

  • All items have a sell-in value which denotes the number of days we have to sell the item
  • All items have a quality value which denotes how valuable the item is
  • At the end of each day our system lowers both values for every item

Pretty simple, right? Well this is where it gets interesting:

  • Once the sell by date has passed, quality degrades twice as fast
  • The quality of an item is never negative
  • “Aged Brie” actually increases in quality the older it gets
  • The quality of an item is never more than 50
  • “Sulfuras”, being a legendary item, never has to be sold or decreases in quality
  • “Backstage passes”, like aged brie, increases in quality as it’s sell-in value approaches; quality increases by 2 when there are 10 days or less and by 3 when there are 5 days or less but quality drops to 0 after the concert

We have recently signed a supplier of conjured items. This requires an update to our system:

  • “Conjured” items degrade in quality twice as fast as normal items

Feel free to make any changes to the update-quality method and add any new code as long as everything still works correctly. However, do not alter the item function as that belongs to the goblin in the corner who will insta-rage and one-shot you as he doesn’t believe in shared code ownership.

Just for clarification, an item can never have its quality increase above 50, however “Sulfuras” is a legendary item and as such its quality is 80 and it never alters.

Analyzing the Requirements

There’s a lot going on in this README, so I’m going to break this problem apart piece by piece. However, the interesting parts of this README aren’t the bullet points contained within it. It’s the content not bulleted (in fact, Im going to leave those bullet points out, scroll back up and read them if needed).

  • You’re making an update to this system to support conjured items. That’s it.
  • the “item” function can NOT be changed. Don’t touch it.
  • Legendary Items are an edge case. Sulfuras breaks the rules.
  • This is implied but not directly stated: YOU NEED TO WRITE TESTS.
  • YOU NEED TO WRITE TESTS FIRST.

If you do this for an interview, you’ll likely be told, “Oh don’t spend more than an hour or two on it.” Which is fair. If you don’t get anything else done in that hour or two, write tests and try to refactor. That is actually what this interview question is all about — the conjured items (the addition) is not substantial compared to ensuring code works before and after refactoring.

Why do I keep mentioning refactoring? Like I said at the beginning, this code is a nightmare. You’ll be given code like this (Clojure, sorry everyone else, but you should still be able to recognize that this is NOT good code):

(ns gilded-rose.core)

(defn update-quality [items]
  (map
    (fn[item] (cond
      (and (< (:sell-in item) 0) (= "Backstage passes to a TAFKAL80ETC concert" (:name item)))
        (merge item {:quality 0})
      (or (= (:name item) "Aged Brie") (= (:name item) "Backstage passes to a TAFKAL80ETC concert"))
        (if (and (= (:name item) "Backstage passes to a TAFKAL80ETC concert") (>= (:sell-in item) 5) (< (:sell-in item) 10))
          (merge item {:quality (inc (inc (:quality item)))})
          (if (and (= (:name item) "Backstage passes to a TAFKAL80ETC concert") (>= (:sell-in item) 0) (< (:sell-in item) 5))
            (merge item {:quality (inc (inc (inc (:quality item))))})
            (if (< (:quality item) 50)
              (merge item {:quality (inc (:quality item))})
              item)))
      (< (:sell-in item) 0)
        (if (= "Backstage passes to a TAFKAL80ETC concert" (:name item))
          (merge item {:quality 0})
          (if (or (= "+5 Dexterity Vest" (:name item)) (= "Elixir of the Mongoose" (:name item)))
            (merge item {:quality (- (:quality item) 2)})
            item))
      (or (= "+5 Dexterity Vest" (:name item)) (= "Elixir of the Mongoose" (:name item)))
        (merge item {:quality (dec (:quality item))})
      :else item))
  (map (fn [item]
      (if (not= "Sulfuras, Hand of Ragnaros" (:name item))
        (merge item {:sell-in (dec (:sell-in item))})
        item))
  items)))

(defn item [item-name, sell-in, quality]
  {:name item-name, :sell-in sell-in, :quality quality})

(defn update-current-inventory[]
  (let [inventory 
    [
      (item "+5 Dexterity Vest" 10 20)
      (item "Aged Brie" 2 0)
      (item "Elixir of the Mongoose" 5 7)
      (item "Sulfuras, Hand Of Ragnaros" 0 80)
      (item "Backstage passes to a TAFKAL80ETC concert" 15 20)
    ]]
    (update-quality inventory)
    ))

Tips for the Gilded Rose

I’m not going to share my solution with you — where’s the fun in that? Also, you can just browse Github if you must know. However, I will leave you with some tips.

  • Sulfuras is an edge case, but refactor it in such a way that you can add new edge cases (Perhaps Thunderfury, Blessed Blade of the Windseeker)
  • Use as many private functions as you find makes the public function readable, in Clojure, it can mean quite a few.
  • Consider using overloading, pattern matching, or value matching for items that have specific requirements (Brie, Concert Tickets)
  • Consider storing items in a map with a lambda that contains their “update-quality” behavior function.
  • Write tests first. Especially if your interviewer is asking you to commit this to Github. They will look at your commit history, because no-one (myself included) should touch this monster without adequate test coverage first.
  • Use comments throughout your code. The idea here is to see if you can take an abomination and make it usable for other developers while not breaking anything.
  • If you’re using Clojure for your project, this problem is an excellent one to consider Multimethods.

That’s all I have for you! Best of luck and have fun with it!

If you’re interested in learning more about Clojure, you can find my other Clojure related posts here!

The post What you need to know about the Gilded Rose interview problem appeared first on Brad Cypert.

Permalink

Reinforcement Learning : Exploration vs Exploitation : Multi-Armed Bandits


;; Reinforcement Learning : Exploration vs Exploitation : Multi-Armed Bandits

;; I'm reading the excellent:

;; Reinforcement Learning: An Introduction
;; by Richard S. Sutton and Andrew G. Barto

;; The book's website, on which is available a complete pdf, is here:
;; http://www.incompleteideas.net/book/the-book.html

;; In Chapter 2, they introduce multi-armed bandits as a simplified model problem

;; On the basis that you don't understand anything you can't explain to a computer, I thought I'd code it up:

;; Here is a 2 armed bandit
(defn bandit [action]
(case action
:arms? [:right :left]
:right (if (< (rand) 0.5) 4 0)
:left (if (< (rand) 0.2) 5 0)
:oops!!))

;; We can ask it how many arms it's got, and what they're called
(bandit :arms?) ; [:right :left]

;; And we can pull those arms. Rewards are variable.
(bandit :right) ; 4 ; 4 ; 4 ; 0 ; 0 ; 0 ; 0
(bandit :left) ; 5 ; 0 ; 0 ; 0 ; 5 ; 0 ; 5 ; 0

;; Once we pull an arm, we'll have an action/reward pair
(bandit :right) ; 4
;; the pair would be:
[:right 4]

;; Here's a function that yanks an arm at random, and gives us such a pair
(defn random-yank [bandit]
(let [a (rand-nth (bandit :arms?))]
[a (bandit a)]))

(random-yank bandit) ; [:left 0]
(random-yank bandit) ; [:right 4]

;; And a utility function to take the average of a sequence. We need to be able to provide a default value if the sequence is empty.
(defn average
([seq default] (if (empty? seq) default (/ (reduce + seq) (count seq))))
([seq] (average seq 0)))

;; with some examples
(average [1 2 3 4 5]) ; 3
(average (list) 10) ; 10
(average (list 1) 2) ; 1
(average [] 100) ; 100


;; If we just pull arms at random we get an average reward of about 1.5

(float (average (map second (repeatedly 1000 #(random-yank bandit))))) ; 1.49

;; Since we can see the code for this particular bandit, we know that
;; the expected value of pulling the right arm is 2 (a half-chance of
;; a reward of 4) and the expected reward for the left arm is 0.2*5 = 1

;; So if we were seeking to maximize reward, we'd probably be best to pull the right arm all the time.

(float (average (map bandit (repeat 10000 :right)))) ; 1.9912
(float (average (map bandit (repeat 10000 :left )))) ; 0.985


;; The interesting question is, if we don't know how the bandit works, how should we design an algorithm that gets the most reward?
;; (Or at least do better than yanking arms at random!)

;; One thing our algorithm is going to have to do is keep some state to record what happens.
;; Let's start by recording the results of all pulls to date:

;; At first, we know nothing, so we can set up a table to represent that we know nothing
(defn initial-state [bandit]
(into {} (for [k (bandit :arms?)] [k (list)])))

;; We haven't pulled either arm yet
(initial-state bandit) ; {:right (), :left ()}

;; When we get a new action reward/pair, we'll add the result to our state
(defn update-state [state [action reward]]
(update-in state [action] #(conj % reward)))

;; here are some examples of using update-state
(update-state {:right (), :left ()} [:right 2]) ; {:right (2), :left ()}
(reduce update-state {:right (), :left ()} [[:right 2] [:left 3] [:right 4] [:right 5]]) ; {:right (5 4 2), :left (3)}

;; here's how we can use it to record the result of ten random yanks
(reduce update-state
(initial-state bandit)
(repeatedly 10 #(random-yank bandit))) ; {:right (4 4 0 0 0), :left (0 0 0 0 5)}


;; Once we actually have some data, we can make estimates of the expected rewards

;; mapvals applies a function to every value in a map, returning a new map with the same keys
(defn mapvals [m f] (into {} (for [[k v] m] [k (f v)])))

;; examples
(mapvals {} inc) ; {}
(mapvals {:a 1} inc) ; {:a 2}
(mapvals {:a 1, :b 2} inc) ; {:a 2, :b 3}
(mapvals {:a 1, :b 2, :c 3} #(* % %)) ; {:a 1, :b 4, :c 9}


;; In the book, Q_t(a) is the current estimate (at time t)
;; We'll use as our estimate of the value of an action the average value seen so far, or zero if we have no information

(defn Q [state] (mapvals state #(average % 0)))

;; examples
(Q '{:right (5 4 2), :left (3)}) ; {:right 11/3, :left 3}
(Q '{:right (5 4 2), :left ()}) ; {:right 11/3, :left 0}
(Q (initial-state bandit)) ; {:right 0, :left 0}
(Q (update-state (initial-state bandit) (random-yank bandit))) ; {:right 0, :left 2}

;; let's check that we get roughly what we expect in the long run
(Q (reduce update-state (initial-state bandit)
(repeatedly 10000 #(random-yank bandit)))) ; {:right 9832/5015, :left 1027/997}


;; If we have estimates of the value of each arm, then a good way to
;; use them is to pull the arm with the highest estimate.

;; This is called 'exploitation', as opposed to 'exploration', which
;; is when you try things you think may be suboptimal in order to get
;; information

;; The 'greedy' action is the one with the highest expected value. Of
;; course there may be more than one greedy action especially at first.

;; To help with this, another utility function:

;; max-keys finds the keys with the highest value in a map, and returns a list with just these keys and values
(defn max-keys [m]
(let [slist (reverse (sort-by second m))
[_ max] (first slist)]
(take-while #(= (second %) max) slist)))

;; examples
(max-keys {}) ; ()
(max-keys {1 0}) ; ([1 0])
(max-keys {1 0, 2 0}) ; ([2 0] [1 0])
(max-keys {1 0, 2 1}) ; ([2 1])
(max-keys {1 0, 2 1, 3 -1 , 4 -3, 5 2, 6 2}) ; ([6 2] [5 2])

;; if there is a tie for the greedy action, we can choose at random between the candidates
;; And so we can go from estimates to greedy action like this:
(defn greedy-action [estimates]
(first (rand-nth (max-keys estimates))))

;; examples
(greedy-action '{:right 10, :left 3}) ; :right
(greedy-action '{:right 10, :left 3 :centre 20}) ; :centre
(greedy-action '{:right 10, :left 3 :centre 3}) ; :right
(greedy-action '{:right 3, :left 3 :centre 3}) ; :right

(greedy-action (Q '{:right (5 4 2), :left (3)})) ; :right
(greedy-action (Q '{:right (), :left (3)})) ; :left
(greedy-action (Q (initial-state bandit))) ; :left

;; after a lot of random pulls, the greedy action should reliably be the one with the highest expected payoff
(greedy-action (Q (reduce update-state (initial-state bandit)
(repeatedly 10000 #(random-yank bandit))))) ; :right

;; OK, so we have our stage set, a way of recording what's happened, and some helpful functions defined.


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Our first try at a learning algorithm will be 'by hand', as it were.

;; We'll always make the 'greedy' choice.

;; At first, we have no records to go on
(initial-state bandit) ; {:right (), :left ()}

;; expected values for both levers are therefore zero
(Q (initial-state bandit)) ; {:right 0, :left 0}

;; so the greedy action will get chosen at random
(greedy-action (Q (initial-state bandit))) ; :left

;; in this case, we've chosen :left, and the bandit's response is
(bandit :left) ; 0

;; we record it
(update-state (initial-state bandit) [:left 0]) ; {:right (), :left (0)}

;; and we have a new state
'{:right (), :left (0)}

;; new estimates
(Q '{:right (), :left (0)}) ; {:right 0, :left 0}

;; and again, we choose at random
(greedy-action (Q '{:right (), :left (0)})) ; :left

;; the bandit is not feeling very generous
(bandit :left) ; 0

(update-state '{:right (), :left (0)} [:left 0]) ; {:right (), :left (0 0)}

;; new state:
'{:right (), :left (0 0)}

;; new estimates
(Q '{:right (), :left (0 0)}) ; {:right 0, :left 0}

;; this time we choose :right
(greedy-action (Q '{:right (), :left (0 0)})) ; :right

;; and the bandit pays out!
(bandit :right) ; 4

(update-state '{:right (), :left (0 0)} [:right 4]) ; {:right (4), :left (0 0)}

;; the greedy action will be :right now, because we have evidence that right is better.
(greedy-action (Q '{:right (4), :left (0 0)})) ; :right

;; You get the idea......

;; Let's automate that....

;; Given a state and a bandit, we decide an action and the bandit
;; responds, producing an action/reward pair, and a new state

(defn greedy-algorithm [bandit state]
(let [action (greedy-action (Q state))
reward (bandit action)]
[[action reward] (update-state state [action reward])]))


(greedy-algorithm bandit (initial-state bandit)) ; [[:left 0] {:right (), :left (0)}]

;; To get something we can iterate:

(defn step [[[a r] state]]
(greedy-algorithm bandit state))

(iterate step [ [:dummy :dummy] (initial-state bandit)])

;; ([[:dummy :dummy] {:right (), :left ()}]
;; [[:left 5] {:right (), :left (5)}]
;; [[:left 0] {:right (), :left (0 5)}]
;; [[:left 0] {:right (), :left (0 0 5)}]
;; [[:left 0] {:right (), :left (0 0 0 5)}]
;; [[:left 0] {:right (), :left (0 0 0 0 5)}]
;; [[:left 0] {:right (), :left (0 0 0 0 0 5)}]
;; [[:left 0] {:right (), :left (0 0 0 0 0 0 5)}]
;; [[:left 0] {:right (), :left (0 0 0 0 0 0 0 5)}]
;; [[:left 0] {:right (), :left (0 0 0 0 0 0 0 0 5)}]
;; [[:left 0] {:right (), :left (0 0 0 0 0 0 0 0 0 5)}]
;; [[:left 0] {:right (), :left (0 0 0 0 0 0 0 0 0 0 5)}]
;; [[:left 5] {:right (), :left (5 0 0 0 0 0 0 0 0 0 0 5)}]
;; [[:left 0] {:right (), :left (0 5 0 0 0 0 0 0 0 0 0 0 5)}]
;; [[:left 0] {:right (), :left (0 0 5 0 0 0 0 0 0 0 0 0 0 5)}]
;; [[:left 0] {:right (), :left (0 0 0 5 0 0 0 0 0 0 0 0 0 0 5)}]
;; [[:left 0] {:right (), :left (0 0 0 0 5 0 0 0 0 0 0 0 0 0 0 5)}]

;; In this case, the greedy algorithm happens to get a payout on its
;; first try, and decides that it will pull that arm for ever. It
;; never even tries the other arm.

;; Try again:

(iterate step [ [:dummy :dummy] (initial-state bandit)])
;;([[:dummy :dummy] {:right (), :left ()}]
;; [[:right 0] {:right (0), :left ()}]
;; [[:right 0] {:right (0 0), :left ()}]
;; [[:left 0] {:right (0 0), :left (0)}]
;; [[:right 4] {:right (4 0 0), :left (0)}]
;; [[:right 4] {:right (4 4 0 0), :left (0)}]
;; [[:right 4] {:right (4 4 4 0 0), :left (0)}]
;; [[:right 4] {:right (4 4 4 4 0 0), :left (0)}]
;; [[:right 4] {:right (4 4 4 4 4 0 0), :left (0)}]
;; [[:right 4] {:right (4 4 4 4 4 4 0 0), :left (0)}]
;; [[:right 0] {:right (0 4 4 4 4 4 4 0 0), :left (0)}]
;; [[:right 0] {:right (0 0 4 4 4 4 4 4 0 0), :left (0)}]
;; [[:right 4] {:right (4 0 0 4 4 4 4 4 4 0 0), :left (0)}]
;; [[:right 0] {:right (0 4 0 0 4 4 4 4 4 4 0 0), :left (0)}]
;; [[:right 4] {:right (4 0 4 0 0 4 4 4 4 4 4 0 0), :left (0)}]
;; [[:right 0] {:right (0 4 0 4 0 0 4 4 4 4 4 4 0 0), :left (0)}]

;; In this case, it tried the right arm a couple of times, then had a
;; go with the left arm, then went back to the right arm, won a
;; payout, and then got hung up on pulling the right arm repeatedly.



;; We've got a couple of problems here!

;; First is that the algorithm has clearly got into a state where it
;; always pulls the left arm (in the first case), and the right
;; arm (in the second case).

;; It can't be doing the right thing in both cases.

;; Secondly the state is growing linearly, as the algorithm remembers
;; all previous results. That's giving us algorithmic complexity
;; problems and the calculation will get slower and slower, and
;; eventually run out of memory.













Permalink

The Clojure Style Guide Redux

Prelude

Recently I’ve noticed that the Community Clojure Style Guide has turned 6! According to git log the project started its life on the 3rd of January, 2013!

commit 3abbcfb8b536999d0b1d64b531a9c57b49b4562a
Author: Bozhidar Batsov <bozhidar@batsov.com>
Date:   Thu Jan 3 10:09:53 2013 -0800

    Initial commit

Time sure flies fast!

After the initial announcement of the guide, I never really revisited the topic, so it seems to me that this “birthday”, is as good of an opportunity as any, to do so.

Impact

The main purpose of a style guide is always to educate (beginners) and align (experts). It showcases in a concise form the convention of a language and how to effectively use it. Its existence also spares you from the burden of having to make a lot of trivial decisions yourselves and makes it easier for people to work on many different projects (provided those projects applied the same style principles).

Did the Clojure style guide achieve its primary objectives? From my perspective it seems that the guide had a good impact on the community - I’ve often seen it cited here and there, which means that people are using it. I can only guess how many people are actually abiding by it, but I’m feeling optimistic in this regard.

The impact was not as big, however, as I had originally envisioned, because I never got to the second step I had planned - namely writing a lint-like tool that enforces the guide automatically. I pretty much wanted to replicate what I had done for Ruby with the Community Ruby Style Guide and RuboCop. I recall I spent a lot of time thinking about the name of the lint tool, but I never actually found time to start it.1

Despite my failure, others managed to fill some of the gap - I know that zprint has a community formatting mode and I think that cljfmt has defaults that are similar to what’s outlined in the style guide.

Current Status

The work on the guide has stagnated a bit in the recent years, as I’ve been very busy with work and other projects, and there haven’t been many contributors to the project. When the guide was announced a lot of people helped with growing and polishing it, but after a while the work grinded almost to a halt.

I’d really love for us to find a few more editors to help with the guide (and more occasional contributors, of course), so that the guide can be completed and always kept up-to-date with the best practices in the Clojure community. Having more editors should also dispel any notion that despite its name (community guide), the guide reflects my personal stylistic preferences, as opposed to those of the broader Clojure community.

Future Goals

So, I guess now you’re wondering how exactly can you help, right? Let me give you a few ideas - some goals I set out to achieve and never got to accomplishing.

Cohesion

One of the problems of the guide today is that certain sections are just stubs, compared to others. All sections should have equally good rationales, examples and so on. Currently that’s certainly no the case for sections like State Management, Macros, Documentation, etc.

A good style guide is a cohesive style guide. It’s a style guide with good style.

Coverage of Newer Clojure Features

Clojure introduced many new features after the guide was created that never made their way to it. Some things that come to mind are transducers, specs, reader conditionals.

We should totally fix this!

Better Rationales

I’m a big fan of Stuart Sierra’s series on Clojure Do’s and Don’t. They are similar to a style guide, but the articles there go into great detail when it comes to the rationale for the suggestions in them.

While a style guide has to be concise by definition, it also has to be clear. The Clojure style guide can certainly take a page from Stu’s book and improve the rationales of many of its suggestions.

I also think that he has covered many of topics that are not covered in the style guide presently, so it’d be nice if we borrowed those as well.

So, what’s “better rationale” to begin with. Well, unfortunately that’s really subjective, but let me try to illustrate this with a few examples. Here’s a rule with a weak rationale (or poor examples):

Leverage comp when doing so yields simpler code.

;; Assuming `(:require [clojure.string :as str])`...

;; good
(map #(str/capitalize (str/trim %)) ["top " " test "])

;; better
(map (comp str/capitalize str/trim) ["top " " test "])

Is this really simpler? Why not use threading?

And here are a few examples a consider better - either because they are self-explanatory, or they have better rationale attached to them:

Prefer condp instead of cond when the predicate & expression don’t change.

;; good
(cond
  (= x 10) :ten
  (= x 20) :twenty
  (= x 30) :thirty
  :else :dunno)

;; much better
(condp = x
  10 :ten
  20 :twenty
  30 :thirty
  :dunno)

Prefer case instead of cond or condp when test expressions are compile-time constants.

;; good
(cond
  (= x 10) :ten
  (= x 20) :twenty
  (= x 30) :forty
  :else :dunno)

;; better
(condp = x
  10 :ten
  20 :twenty
  30 :forty
  :dunno)

;; best
(case x
  10 :ten
  20 :twenty
  30 :forty
  :dunno)

Don’t use the interop syntax to construct type and record instances. deftype and defrecord automatically create constructor functions. Use those instead of the interop syntax, as they make it clear that you’re dealing with a deftype or a defrecord. See this article for more details.

(defrecord Foo [a b])
(deftype Bar [a b])

;; good
(->Foo 1 2)
(map->Foo {:b 4 :a 3})
(->Bar 1 2)

;; bad
(Foo. 1 2)
(Bar. 1 2)

Note that deftype doesn’t define the map->Type constructor. It’s available only for records.

I hope you get the point.

Looking Forward

Once the goals I’ve mentioned above have been dealt with, I’m considering to submit the style guide to https://clojure.org. Hopefully Alex, Rich and Stu would consider it a valuable addition to the official documentation.

I’d also love to find the time to write that lint tool I mentioned earlier, but I doubts that’s going to happen in the foreseeable future.

Epilogue

If you’ve got any generic feedback about the style guide (structure, content, examples, whatever) - I’d love to hear it! I’m looking forward to working with all of you on making the guide better and more useful to all the members of the Clojure community!

Keep hacking and keep editing!

  1. I think my front-runners for the name were Caliber and Zealot

Permalink

Senior Software Engineer | Scala, Clojure at Otravo (Full-time)

Are you passionate about software development? Do you believe in making a difference? We have a challenging role within our highly skilled team of dé VakantieDiscounter!

What are you going to do?

Because we build all our systems in-house with our own integrated development teams, we could really use your knowledge and skills, helping us leverage technologies such as

  • Apache Spark, for distributed data processing
  • Clojure and ClojureScript/React.js, for powering our website
  • Scala, Akka, and Play for a scalable stateless REST back-end
  • Kafka and Solr, for our data management platform
  • A/B experiments, for sensible decision making

You will be pairing to write lots of (clean) code that goes to production continuously and will be spending ± 10% on private projects to find the next big thing

Skills And Qualifications

You are a seasoned software developer (5+ years) and have solid experience with one of the technologies we use. You know functional programming and have a passion for learning and sharing your knowledge.

We consider it a plus if you have experience with:

  • Big Data, Spark, Solr/Elasticsearch
  • Cats / Scalaz / Shapeless functional programming libraries
  • React, Play framework
  • AWS, Docker or OpenStack

What do we offer?

Otravo is an informal yet professional company, beautifully situated at the Amsterdam canals. We use agile development and best practices to deliver working software with little overhead, fast time to market and high quality.

The package we offer contains the following benefits:

  • A good salary 40.000 - 65.000 gross per year;
  • 30 paid holidays;
  • Opportunities to attend conferences, training sessions, workshops, etc.;
  • Discount on your holiday trips (of course :)
  • Vibrant company culture;
  • Support with relocation (visa, housing, 30% tax ruling)
  • Free snacks and amenities; cake every sprint;
  • Free use of company gym.

About Otravo

Otravo means Online Travel Organisation and is market leader in the Benelux, Scandinavia and South Europe of online air travel sales. Travellers can book airline tickets to worldwide destinations via self-service and online, and for both leisure and business trips. Otravo offers travellers a wide selection of options, everything from worldwide flights, rental cars, hotels and sun holidays to all possible dynamic travel combinations.

Otravo is the parent company of several well-known travel brands. Brands like Vliegtickets.nl, Vliegtickets.be, WTC.nl, Schipholtickets.nl, de Vakantiediscounter, Flygstolen, Tripmonster and Greitai and most recently the brand Travelgenio are all part of Otravo. Every day over hundred thousands of visitors are welcomed to our websites and since 1983 millions of people have travelled through our brands. Otravo is a very healthy and innovative company with a growing yearly turnover of over 1,5 billion euro’s and a team of more than 500 professionals.

Get information on how to apply for this position.

Permalink

CIDER 0.20 (Oslo)

Hot on the heals of yesterday’s article) I’ve got a new one for you! This one is going to be short and sweet - just a few notes about the release of CIDER 0.20 (Oslo).

I guess some of you are surprised by the news - after all wasn’t CIDER 0.19 (Raleigh) released only a couple of weeks ago? Usually CIDER releases are (many) months apart. Well, while that’s normally the case, I’ve always wanted to do more focused and frequent releases and it seems that I managed to succeed this time around.

CIDER 0.20 includes very few changes overall.1 I’ll cover them briefly here.

Cleaning up Shop

CIDER (or cider-nrepl to be precise) dropped support for nREPL 0.2 (a.k.a. tools.nrepl). Now you’ll need nREPL 0.4+ (ideally 0.5.3+), but that’s not a big deal as at this point both Boot and Leiningen ship the modern nREPL. Just make sure you’re on Boot 2.8.2+ or Leiningen 2.8.3+.

I’ll be gradually removing support for nREPL 0.2 from all nREPL extensions I’m maintaining in the months to come. I’d advise other people maintaining nREPL extensions to do the same.

Native nREPL Pretty-printing

CIDER used to rely on a custom middleware for result pretty-printing for many years now, but nREPL recently added native support for pretty-printing values, and this rendered our custom approach redundant. The biggest change in CIDER 0.20 is that it switches to nREPL’s built-in pretty-printing API.

Some other related changes:

  • Support for zprint has been added.
  • We’re passing to the print engines (e.g. zprint) their native options, instead of wrapping their invocations in dynamic bindings (e.g. *print-length*, *print-level*, etc).
  • Pretty-printing is enabled in REPL buffers by default.
  • You can pretty-print results in scratch buffers using C-u C-j.

There’s now some work underway in nREPL to support streamed printing of values. That’s going to be a really great improvement of the current situation as it would mean faster feedback for the clients and no possibility for “invisible” printing of infinite/huge sequences on the server-side.2

CIDER now has a new section of its manual dedicated to pretty-printing. You should definitely check it out!

Note that the new pretty-printing requires for you to be on nREPL 0.5+. If you’re not, you’re simply going to get results that are not pretty-printed.

Misc

I’ve spent a bit of time reorganizing CIDER’s manual and breaking down some ridiculously big sections (e.g. Using the REPL, Configuration, etc) into smaller chunks. I think you’ll find the new layout easier to navigate, but there’s still a lot of room for improvement.

A handful of small changes and bug improvements also made it in this release, but there’s nothing worth highlighting there. Just the usual quality of life improvements you’re bound to expect from each new release.

On a related note - there’s also a new clojure-mode release, that you should certainly check out!

CIDER Next

I don’t have a specific plan for the next release - neither in terms of features nor in terms of a timeline.

I’ll be taking some time off from working on open-source now that the new CIDER is out, and I hope that in the mean time the work on the next nREPL release will be done and I’ll just have to incorporate all that awesomeness into CIDER when I’m back.

As some of you probably know, Christophe Grand has joined nREPL’s team and is working on bringing some amazing features from unrepl to nREPL:

(Extremely) Exciting times ahead! I cannot thank Christophe enough for the amazing work he has been doing on nREPL recently! People like him are the heart and soul of the Clojure community and the reason why I love being part of it!

Epilogue

The year had an explosive start for me - so many project release, so much open-source work done! I was really eager to start “clean” and finish many of the initiatives that have been dragging throughout the past year. It was pretty painful and exhausting, but I got to a point where I did everything I felt I needed to do (for the time being) and now I’m looking forward to some respite and re-energizing myself.

While, I’m gone the development won’t stop, of course. In my absence the other CIDER and nREPL Core Team members will be reviewing PRs and cutting releases. I hope you’ll surprise me with many great developments next time I open my GitHub!

Enjoy CIDER Oslo (ir)responsibly! Keep hacking!

  1. https://github.com/clojure-emacs/cider/releases/tag/v0.20.0 

  2. Those are hard to spot and result in out of memory errors in nREPL. 

Permalink

PurelyFunctional.tv Newsletter 309: Hiring, cljdoc, Probability

Issue 309 – January 14, 2019 · Archives · Subscribe

Hi Clojurists,

As I write this, I’m sitting in my hotel room in Bengaluru, India. Yesterday was IN/Clojure. I was so impressed with the organization, the speakers, and the attendees. I’ve had such a great time here. And it really proves that a community can be created around a language. All it takes is people willing to guide people to learn.

Rock on!
Eric Normand <eric@purelyfunctional.tv>

PS Want to get this in your email? Subscribe!


All Functional Programming Podcasts

I’ve been getting more into podcasts recently and realized there wasn’t a good collection of functional programming podcasts. So I made one. Please let me know if any are missing.


My Worst and Best Code of 2018

Jonathan Boston on what makes code good. Is it because it’s well designed or because it solves someones problems?


cljdoc

I saw Martin Klepsch speak at IN/Clojure about cljdoc and I was convinced it was an important tool. Check it out. Run it on your libraries. And consider contributing to this open source project.


State of Clojure 2019 Survey

The survey is open now! Please go fill it out.

This survey is one of the most important ways that the community understands itself. It helps us and Cognitect know how people use Clojure and why they don’t use it more. Please fill it out, regardless of your skill level.


Dueling Keyboards YouTube

Chris Ford talks about expressing music with different keyboards. He explores different tunings, consonance and dissonance, and algorithmic performance.


Cloud Computing without Containers

I did not know about V8 Isolates. This idea is making me interested in exploring serverless systems.


A developer’s guide to probabilistic programming YouTube

Evelina Gabasova talks about probabilistic programming, which seems like an awesome tool to add to your toolbelt. You basically can compose probabilistic distributions algebraically.


Clojure at Apple with David Taylor Podcast

I find it really interesting when people with experience hiring Clojure developers find that it’s not hard to recruit. Sure, there are fewer Clojure developers than JavaScript developers. But because there are fewer Clojure jobs and Clojure learning programs than JavaScript jobs and learning programs, Clojurists are a self-selected group.

What kind of self-selection? It’s hard to say, but here are some descriptors that come to mind: curious, persistent, discriminating (as in they know what they like). If these match what you’re looking for in a candidate, you could do worse than hiring Clojurists.

Please listen to this episode of The REPL where Daniel Compton interviews David Taylor, who hired Clojure programmers at Apple.


Accessing DOM nodes Free lesson

This week’s free lesson is a very difficult topic. Sometimes you need to get access to the actual DOM nodes in your Reagent components. For instance, if you have ah HTML canvas you need to draw on, or an embedded video you need to play, you’ll need to start with an actual DOM node. This 18-minute video talks about how you can grab a reference to any DOM node within a component. And it’s free for this week!

The post PurelyFunctional.tv Newsletter 309: Hiring, cljdoc, Probability appeared first on PurelyFunctional.tv.

Permalink

Who Is Peter Drucker?

Peter Drucker — you haven’t heard of him, but he is a prophet among people who sign checks…

Don’t Call Yourself A Programmer, And Other Career Advice by Patrick McKenzie

That quote, while written somewhat in jest, is an insightful commentary on software developers. Peter Drucker is considered the father of modern management. He’s written 39 books that have been translated into 36 languages. How can it be said that developers, the majority working for managers, don’t know who he is?

Who Signs The Checks?

There are a lot of tools and techniques designed to make developers think more about the end user, and rightly so. But for many of us, we don’t put in similar work to understand the problems and needs of the people actually paying us to write code. You can find an infinite number of blogs about insert your favorite language/tool, but it’s rare to find technical writing that points us to think about code from a business perspective.

If we want to serve our clients/managers better, we would do well to learn more about the problems they face. Instead of reading another programming book or listening to a conference talk yet again, why not study something that will give us insight into the people we work for? We can make space to understand managers and executive teams without having to abandon all technical study.

When was the last time you thought about your position in relation to everything your manager is responsible for?

What are the top problems facing your executive team? What framework or mental model are they using to approach those problems? In what way are you well suited to help solve those problems? Is that what you’re currently doing?

There are myriad questions to ask and approaches to take; the goal is simply a deeper understanding of managers and executives. That deeper understanding will lead us to develop more effective solutions for the people we serve.

Permalink

Clojure (FP) Software Engineer at Reify Health (Full-time)

At Reify Health, we are building a more creative healthcare system. We envision a world where every potential therapy, if safe and effective, is available to the patients who can benefit.

Our healthcare system relies on clinical trials to develop new, potentially life-saving treatments for patients. But clinical trials continue to be slow, unpredictable, and expensive. Reify Health’s product helps both the research leaders driving forward clinical trials and the doctors and nurses who care for the patient participants.

As we continue scaling the adoption of our product, we accelerate world-class clinical research and unlock innovation. By joining our team, you will play a significant role in our growing engineering team, further establishing our foundation for a culture of impact and empathy.

Your Responsibilities

  • Deliver an extraordinary user experience with HIPAA-compliant technology to address complex, real-world problems in healthcare.
  • Work with backend and frontend projects to build high-quality, maintainable, and well-tested code.
  • Strive for technological excellence and accomplishment by driving modern processes and standards for work.
  • Collaborate with our design and product teams as product features move from product specs to production software.
  • Become a significant teammate in our Bro-Free Zone by communicating well and being humble.

Technology

  • Our primary applications are interactive ClojureScript web applications built atop React.
  • Our backend API is written in Clojure and our primary data store is PostgreSQL.
  • Experience with Clojure(Script) or other functional programming languages preferred. In absence of experience, enthusiastic interest to learn our tech.
  • Bonus: Data engineering experience with solutions that include Kafka, Kinesis, Spark, Onyx or Samza.
  • Bonus: Experience building mobile web, native or hybrid applications (e.g. React Native).

Nice to Haves

  • Experience with product development. You believe empathy for users is the foundation of good products.
  • Open source experience. You look for opportunities to extract and open source libraries from production code that would be beneficial to others. It helps the community and helps us to build at a high level of quality.
  • Interest in helping our team grow. Help us grow sustainably as we build products our customers love.
  • Experience with healthcare related technologies. Any previous healthcare experience is appreciated as it increases the potential to take our products in new and interesting directions.

Compensation and Perks

  • Competitive Salary and Stock Options: Competitive salary and meaningful stock options -- commensurate to your experience and expertise. Compensation varies from mid-level to very senior.
  • Comprehensive Health / Wellness Coverage and Retirement Plan: Health (including telemedicine), dental, vision, disability and life insurance. We pay 100% of your premiums and more than half for dependents.
  • Vacation and Holiday Flexibility: Generous paid-time-off policy that accrues with your tenure at Reify which includes holiday flexibility and parental leave
  • Company-provided Workstation: Brand new Macbook Pro provided.
  • Location Convenience & Transportation: A public transportation monthly pass. We are a short 2-3 minute walk from Downtown Crossing. Full suite of office accommodations provided by WeWork: unlimited coffee, infused water, and more.

We value diversity and believe the unique contributions each of us brings drives our success. We do not discriminate on the basis of race, religion, color, national origin, gender, sexual orientation, age, marital status, veteran status, or disability status.

Note: We are currently only considering US citizens or Green Card holders. Thanks!

Get information on how to apply for this position.

Permalink

Kotlin: How Can a Programming Language be so Young And so Mature at The Same Time?

In this article, we’re going to explore why young programming languages with modern features can’t be adopted quickly. Additionally, we’re going to take a look at one exceptional example that got specific parameters right to be both young, modern and mature, just ready for adoption at small and big scale.

DISCLAIMER: Some details are omitted intentionally. Keep your mind open when reading. If something seems naive, or not entirely true, bear in mind that the author thought into both the front and back sides of the coin, and the unseen third side of the coin, as well. You’re welcome in comments for a more in-depth discussion—it’s going to be worth your time for sure!

We all know that most of us, software developers, love to chase after shiny things just as much as we like to build them. This is not different for programming languages.

And, in this day and age, new young programming languages pop up like these mushrooms:

Why new modern languages can’t be adopted quickly?

A distinct feature of a young language is that creators and community gets the chance to experiment with different features and designs to potentially fix some problems of the existing mature languages. Mind you, any solution to any problem will bring minimum two more subtle issues. So this cycle of continuous improvement will never end.

While young languages can explore more advanced features and designs, they usually don’t have a robust ecosystem of libraries, frameworks, and tooling. Moreover, when the community develops these, people will still have to learn them, and that means that they (albeit temporarily) will be much less productive. And I’m not even going to talk about potential vulnerabilities, bugs, and so on.

Now, wouldn’t it be great if the new young modern language could rely on the existing mature ecosystem? Wouldn’t it be fantastic if using existing libraries, frameworks and tooling was entirely natural and not awkward at all?

If you ask me these questions, I’ll tell you that you’re perhaps describing Kotlin. Kotlin is a programming language where you can naturally use any library, framework or tool from the JVM ecosystem without any awkwardness.

Now, there were languages before, that could do that as well, like Scala, Groovy, Clojure, and so on. And they all got something wrong: natural use.

Why these languages don’t want to rely on the existing mature ecosystem?

Either it wasn’t natural to use the same framework or library, or it was even impossible to use the same tooling. And in the scenario where it would be possible to use same tooling, framework, and libraries naturally; somehow, the majority of the community went out there and built new tools, frameworks, and libraries (I’m looking at you, Scala, and sbt).

My hunch is that this happens because most of the early adopters of these new languages (“promised replacements for Java”) have somewhat an adversarial relationship with Java language and JVM ecosystem.

They hate it.

As early adopters, they set the playing field, because they are in the vast majority in the community during the first few years of the new young language. This way, they get to write all the brand new shiny tools, frameworks and libraries. And they are the ones who start new projects in these languages at actual companies and startups.

This way, the community style sets in: unique tooling, frameworks, and sets of libraries that people use. Sometimes, these are even designed on purpose to be radically different from their older alternatives from the JVM community.

This makes the learning curve very steep, and so the language gets adopted only by like-minded people, and poor developers who just have no choice but to maintain the codebases where this set of tools is being used already.

How to attract different early adopters that will be happy to leverage existing ecosystem?

Kotlin is radically different from this approach. The playing field rule number one: “100% bidirectional interoperability with Java and JVM ecosystem.”

Thus the community attracts fellows who are not looking to do everything in the entirely different approach; instead, they want a more modern language, AND they want to transfer their skills, knowledge, and experience from working with the mature ecosystem like JVM.

These early adopters don’t want to rewrite their existing codebases entirely because they are large, so being able to write only new code in Kotlin, and keep older code in Java helps a lot!

Small story of a fellow software engineer

I’m going to give you a tangible example:

Clara is a software engineer, working for a big organization, mostly working with Java, JVM, and Spring Boot. Clara’s professional level is senior and perhaps even beyond that:

  • she applies best software practices,
  • knows how to design simple software iteratively,
  • can solve any complex problem you throw at her,
  • knows her audience when writing the code (current and expected in the future level of her team members),
  • excellent communicator and team player,
  • teaches and mentors other developers,
  • can take on the role of a team lead today,
  • and lives the life of a life-long learner.

Her current development workflow includes building the application and automating development tasks with Gradle, and she gets her coding done within a modern IDE like IntelliJ. She doesn’t have any of the adversarial feelings towards any of these tools and technologies.

Quite the opposite: she loves them!

Sometimes though, she feels feature envy to all these cool modern programming languages where you could have one-liner data classes, less boilerplate in general, and the null-pointer exception is not a dreaded threat at every step.

And here is Kotlin programming language that recently became “production-ready.”

So she tries out to build a tiny web app (a tad more complicated than “Hello world”). And it seems like she can still use the same build tool—Gradle; and she still can use any of her favorite libraries, and much more critical, she can use the same web framework—Spring Boot.

And using all these things is not awkward at all, and feels just as natural, as in their “home” environment—Java.

Now, she has the next question: “This language is great but quite new… do I have to lose all the great features of my IDE now?” Because that is what happens to most modern languages—the IDE support is lagging way… way behind.

And now she gets pleasantly surprised: 95% of the IDE features that she needs are there and are supported:

  • basic and advanced refactorings,
  • jump to definition,
  • find usages,
  • running application and tests from IDE,
  • running a single test in the current context,
  • debugger,
  • code completion, even cross-language,
  • etc.

And this makes so much sense because the language is developed by the same company that develops the IDE (JetBrains). This certainly ensures that the tooling will be at least somewhat up to speed with the new modern language.

“This all sounds great!” Clara thinks, “Do I have to rewrite my application at work now fully to Kotlin?”

Most important playing field rules fostering the adoption

And she gets pleasantly surprised here as well. All she really needs to do is to add Kotlin Gradle plugin (few lines of change in a single file), and she can start adding Kotlin files to the source code base. All the old files, written in Java, are still working correctly.

Moreover, cross-language calls are working out of the box: she can call Java code from Kotlin, and Kotlin code from Java; AND it all feels just as natural. At almost no point, the calling code is even aware that it is calling one or the other because there is no difference.

That is what making “100% interoperability” a most significant playing field rule right from the beginning can do for the new modern language!

So if you’re going to design a new programming language, I suggest you consider imbuing it as one of the top priorities, as it will allow for much better adoption. And what creator of the library, tool, or language doesn’t want this kind of adoption superpower? ;)

As you could notice, Clara is not looking for an entirely radically different approach to coding, and instead, she is looking for a way to keep 75% of what is working well, and perhaps use a few more modern features of the language to improve the other 25% significantly.

This could be reducing the amount of the boilerplate by letting the language figure out the obvious so that you don’t have to specify it every time. This could also be a different decision to make everything an expression, which makes specific scenarios much more ergonomic and more comfortable to read, for example, try-catch block, where you need to set a variable in the “try” part and rethrow an enhanced error in the “catch” part.

These things and more are improving the readability significantly.

Additionally, the ability to have first-class immutable variables and nullable/non-nullable types eliminates whole classes of possible mistakes and errors. It is fantastic!

Stand on the shoulders of the giants!

Most of the modern languages are created to be used standalone, detached from the existing library and tooling ecosystem, either by design or “forced” by community guidelines or culture.

These modern languages might see some adoption, but they will eventually die out or stay in the small niche because the majority of professionals will just continue using the mature ones because they have better support for literally everything. There are rare, infrequent, exceptions from this rule.

If the new programming language wants to stay strong, it needs to stand on the shoulders of the giants.

So, instead of going against the former mature language, and doing everything differently, leverage as much as you can from the mature ecosystem, and add more value on top of that. This way, people, when learning this language don’t have to give up anything, and they only stand to gain— a real win-win situation!

Did that spike your curiosity about Kotlin?

I have written a 4-part (350-pages total) “Ultimate Tutorial: Getting Started With Kotlin” (+ more to come), and you can get it as a free bonus by becoming a member of my monthly newsletter.

On top of just Kotlin, it is full of goodies like TDD, Clean Code, Software Architecture, Business Impacts, 5 WHYs, Acceptance Criteria, Personas, and more.

—Sign up here and start learning how to build full-fledged Kotlin applications!

Thank you and ask for you!

Thank you so much for reading this article! I hope you enjoyed it. Please, tell me what you think about this in the comments!

Also, it would make me so much happier if you could share this post with your friends and colleagues who you think might benefit from it. Or you could share it on your favorite social media!

You are welcome to read my blog about Kotlin, and my blog about TDD and best software engineering practices.

And let’s connect on LinkedIn: I post short weekly updates about software developer’s productivity and happiness, teamwork, mental health, and a bit about Kotlin.

If you want to hear more of my opinions, follow me on Twitter.

Originally published on iwillteachyoukotlin.com blog.

cover image source: pexels

Permalink

Boot — (future (boot))

Boot — (future (boot))

boot-clj logo

The future of boot-clj — we at Degree9 use boot everyday, from running local builds on our dev laptops to containers in production. Boot is an essential part of our toolkit, so you can imagine we jumped at the opportunity when an open-call was placed for maintainers.

TL;DR
1. Boot has new maintainers — Degree9
2. V3 targets performance, long-term support
3. V4 targets cross-platform, boot as libraries
4. Joining Open Collective
5. Commercial Support
6. T-Shirts & Stickers!

Boot is a great example of clojure and open-source. We want to continue developing boot the way it always has been, by driven from real-world problems by the people who use it. There are many loyal boot users within the clojure community and we want them to thrive!

So what do we have in the pipeline for boot? Well a few things!

First we want to maintain backwards compatibility with boot v2.7/2.8. If you are not already on one of these versions start updating your projects now.

There are a few common issues with boot that come up from time to time. The more noticeable problems being performance and cross-platform support. These will be the intended goals for v3/v4 respectively.

Boot V3 will focus on making performance orientated changes internally with backwards compatibility in mind. Any changes which are breaking will be delayed to v4. Our goal is to improve the experience of boot users who are familiar with the current releases, while also pinning the first LTS release.

V4 will bring boot to the masses, we have always wanted to run boot with more freedom, on more platforms. There is already a prototype replacement for boot-bin which compiles the loader shim into a native image using GraalVM. You can get a sneak peek at boot/boot-graalvm. This project will create a binary that loads almost instantly, and can be built for any Host OS. It only runs on Linux today but improvements to graal will get pulled in quickly and make native image for Windows and MacOS a possibility.

Today boot is already made up of many libraries. Moving forward these will be separated into a loose collection of tools that support one another. Making the task of improving individual features and enabling cross platform builds much easier.

open collective logo

Open-source projects don’t happen for free, there is a considerable amount of work that goes into them. So in order to continue supporting boot we are joining the Open Collective, which will provide a channel for those who want to donate to the project directly.

boot-clj - Open Collective

We know getting from development to production can be a difficult task, that’s why we are launching professional services for Boot-clj consulting.

D9 Pipeline will enable your team to take advantage of our DevOps tools and experience — automating even the most complex builds using Boot, without the need for your team to learn another tool. Your app is written in clojure, why not your build?

Already running boot in dev/prod — thats great! Get expert advice when things don’t go as planned.

Lastly we are opening an online merchandise shop <openswag.co> where you can buy products with your favourite open-source project logos.

A portion of every sale goes to support the open-source community.

Boot & Hoplon will be among the first projects to get their logo’s featured.

openswag.co

Boot — (future (boot)) was originally published in degree9 on Medium, where people are continuing the conversation by highlighting and responding to this story.

Permalink

Copyright © 2009, Planet Clojure. No rights reserved.
Planet Clojure is maintained by Baishamapayan Ghose.
Clojure and the Clojure logo are Copyright © 2008-2009, Rich Hickey.
Theme by Brajeshwar.