Record of experiments, readings, links, videos and other things that I find on the long road.
Registro de experimentos, lecturas, links, vídeos y otras cosas que voy encontrando en el largo camino.
Friday, December 30, 2016
Books I read 2016
- El Eternauta, Héctor Germán Oesterheld and Francisco Solano López
- Barcelona. Los vagabundos de la chatarra, Jorge Carrión and Sagar Fornies
- Rip Van Winkle, Washington Irving
- La guerra interminable (The Forever War), Joe Haldeman
- Maintanable JavaScript, Nicholas C. Zakas
- Ve y pon un centinela (Go Set a Watchman), Harper Lee
- El nombre del viento (The Name of the Wind), Patrick Rothfuss
- You Don't Know JS: Async & Performance, Kyle Simpson
- Sapiens: A Brief History of Humankind, Yuval Noah Harari
- The Principles of Object Oriented JavaScript, Nicholas C. Zakas
February
- The Leprechauns of Software Engineering, Laurent Bossavit
- The Wise Man's Fear, Patrick Rothfuss
- Fundamentals of Object-Oriented Design in UML, Meilir Page-Jones
- Old Man's War, John Scalzi
- Cevdet Bey e hijos (Cevdet Bey ve Oğulları), Orhan Pamuk
- Ringworld, Larry Niven
- Why Nations Fail: The Origins of Power, Prosperity, and Poverty, Daron Acemoglu and James A. Robinson
March
- The Grumpy Programmer's Guide To Building Testable PHP Applications, Chris Hartjes
- The Grapes of Wrath, John Steinbeck
- The Coding Dojo Handbook, Emily Bache
- Fahrenheit 451, Ray Bradbury
April
- Implementation Patterns, Kent Beck (3rd time)
- The Power of Habit: Why We Do What We Do in Life and Business, Charles Duhigg
- How to Win Friends and Influence People, Dale Carnegie
- Understanding the Four Rules of Simple Design, Corey Haines (2nd time)
- La llamada de Cthulhu (The Call of Cthulhu), El horror de Dunwich (The Dunwich Horror) and Las ratas en las paredes (The Rats in the Walls), H. P. Lovecraft
- The Tales of Beedle the Bard, J. K. Rowling
May
- The Slow Regard of Silent Things, Patrick Rothfuss
- Building Microservices, Designing Fine-Grained Systems, Sam Newman
- Man's Search for Meaning, Viktor Frankl
- Manifiesto del Partido Comunista (Das Kommunistiche Manifest), K. Marx and F. Engels
- Web Development with Clojure, Build Bulletproof Web Apps with Less Code, Dimitri Sotnikov
- Patterns of Enterprise Application Architecture, Martin Fowler
- The Checklist Manifesto: How to Get Things Right, Atul Gawande
- How to Fail at Almost Everything and Still Win Big: Kind of the Story of My Life, Scott Adams
June
- Pragmatic Thinking and Learning, Refactor Your Wetware, Andy Hunt
- Testable JavaScript, Mark Ethan Trostler
- The Secrets of Consulting: A Guide to Giving & Getting Advice Successfully, Gerald M. Weinberg
- La desfachatez intelectual. Escritores e intelectuales ante la política, Ignacio Sánchez-Cuenca
- REST in Practice, Jim Webber, Savas Parastatidis and Ian Robinson
- Mindset, Carol Dweck
July
- The Call of the Wild, Jack London
- Weinberg on Writing: The Fieldstone Method, Gerald M. Weinberg
- Dinner at the Homesick Restaurant, Anne Tyler
- How to Solve It: A New Aspect of Mathematical Method, G. Polya
- Object Oriented Software Engineering: A Use Case Driven Approach, Ivar Jacobson
- A Far Cry from Kensington, Muriel Spark
August
- The Difference Engine, William Gibson and Bruce Sterling
- Clojure Applied, From Practice to Practitioner, Ben Vandgrift and Alex Miller
- Vampir, Joann Sfar
- Los mares del Sur, Manuel Vázquez Montalbán
- El océano al final del camino (The Ocean at the End of the Lane), Neil Gaiman
- La Guerra Civil Española, Paul Preston and José Pablo García
- Wiser: Getting Beyond Groupthink to Make Groups Smarter, Cass R. Sunstein and Reid Hastie
September
- And The Weak Suffer What They Must?: Europe, Austerity and the Threat to Global Stability, Yanis Varoufakis
- Clojure Reactive Programming, Leonardo Borges
- Designing Object-Oriented Software 1st ed., Rebecca Wirfs-Brock, Brian Wilkerson and Lauren Wiener
- Divina Comedia (Divina Commedia), Dante Alighieri
October
- David Copperfield, Charles Dickens
- Utopía (Utopia), Thomas More
- La pulga de acero (Сказ о тульском косом Левше и о стальной блохе), Nikolái Leskov
- Living Clojure, An Introduction and Training Plan for Developers, Carin Meier
- Buddha in Blue Jeans: An Extremely Short Zen Guide to Sitting Quietly and Being Buddha, Tai Sheridan
November
- De tu tierra (Paesi tuoi), El Camarada (Il Compagno), Cesare Pavese
- A Passage to India, E. M. Forster
- React: Up & Running. Building Web Applications, Stoyan Stefanov
- El relato de Arthur Gordon Pym (The Narrative of Arthur Gordon Pym of Nantucket),Edgar Allan Poe.
December
- The Remains of the Day, Kazuo Ishiguro
- Quiet: The Power of Introverts in a World That Can't Stop Talking, Susan Cain
- Robinson Crusoe, Daniel Defoe
- All you Zombies, Robert A. Heinlein
Tuesday, December 27, 2016
TDD Training at Gradiant (in spanish)
La semana pasada Luis Rovirosa y yo impartimos una formación de TDD en las oficinas de Gradiant en Vigo.
Gradiant es el acrónimo en inglés del Centro Tecnolóxico de Telecomunicacións de Galicia (Galician Research and Development Center in Advanced Telecomunications) y utilizan los conocimientos y experiencia de sus profesionales de investigación para aportar valor a empresas y organizaciones mediante la transferencia de conocimiento y el diseño de productos especializados.

La naturaleza del centro y su ubicación en el campus de la Universidad de Vigo me recordó bastante a la época en que trabajé en el BSC.
Este fue el segundo curso de TDD de Codesai que imparto con Luis, y era todo un reto, debido al número de asistentes, 18 personas, y, sobretodo, a la gran variedad de tecnologías y tipos de sistemas que desarrollan, que iban desde C empotrado para procesado de señales o IOT, hasta APIs REST con node.js, pasando por diversas aplicaciones en Java, C++ o Scala.
Creo que hacer los cursos en pareja es una gran ventaja. Por un lado, porque podemos atender mucho mejor, sobre todo si el grupo es numeroso, a las parejas durante las sesiones prácticas dándoles feedback y contestando sus dudas, para ayudar a que asimilen mejor los conceptos que persigue trabajar cada kata. Por otro lado, porque nos permite asumir roles diferentes, parecidos a los de conductor y navegador en pair-programming, para percibir mejor el pulso de nuestra audiencia y modular mejor nuestro mensaje. En este sentido, Luis y yo, nos complementamos bastante bien porque, al tener backgrounds bastante distintos y personalidades diferentes, podemos enriquecer tanto las explicaciones teóricas como las prácticas, con un rango de experiencias y perpectivas más amplio, lo que considero que fue muy útil, dada la diversidad del grupo.
Algunos de los desarrolladores que asistieron al curso trabajaban en sistemas empotrados haciendo IOT o procesado de señales en los que cuentan con recursos muy limitados y en entornos en los que testear es bastante complicado. Para tratar de ayudarlos, aparte de responder dudas concretas y de las conversaciones de pasillo, reunimos una compilación de recursos interesantes para hacer testing y TDD con C empotrado.
Nos gustó muchísimo la experiencia de trabajar con las personas que asistieron al curso. Era un grupo muy agradable y con muchísimas ganas de aprender. Además, durante el curso, surgieron preguntas y debates muy interesantes debido a la gran diversidad de tipos de proyecto desarrollados por los asistentes.
Hemos acabado reventados después de dos días intensos... Pero también motivadísimos #tdd #craftmanship https://t.co/eXtEBVssmZ
— Javi (@jsrois) December 20, 2016
Mega cursazo! Gracias @trikitrok @luisrovirosa y gracias @Gradiant por traerlos! A ver si vuelven 😉https://t.co/pMuI7aZl2E
— Fernando Vilas (@fer_vilas) December 20, 2016
Como siempre acabamos cansadísimos pero muy contentos porque creo que conseguimos nuestro objetivo de iniciar al grupo en testing y TDD, y de ilusionarlos para empezar a aventurarse en una forma de trabajo diferente.
Antes de terminar quería darle las gracias especialmente a Javi Sánchez Rois y a Fernando Vilas por acogernos, llevarnos a sitios ricos para comer y presentarnos al Dinoseto 😉.
Publicado originalmente en el blog de Codesai.Saturday, December 24, 2016
An example of introducing symmetry to enable duplication removal
"...finding and expressing symmetry is a preliminary step to removing duplication. If a similar thought exists in several places in the code, making them symmetrical to each other is a first good step towards unifying them"In this post we'll look at an example of expressing symmetry as a way to make duplication more visible. This is the initial code of a version of a subset of the Mars Rover kata that Álvaro García and I used in a refactoring workshop some time ago:
public class Rover { | |
private String direction; | |
private int y; | |
private int x; | |
public Rover(int x, int y, String direction) { | |
this.direction = direction; | |
this.y = y; | |
this.x = x; | |
} | |
public void receive(String commandsSequence) { | |
for (int i = 0; i < commandsSequence.length(); ++i) { | |
String command = commandsSequence.substring(i, i + 1); | |
if (command.equals("l") || command.equals("r")) { | |
// Rotate Rover | |
if (direction.equals("N")) { | |
if (command.equals("r")) { | |
direction = "E"; | |
} else { | |
direction = "W"; | |
} | |
} else if (direction.equals("S")) { | |
if (command.equals("r")) { | |
direction = "W"; | |
} else { | |
direction = "E"; | |
} | |
} else if (direction.equals("W")) { | |
if (command.equals("r")) { | |
direction = "N"; | |
} else { | |
direction = "S"; | |
} | |
} else { | |
if (command.equals("r")) { | |
direction = "S"; | |
} else { | |
direction = "N"; | |
} | |
} | |
} else { | |
// Displace Rover | |
int displacement1 = -1; | |
if (command.equals("f")) { | |
displacement1 = 1; | |
} | |
int displacement = displacement1; | |
if (direction.equals("N")) { | |
y += displacement; | |
} else if (direction.equals("S")) { | |
y -= displacement; | |
} else if (direction.equals("W")) { | |
x -= displacement; | |
} else { | |
x += displacement; | |
} | |
} | |
} | |
} | |
} |
- Which command to execute depending on a command code.
- How to execute the command depending on the direction the rover faces.
If we start applying Replace Type Code with State/Strategy refactoring to the code as it is now, there is a subtle problem, though.
Looking carefully more carefully, we can observe that in the case of the rover's displacement, the sequence of two decisions is clearly there:
However, that sequence is not there in the case of the rover's rotations. In this case there's a third decision (conditional) based on the command type which as, we'll see, is not necessary:
This difference between the two sequences of decisions reveals a lack of symmetry in the code. It's important to remove it before starting to refactor the code to substitute the conditionals with polymorphism.
I've seen many cases in which developers naively start extracting methods and substituting the conditionals with polymorphism starting from asymmetrical code like this one. This often leads them to create entangled and leaking abstractions.
Particularly, in this case, blindly applying Replace Type Code with State/Strategy refactoring to the left and right rotations can very likely lead to a solution containing code like the following:
public enum Direction { | |
private final String value; | |
Direction(String value) { | |
this.value = value; | |
} | |
public static Direction from(String value) { | |
for (Direction current : values()) { | |
if (current.value.equals(value)) { | |
return current; | |
} | |
} | |
throw new RuntimeException("Passed value is not correct: " + value); | |
} | |
public Direction rotate(Command command) { | |
if (Command.RIGHT == command) { | |
return rotateRight(); | |
} else { | |
return rotateLeft(); | |
} | |
} | |
protected abstract Direction rotateRight(); | |
protected abstract Direction rotateLeft(); | |
NORTH("N") { | |
@Override | |
protected Direction rotateRight() { | |
return EAST; | |
} | |
@Override | |
protected Direction rotateLeft() { | |
return WEST; | |
} | |
}, | |
SOUTH("S") { | |
@Override | |
protected Direction rotateRight() { | |
return (WEST); | |
} | |
@Override | |
protected Direction rotateLeft() { | |
return (EAST); | |
} | |
}, | |
WEST("W") { | |
@Override | |
protected Direction rotateRight() { | |
return (NORTH); | |
} | |
@Override | |
protected Direction rotateLeft() { | |
return (SOUTH); | |
} | |
}, | |
EAST("E") { | |
@Override | |
protected Direction rotateRight() { | |
return (SOUTH); | |
} | |
@Override | |
protected Direction rotateLeft() { | |
return (NORTH); | |
} | |
}; | |
} |
public enum Command { | |
RIGHT("r"), | |
LEFT("l"); | |
private String value; | |
Command(String value) { | |
this.value = value; | |
} | |
public static Command from(String value) { | |
for (Command current : values()) { | |
if (current.value.equals(value)) { | |
return current; | |
} | |
} | |
throw new RuntimeException("Passed value is not correct: " + value); | |
} | |
} |
This is bad... Some knowledge about the commands encoding has leaked from the Command class into the Direction class. Direction shouldn't be taking that decision in the first place. Neither should it know how commands are encoded. Moreover, this is a decision that was already taken and doesn't need to be taken again.
How can we avoid this trap?
We should go back to the initial code and instead of mechanically applying refactoring, we should start by removing the asymmetry from the initial code.
You can see one way of doing it in this video:
After this refactoring all cases are symmetrical (they present the same sequence of decisions)
We've not only removed the asymmetry but we've also made more explicit the case statements (Case Statements smell) on the command encoding and the duplicated switches on the direction the rover is facing:
public void receive(String commandsSequence) { | |
for (int i = 0; i < commandsSequence.length(); ++i) { | |
String command = commandsSequence.substring(i, i + 1); | |
if (command.equals("l")) { | |
// Rotate Rover to the left | |
if (direction.equals("N")) { | |
direction = "W"; | |
} else if (direction.equals("S")) { | |
direction = "E"; | |
} else if (direction.equals("W")) { | |
direction = "S"; | |
} else { | |
direction = "N"; | |
} | |
} else if (command.equals("r")) { | |
// Rotate Rover to the right | |
if (direction.equals("N")) { | |
direction = "E"; | |
} else if (direction.equals("S")) { | |
direction = "W"; | |
} else if (direction.equals("W")) { | |
direction = "N"; | |
} else { | |
direction = "S"; | |
} | |
} else { | |
// Displace Rover | |
int displacement1 = -1; | |
if (command.equals("f")) { | |
displacement1 = 1; | |
} | |
int displacement = displacement1; | |
if (direction.equals("N")) { | |
y += displacement; | |
} else if (direction.equals("S")) { | |
y -= displacement; | |
} else if (direction.equals("W")) { | |
x -= displacement; | |
} else { | |
x += displacement; | |
} | |
} | |
} | |
} |
If we start the Replace Type Code with State/Strategy refactoring now, it's more likely that we'll end with a code in which Direction knows nothing about how the commands are encoded:
public enum Direction { | |
NORTH { | |
@Override | |
public Coordinates move(Coordinates coordinates, int displacement) { | |
return coordinates.incrementY(displacement); | |
} | |
@Override | |
public Direction rotateRight() { | |
return EAST; | |
} | |
@Override | |
public Direction rotateLeft() { | |
return WEST; | |
} | |
}, SOUTH { | |
@Override | |
public Coordinates move(Coordinates coordinates, int displacement) { | |
return coordinates.incrementY(-displacement); | |
} | |
@Override | |
public Direction rotateRight() { | |
return WEST; | |
} | |
@Override | |
public Direction rotateLeft() { | |
return EAST; | |
} | |
}, EAST { | |
@Override | |
public Coordinates move(Coordinates coordinates, int displacement) { | |
return coordinates.incrementX(displacement); | |
} | |
@Override | |
public Direction rotateRight() { | |
return SOUTH; | |
} | |
@Override | |
public Direction rotateLeft() { | |
return NORTH; | |
} | |
}, WEST { | |
@Override | |
public Coordinates move(Coordinates coordinates, int displacement) { | |
return coordinates.incrementX(-displacement); | |
} | |
@Override | |
public Direction rotateRight() { | |
return NORTH; | |
} | |
public Direction rotateLeft() { | |
return SOUTH; | |
} | |
}; | |
public static Direction pointingTo(String directionCode) { | |
if (directionCode.equals("N")) { | |
return NORTH; | |
} else if (directionCode.equals("S")) { | |
return SOUTH; | |
} else if (directionCode.equals("E")) { | |
return EAST; | |
} else { | |
return WEST; | |
} | |
} | |
abstract public Coordinates move(Coordinates coordinates, int displacement); | |
abstract public Direction rotateRight(); | |
abstract public Direction rotateLeft(); | |
} |
public class CommandCodeInterpreter { | |
private static final int DISPLACEMENT_LENGTH = 1; | |
private static final String FORWARD_MOVEMENT = "f"; | |
private static final String BACKWARD_MOVEMENT = "b"; | |
private static final String RIGHT_ROTATION = "r"; | |
private static final String LEFT_ROTATION = "l"; | |
private static Map<String, Command> knownCommands = knownCommands(); | |
public static Command interpret(String commandCode) { | |
return getCommandFor(commandCode); | |
} | |
private static Command getCommandFor(String commandCode) { | |
return knownCommands.get(commandCode); | |
} | |
private static Map<String, Command> knownCommands() { | |
Map<String, Command> knownCommands = new HashMap<>(); | |
knownCommands.put(FORWARD_MOVEMENT, new Movement(DISPLACEMENT_LENGTH)); | |
knownCommands.put(BACKWARD_MOVEMENT, new Movement(-DISPLACEMENT_LENGTH)); | |
knownCommands.put(RIGHT_ROTATION, new RightRotation()); | |
knownCommands.put(LEFT_ROTATION, new LeftRotation()); | |
return Collections.unmodifiableMap(knownCommands); | |
} | |
} |
Then, by removing those asymmetries, you can make the duplication more visible and disentangle the entangled dimensions of complexity. The only thing you need is to be patient and don't start "obvious" refactorings before thinking a bit about symmetry.
[1] Dimension of Complexity is a term used by Mateu Adsuara in a talk at SocraCan16 to name an orthogonal functionality. In that talk he used dimensions of complexity to group the examples in his test list and help him choose the next test when doing TDD. He talked about it in this three posts: Complexity dimensions - FizzBuzz part I, Complexity dimensions - FizzBuzz part II and Complexity dimensions - FizzBuzz part III. Other names for the same concept that I've heard are axes of change, directions of change or vectors of change.
Sunday, December 18, 2016
Recorded talk about functions in Clojure (in Spanish)
This time we talked a bit about functions and we recorded it as well.
This is the resulting video:
You'll find the examples we used here.
Again I'd like to especially thank Ángel and Magento Barcelona for letting me do the talk from their office.
I hope you find it useful.
Tuesday, December 13, 2016
Interesting Talk: "GOOS style TDD by Example"
Sunday, December 11, 2016
Interesting Talk: "Neurobollocks: el nuevo aceite de serpiente"
Friday, December 9, 2016
TDD Training at Ve Interactive (in spanish)
Hace unas semanas Luis Rovirosa y yo impartimos una formación de TDD en las oficinas de Ve Interactive en Bilbao.
Luis lleva bastante tiempo impartiendo el curso de TDD de Codesai, tanto en solitario como junto con Carlos Blé, pero para mi era la privera vez. Me gustó mucho la estructura y el ritmo que le da Luis al curso.
Se trata un curso muy intenso y eminentemente práctico que dura dos días, cada uno de los cuales se divide en cuatro bloques de dos horas. Al comienzo de cada bloque presentamos una serie de conceptos teóricos, que comentamos y debatimos con el grupo, y rápidamente pasamos a ponerlos en práctica mediante una serie de katas de complejidad y dificultad creciente, en las que acompañamos continuamente a las parejas dándoles feedback y contestando sus dudas, para que los conceptos acaben de ser asimilados.
Fue todo un placer trabajar con los 14 desarrolladores que asistieron al curso. Era un grupo con una actitud genial y con un nivel muy bueno. A pesar de lo intenso del curso, trabajaron con mucho entusiasmo cada uno de los ejercicios y se produjeron debates muy interesantes y constructivos.

Nos fuimos de Bilbao exhaustos, pero muy contentos, tanto por las personas que conocimos como por el feedback que recibimos. Personalmente, me encanto trabajar con Luis y aprendí muchísimo de él.
Publicado originalmente en el blog de Codesai.Sunday, December 4, 2016
Interesting Talk: "Overcoming the Challenges of Mentoring"
Thursday, December 1, 2016
Interesting Talk: "Code Symbiosis, a technical keynote"
Recorded talk about destructuring in Clojure (in Spanish)
Some members of the team wanted to learn Clojure, so we started a small Clojure/ClojureScript study group.
We created a slack called clojuresai where I'm posting some readings (we're reading Clojure for the Brave and True) and exercises (we're working through a selection of exercises from 4Clojure) each week and where they can ask doubts about the exercises and readings, and comment things they find interesting.
Some colleagues from the Clojure Developers Barcelona meetup that are beginning to learn clojure have also joined the study group.
Now and then, we do introductory talks using Google Hangouts (Codesai's team is distributed) to go deeper in some of the topics they've just read in the book.
So far we've done two talks. The first one was about Clojure collections. Unfortunately, we didn't record it, but you can find the examples we used here.
Today we did a second remote talk. This time it was about destructuring in Clojure, and since some of the members of the study group members weren't able to make it, we decided to record it.
This is the resulting video:
You'll find the examples we used in this previous post.
I'd like to especially thank Ángel and Magento Barcelona for letting me do the talk today from their office (their internet connection is much better than mine).
Recently, Miguel de la Cruz (thanks a million Miguel!!) has joined our study group as a tutor. He knows a lot of Clojure so he will help answering doubts and giving some talks that we'll hopefully share with you soon, as well.
Saturday, November 26, 2016
Interesting Talk: "Testing on the Toilet"
Interesting Talk: "3X"
Interesting Talk: "Productivity is Killing Us"
Interesting Talk: " Punishment Driven Development"
Friday, November 25, 2016
Creating custom effects in re-frame
The example with effects we saw, used two of re-frame's built-in effect handlers (dispatch and dispatch-later). Since the set of possible side-effects is open-ended, re-frame gives you a way to define your own effects.
In this post, we'll show how to create your own effect by defining one that writes to the local storage.
Let's have a look at an event handler using that effect that writes to the local storage:
(ns visual-spelling.play.answer.handlers | |
(:require | |
[visual-spelling.db :as db])) | |
(defn word-checked-handler [{:keys [db]} _] | |
(let [db (db/update-on-word-checked db)] | |
{:db db | |
:write-localstore {:visited-words (:visited-words db) | |
:results (:results db)}})) |
We have to use re-frame's reg-fx function to associate the effect key, (:write-localstore), with an effect handler, (local-store/write function):
(ns visual-spelling.register-effects | |
(:require | |
[re-frame.core :as re-frame] | |
[visual-spelling.local-store :as local-store])) | |
(re-frame/reg-fx | |
:write-localstore local-store/write) |
When an event handler returns an effects map which contains a given effect key, the effect handler associated with it using reg-fx will be called to action on the effect, passing it the value associated to the effect key in the effects map.
The local-store/write function is passed a map containing the key-value pairs it has to write to the local store.
(ns visual-spelling.local-store) | |
(defn- write-key | |
[ls-key data] | |
(.setItem js/localStorage ls-key (str data))) | |
(defn- load-key [ls-key] | |
(some->> (.getItem js/localStorage ls-key) | |
(cljs.reader/read-string))) | |
(defn write [data-kv] | |
(doseq [[k v] data-kv] | |
(write-key k v))) | |
(defn load [ls-keys] | |
(reduce | |
#(assoc %1 %2 (load-key %2)) | |
{} | |
ls-keys)) |
(ns visual-spelling.play.answer.word-checked-handler-test | |
(:require | |
[cljs.test :refer-macros [deftest testing is]] | |
[visual-spelling.test-helpers.builders | |
:refer [make-word make-raw-word raw-parts parts]] | |
[visual-spelling.play.answer.handlers :refer [word-checked-handler]] | |
[visual-spelling.test-helpers.db-builder | |
:refer [make-db]] | |
[visual-spelling.test-helpers.handlers-checkers | |
:refer [check-writing-to-local-storage-contains | |
check-writing-to-local-storage]])) | |
(deftest test-right-and-wrong-word-handlers | |
(let [answer-timestamp :some-ts | |
correct-word-text "ess" | |
initial-current-word-index 1 | |
db (make-db | |
:language :catalan | |
:current-word (make-word | |
:status :all-right-answers | |
:correct-word correct-word-text | |
:answer-ts answer-timestamp | |
:parts (parts "e" "s" | |
{:correct-answer "s" | |
:user-answer "s"})) | |
:current-word-index initial-current-word-index | |
:words {"first-word" (make-raw-word) | |
"current-word" (make-raw-word | |
:description "current-word" | |
:parts (raw-parts | |
"e" | |
{:correct-answer "ss" | |
:options ["c", "ç", "k", "s", "ss", "z", "q"]})) | |
"next-word" (make-raw-word | |
:description "next-word" | |
:parts (raw-parts | |
"e" | |
{:correct-answer "ss" | |
:options ["c", "ç", "k", "s", "ss", "z", "q"]}))}) | |
co-fx {:db db} | |
resulting-fx (word-checked-handler co-fx :no-event)] | |
(testing "it saves the visited words in the local storage" | |
(check-writing-to-local-storage-contains | |
resulting-fx :visited-words correct-word-text)) | |
(testing "that right word handler saves the user answer to results" | |
(check-writing-to-local-storage resulting-fx :results)))) |
;; some more code | |
;;..... | |
(defn- extract-from-writing-to-local-storage | |
[fx saved-thing-key] | |
(-> fx :write-localstore saved-thing-key)) | |
(defn check-writing-to-local-storage | |
[fx saved-thing-key] | |
(is (= (extract-from-writing-to-local-storage fx saved-thing-key) | |
(extract-from-db-in-fx-or-cofx fx saved-thing-key)))) | |
(defn check-writing-to-local-storage-contains | |
[fx saved-thing-key expected-content] | |
(is (contains? (extract-from-writing-to-local-storage | |
fx saved-thing-key) | |
expected-content))) | |
;;..... | |
;; some more code |
Sunday, November 20, 2016
Interesting Talk: "No, Really, Learning Clojure Was Hard"
Friday, November 18, 2016
Interesting Talk: "Arquitecturas funcionales: más allá de las lambdas"
Thursday, November 17, 2016
Interesting Podcast: "Cognicast with Paul Stadig"
Quotes from "Why should physicists study history?"
"History is not a list of names and dates. It's a way of thinking that can be powerful and illuminating."
"Social interactions really do influence what scientist produce."
"How a community tells its history changes the way it thinks about itself."
"A historical perspective on science can help physicists understand what is going on when they practice their craft, and it provides numerous tools that are useful for physicists themselves."
"Physics is a social endeavor."
"Research is done by people. And people have likes and dislikes, egos and prejudices. Physicists, like everyone else get attached to their favorite ideas and hang on them perhaps long after they should let them go."
"The history of science can help dismantle the myth of the purely rational genius living outside the everyday world. It makes physics more human."
"And a more human physics is a good thing. For starters, it makes physics more accessible."
"A field in which people are aknowledged as people is much more appealing than one in which they are just calculating machines."
"Physics only work when people talk to each other and communication is not always easy."
"Everything seems obvious in retrospect."
"The history of physics can remind us how difficult is to justify ideas that now seem obvious."
"Complexity, not simplicity , has ruled the practice of science."
"Every discovery has come out of a messy mix of people, ideas, accidents and arguments."
"Students and young researchers are often heartened to learn that physics is hard work and that it is ok for their own efforts not to look like a text-boo presentation. Messiness is a standard. Mistakes are normal. The results of physics are not self-evident."
"The history of physics suggests that there are usually several ways to approach a problem."
"Turning complexity into good physics requires creativity. You can never tell what weird idea will help clarify a confusing observation or provide the key to interpreting an equation. History uncovers the strange stew of concepts that were necessary for the development of physics."
"The interplay of various approaches is what brought us a modern view."
"Strange but ultimately useful perspectives come from fields and disciplines apparently distant from the problem at hand."
"The history of science shows how important it is for scientists across different fields to talk to each other. Conversations among separate groups are healthy. Apparently isolated problems are often closedly tied together, and you never know where you will find the weird idea that solves your difficulty."
"The best strategy for encouraging diverse ideas is to cultivate a diverse community."
"Underrepresented groups that offer different ways of thingking are often the source of fresh insights and novel methods."
"Underrepresented groups are usually marginalized because of cultural inertia or deliberate decisions made long ago."
"The diversity of ideas and interpretations serves as a reminder that physics is a work in progress. Knowledge is provisional. There are always new ways to tackle a problem, and there is always more to be learned."
"Accepting uncertainty would require changes in how science is taught."
"Curiosity should be revered, and everyone should be encouraged to ask, what else?"
"If scientist are not explicit and honest about their doubts, a crisis of confidence arises when that uncertainty is revealed."
"Physics wasn't always as it is."
"The flip side of accepting physics will be different in the future is accepting that it was different in the past. Everyone has a tendency to assume that the way things are now is the norm. But history makes it clear that things were not always this way. An understanding of why people used to think differently is a powerful tool for understanding people today. By drawing attention to older, usnpoken assumptions, history shows us how to start paying attention to our own."
"A knowledge of the historic and physical background, gives the kind of independence from prejudices of his generation from which most scientists are suffering" Einstein
"..they should study the history of those ideas and understand the circumstances in which they were justified and found useful."
"History trains you to think critically about received ideas. History provides evidence of roads not taken."
"Science's plurality of interpretation can make the history of science a resource for modern scientific research." Hasok Chang"Complementary Science -> recovering forgotten and unsolved puzzles from the past."
"Putting complementary science into practice demands difficult self-examination. Thinking deeply about assumptions and accepted knowledge can be hard to do in professional scientific contexts, but history is a mode in which it's encouraged."
"The simple realization that people used to think differently can be quite powerful."
"Physics doesn't have rigid rules."
"Scientist simply don't follow a rigid, linear problem-solving system. Sometimes they start with a hypothesis, sometimes with a strange observation, sometimes with a weird anomaly in an otherwise straightforward experiment."
"... a scientist must be an "unscrupulous opportunist", adopting and adapting various approaches as new challenges arise."
"History teaches that knowledge is not fixed."
"Engaging with history will teach you to understand ideas on their own terms."
"Historical thinking makes its subject dynamic. It helps you think about science as a series of questions rather than a series of statements. Those questions will continue into the future, and it is helpful to know what has been asked so far." "In the end, history of science exposes scientists to new ways of thinking and forces them to reexamine what is already known. Such intellectual flexibility is essential for any discipline, but it's particularly important for fields as influential and authoritative as physics and other sciences."
Wednesday, November 16, 2016
Interesting Talk: "So Long, and Thanks for All the Tests"
Thursday, November 10, 2016
Using effects in re-frame
However, as we said, to build a program that does anything useful, it's inevitable to have some side-effects and/or side-causes. So, there will be many cases in which event handlers won't be pure functions.
We also saw how using coeffects in re-frame allows to have pure event handlers in the presence of side-causes.
In this post, we'll focus on side-effects:
If a function modifies some state or has an observable interaction with calling functions or the outside world, it no longer behaves as a mathematical (pure) function, and then it is said that it does side-effects.Let's see some examples of event handlers that do side-effects (from a code animating the evolution of a cellular automaton):
(ns cellular-animation.handlers | |
(:require | |
[cellular-animation.evolution :as evolution] | |
[re-frame.core :as re-frame])) | |
(defn evolve [db _] | |
(if (:evolving db) | |
(let [db (update | |
db :automaton-states | |
(partial | |
evolution/evolve (:rule db)))] | |
(js/setTimeout | |
(fn [] (re-frame/dispatch [:evolve])) | |
100) ; <- side-effect! | |
db) | |
db)) | |
(defn start-stop-evolution [db _] | |
(let [db (update db :evolving not)] | |
(re-frame/dispatch [:evolve]) ; <- side-effect! | |
db)) |
(ns cellular-animation.register-handlers | |
(:require | |
[re-frame.core :as re-frame] | |
[cellular-animation.db :as db] | |
[cellular-animation.handlers :as handlers])) | |
(re-frame/reg-event-db | |
:initialize-db | |
(fn [_ _] | |
db/default-db)) | |
(re-frame/reg-event-db | |
:evolution-started-or-stopped | |
handlers/start-stop-evolution) | |
(re-frame/reg-event-db | |
:evolve | |
handlers/evolve) |
These impure event handlers are hard to test. In order to test them, we'll have to somehow spy the calls to the function that is doing the side-effect (the dispatch). Like in the case of side-causes from our previous post, there are many ways to do this in ClojureScript, (see Isolating external dependencies in Clojure), only that, in this case, the code required to test the impure handler will be a bit more complex, because we need to keep track of every call made to the side-effecting function.
In this example, we chose to make explicit the dependency that the event handler has on the side-effecting function, and inject it into the event handler which becomes a higher order function. Actually, we injected a wrapper of the side-effecting function in order to create an easier interface.
Notice how the event handlers, evolve and start-stop-evolution, now receive as its first parameter the function that does the side-effect, which are dispatch-later-fn and dispatch, respectively.
(ns cellular-animation.handlers | |
(:require | |
[cellular-animation.evolution :as evolution])) | |
(defn evolve [dispatch-later-fn db _] | |
(if (:evolving db) | |
(let [db (update | |
db :automaton-states | |
(partial evolution/evolve (:rule db)))] | |
(dispatch-later-fn :evolve 100) | |
db) | |
db)) | |
(defn start-stop-evolution [dispatch-fn db _] | |
(let [db (update db :evolving not)] | |
(dispatch-fn :evolve) | |
db)) |
(ns cellular-animation.register-handlers | |
(:require | |
[re-frame.core :as re-frame] | |
[cellular-animation.db :as db] | |
[cellular-animation.handlers :as handlers] | |
[cellular-animation.dispatchers :as dispatchers])) | |
(re-frame/reg-event-db | |
:initialize-db | |
(fn [_ _] | |
db/default-db)) | |
(re-frame/reg-event-db | |
:evolution-started-or-stopped | |
(partial | |
handlers/start-stop-evolution | |
dispatchers/dispatch)) | |
(re-frame/reg-event-db | |
:evolve | |
(partial | |
handlers/evolve | |
dispatchers/dispatch-later)) |
(ns cellular-animation.dispatchers | |
(:require | |
[re-frame.core :as re-frame])) | |
(defn dispatch [event] | |
(re-frame/dispatch [event])) | |
(defn dispatch-later [event ms] | |
(js/setTimeout | |
(fn [] (dispatch event)) | |
ms)) |
(ns cellular-animation.handlers-test | |
(:require | |
[cljs.test :refer-macros [deftest testing is]] | |
[cellular-animation.handlers :as handlers] | |
[cellular-animation.rules :as rules])) | |
(defn- make-spy [args] | |
(fn [& parameters] (swap! args conj parameters))) | |
(deftest evolve-handler-test | |
(testing | |
"when the automaton is evolving it calls itself after changing its state" | |
(let [initial-states [[1]] | |
expected-states [[0 1 0] [1 0 1]] | |
db {:automaton-states initial-states | |
:rule rules/rule-90 | |
:evolving true} | |
args (atom []) | |
dispatch-later-fn (make-spy args) | |
evolve (partial | |
handlers/evolve dispatch-later-fn) | |
resulting-db (evolve db :not-used-event)] | |
(is (= resulting-db | |
(merge db {:automaton-states expected-states}))) | |
(let [first-call-args (nth @args 0)] | |
(is (= (count first-call-args) 2)) | |
(is (= (first first-call-args) :evolve)) | |
(is (= (second first-call-args) 100)))))) | |
(testing | |
"when the automaton is not evolving it does not change its state" | |
(let [db :some-db | |
args (atom []) | |
dispatch-later-fn (make-spy args) | |
evolve (partial | |
handlers/evolve dispatch-later-fn) | |
resulting-db (evolve db :not-used-event)] | |
(is (= resulting-db db)) | |
(is (zero? (count @args))))) | |
(deftest start-stop-evolution-handler-test | |
(testing | |
"when the automaton is evolving it stops the evolution" | |
(let [db {:evolving true} | |
args (atom []) | |
dispatch (make-spy args) | |
start-stop-evolution (partial | |
handlers/start-stop-evolution dispatch) | |
resulting-db (start-stop-evolution db :not-used-event)] | |
(is (= resulting-db (assoc db :evolving false))) | |
(let [first-call-args (nth @args 0)] | |
(is (= (count first-call-args) 1)) | |
(is (= (first first-call-args) :evolve))))) | |
(testing | |
"when the automaton is not evolving it starts the evolution" | |
(let [db {:evolving false} | |
args (atom []) | |
dispatch (make-spy args) | |
start-stop-evolution (partial | |
handlers/start-stop-evolution dispatch) | |
resulting-db (start-stop-evolution db :not-used-event)] | |
(is (= resulting-db (assoc db :evolving true))) | |
(let [first-call-args (nth @args 0)] | |
(is (= (count first-call-args) 1)) | |
(is (= (first first-call-args) :evolve)))))) |
Using test doubles makes the event handler testable again, but it's still impure, so we have not only introduced more complexity to test it, but also, we have lost the two other advantages cited before: local reasoning and events replay-ability.
Since re-frame's 0.8.0 (2016.08.19) release, this problem has been solved by introducing the concept of effects and coeffects.
Whereas, in our previous post, we saw how coeffects can be used to track what your program requires from the world (side-causes), in this post, we'll focus on how effects can represent what your program does to the world (side-effects). Using effects, we'll be able to write effectful event handlers that keep being pure functions.
Let's see how the previous event handlers look when we use effects:
(ns cellular-animation.handlers | |
(:require | |
[cellular-animation.evolution :as evolution])) | |
(defn evolve [{:keys [db]} _] | |
(if (:evolving db) | |
{:db (update db :automaton-states | |
(partial evolution/evolve (:rule db))) | |
:dispatch-later [{:ms 100 :dispatch [:evolve]}]} | |
{:db db})) | |
(defn start-stop-evolution [{:keys [db]} _] | |
{:db (update db :evolving not) | |
:dispatch [:evolve]}) |
In this particular case, when the automaton is evolving, the evolve event handler is returning a map of effects which contains two effects represented as key/value pairs. The one with the :db key describes the effect of resetting the application state to a new value. The other one, with the :dispatch-later key describes the effect of dispatching the :evolve event after waiting 100 microseconds. On the other hand, when the automaton is not evolving, the returned effect describes that the application state will be reset to its current value.
Something similar happens with the start-stop-evolution event handler. It returns a map of effects also containing two effects. The one with the :db key describes the effect of resetting the application state to a new value, whereas the one with the :dispatch key describes the effect of immediately dispatching the :evolve event.
The effectful event handlers are pure functions that accept two arguments, being the first one a map of coeffects, and return, after doing some computation, an effects map which is a description of all the side-effects that need to be done by re-frame.
As we saw in the previous post about coeffectts, re-frame's effectful event handlers are registered using the reg-event-fx function:
(ns cellular-animation.register-handlers | |
(:require | |
[re-frame.core :as re-frame] | |
[cellular-animation.db :as db] | |
[cellular-animation.handlers :as handlers])) | |
(re-frame/reg-event-db | |
:initialize-db | |
(fn [_ _] | |
db/default-db)) | |
(re-frame/reg-event-fx | |
:evolution-started-or-stopped | |
handlers/start-stop-evolution) | |
(re-frame/reg-event-fx | |
:evolve | |
handlers/evolve) |
(ns cellular-animation.handlers-test | |
(:require | |
[cljs.test :refer-macros [deftest testing is]] | |
[cellular-animation.handlers :as handlers] | |
[cellular-animation.rules :as rules])) | |
(deftest evolve-handler-test | |
(testing | |
"when the automaton is evolving it calls itself after changing its state" | |
(let [initial-states [[1]] | |
expected-states [[0 1 0] [1 0 1]] | |
db {:automaton-states initial-states | |
:rule rules/rule-90 | |
:evolving true}] | |
(is (= (handlers/evolve {:db db} []) | |
{:db (merge db {:automaton-states expected-states}) | |
:dispatch-later [{:ms 100 :dispatch [:evolve]}]})))) | |
(testing | |
"when the automaton is not evolving it does not change its state" | |
(let [db {:automaton-states :not-used-initial-states | |
:rule :not-used-rule | |
:evolving false}] | |
(is (= (handlers/evolve {:db db} []) {:db db}))))) | |
(deftest start-stop-evolution-test | |
(testing | |
"when the automaton is evolving it stops the evolution" | |
(let [db {:evolving true}] | |
(is (= (handlers/start-stop-evolution {:db db} []) | |
{:db (assoc db :evolving false) | |
:dispatch [:evolve]})))) | |
(testing | |
"when the automaton is not evolving it starts the evolution" | |
(let [db {:evolving false}] | |
(is (= (handlers/start-stop-evolution {:db db} []) | |
{:db (assoc db :evolving true) | |
:dispatch [:evolve]}))))) |
:dispatch and :dispatch-later are builtin re-frame effect handlers already defined. It's possible to create your own effect handlers. We'll explain how and show an example in a future post.
Interesting Paper: "Designing software for ease of extension and contraction"
Wednesday, November 9, 2016
Interesting Talk: "A brief history and mishistory of modularity"
Wednesday, November 2, 2016
Codesai
Many reasons made my path converge with Codesai's one.
First of all, the people in Codesai.
I met Carlos in 2011, because he was teaching the first TDD course I attended. Carlos is a humble and honest person that tries very hard to make sure his actions match what he believes. Later I've known him as a quiet, open-minded leader who is always willing to listen what other people have to say, and this permeates the whole Codesai team. During the last two years, I had the opportunity to meet most of Codesai's apprentices. They are very enthusiastic and eager to learn. I had great fun participating with them in some Software Craftsmanship Gran Canaria events and talking about code and life in general in Codesai's apartment.
I share Codesai's values and feel very confortable with their culture and communication style because the team creates a healthy and safe environment which respects the diversity of its members and the debate of ideas.
I met the rest of the team last week during Codesai's summit (aka #flejesai16) and it was a confirmation that I'm in the right place.
Second, the opportunity to learn from experienced XP practicioners.
I've been developing software for 6 years now. I've learned a lot, most of it by reading books, practising on my own, attending courses and from colleagues, both from the community and at work. However, I felt the need of having a mentor, because I think I'll learn more and better than if I keep working on my own. I've found some great mentors in Codesai who acknowledge that I need a safe environment to get experience step by step.
Finally, the last reason is more personal.
I live in Barcelona but I'm from Canary Islands. Codesai is a remote-first team but the core of Codesai's team is currently in my hometown in the Canary Islands. So being a part of Codesai also gives me the chance of visiting my family more often.
So far I'm enjoying the Codesai experience a lot.
I think we'll keep on evolving as a team and do great things.
Thanks a million for letting me in!
Tuesday, November 1, 2016
Interesting Paper: "Design Methodology for Reliable Software Systems"
Monday, October 24, 2016
Using coeffects in re-frame
This is important because the fact that event handlers are pure functions brings great advantages:
- Local reasoning, which decreases the cognitive load required to understand the code.
- Easier testing, because pure functions are much easier to test.
- Events replay-ability, you can imagine a re-frame application as a reduce (left fold) that proceeds step by step. Following this mental model, at any point in time, the value of the application state would be the result of performing a reduce over the entire collection of events dispatched in the application up until that time, being the combining function for this reduce the set of registered event handlers.
However, to build a program that does anything useful, it's inevitable to have some side-effects and/or side-causes. So, there would be cases in which event handlers won't be pure functions.
In this post, we'll focus on side-causes.
"Side-causes are data that a function, when called, needs but aren't in its argument list. They are hidden or implicit inputs."Let's see an example:
(ns visual-spelling.play.answer.handlers | |
(:require | |
[re-frame.core :as re-frame] | |
[visual-spelling.db :as db] | |
[visual-spelling.words-in-use :as words-in-use])) | |
(re-frame/reg-event-db | |
:word-ready-to-check | |
(fn [db _] | |
(update | |
(db/save-user-answer | |
(js/Date.now) ;; <- side-cause!! | |
db) | |
:words-in-use | |
words-in-use/check-current-word))) |
To be able to test this event handler we'll have to somehow stub the function that produces the side-cause. In ClojureScript, there are many ways to do this (using with-redefs, with-bindings, a test doubles library like CircleCI's bond, injecting and stubbing the dependency, etc.).
In this example, we chose to make the dependency that the handler has on a function that returns the current timestamp explicit and inject it into the event handler which becomes a higher order function.
(ns visual-spelling.play.answer.handlers | |
(:require | |
[visual-spelling.db :as db] | |
[visual-spelling.words-in-use :as words-in-use])) | |
(defn word-ready-to-check-handler [time-fn db _] | |
(update (db/save-user-answer (time-fn) db) | |
:words-in-use | |
words-in-use/check-current-word)) |
And this would be how before registering that event handler with reg-event-db, we perform a partial application to inject JavaScript's Date.now function into it:
(ns visual-spelling.play.answer.register-handlers | |
(:require | |
[re-frame.core :as re-frame] | |
[visual-spelling.play.answer.handlers :as handlers])) | |
(re-frame/reg-event-db | |
:word-ready-to-check | |
(partial handlers/word-ready-to-check-handler js/Date.now)) |
(deftest test-word-ready-to-check-handler | |
(testing "when answer is correct, saves current word and updates status" | |
(let [time-fn (stub-time-fn :any-timestamp) | |
right-answered-word (make-word | |
:correct-word "o" | |
:status :all-gaps-answered | |
:parts (parts {:correct-answer "o" | |
:options ["a" "o" "e" "u"] | |
:user-answer "o"})) | |
db (build-db :current-word right-answered-word) | |
resulting-db (answer-handlers/word-ready-to-check-handler | |
time-fn db :no-event)] | |
(check-status | |
resulting-db :all-right-answers) | |
(check-results-contains | |
resulting-db (results/correct "o" :any-timestamp))))) |
(ns visual-spelling.tests-helpers) | |
(defn stub-time-fn [& timestamps] | |
(let [a-ts (atom timestamps)] | |
(fn [] | |
(let [ts (first @a-ts)] | |
(swap! a-ts rest) | |
ts)))) |
The bottom line problem is that the event handler is not a pure function. This makes us lose not only the easiness of testing, as we've seen, but also the rest of advantages cited before: local reasoning and events replay-ability. It would be great to have a way to keep event handlers pure, in the presence of side-effects and/or side-causes.
Since re-frame's 0.8.0 (2016.08.19) release, this problem has been solved by introducing the concept of effects and coeffects. Effects represent what your program does to the world (side-effects) while coeffects track what your program requires from the world (side-causes). Now we can write effectful event handlers that keep being pure functions.
Let's see how to use coeffects in the previous event handler example. As we said, coeffects track side-causes, (see for a more formal definition Coeffects The next big programming challenge).
At the beginning, we had this impure event handler:
(ns visual-spelling.play.answer.handlers | |
(:require | |
[re-frame.core :as re-frame] | |
[visual-spelling.db :as db] | |
[visual-spelling.words-in-use :as words-in-use])) | |
(re-frame/reg-event-db | |
:word-ready-to-check | |
(fn [db _] | |
(update | |
(db/save-user-answer | |
(js/Date.now) ;; <- side-cause!! | |
db) | |
:words-in-use | |
words-in-use/check-current-word))) |
(ns visual-spelling.play.answer.handlers | |
(:require | |
[visual-spelling.db :as db] | |
[visual-spelling.words-in-use :as words-in-use])) | |
(defn word-ready-to-check-handler [time-fn db _] | |
(update (db/save-user-answer (time-fn) db) | |
:words-in-use | |
words-in-use/check-current-word)) |
(ns visual-spelling.play.answer.handlers | |
(:require | |
[visual-spelling.db :as db] | |
[visual-spelling.words-in-use :as words-in-use])) | |
(defn word-ready-to-check-handler [cofx _] | |
(let [db (update (db/save-user-answer (:timestamp cofx) (:db cofx)) | |
:words-in-use | |
words-in-use/check-current-word)] | |
{:db db})) |
The map of coeffects, cofx, is the complete set of inputs required by the event handler to perform its computation. Notice how the application state is just another coeffect.
This is the same event handler but using destructuring (which is how I usually write them):
(ns visual-spelling.play.answer.handlers | |
(:require | |
[visual-spelling.db :as db] | |
[visual-spelling.words-in-use :as words-in-use])) | |
(defn word-ready-to-check-handler [{:keys [db timestamp]} _] | |
(let [db (update (db/save-user-answer timestamp db) | |
:words-in-use | |
words-in-use/check-current-word)] | |
{:db db})) |
We need to do two things previously:
First, we register the event handler using re-frame's reg-event-fx instead of reg-event-db.
When you use reg-event-db to associate an event id with the function that handles it, its event handler, that event handler gets as its first argument, the application state, db.
While event handlers registered via reg-event-fx also get two arguments, the first argument is a map of coeffects, cofx, instead of the application state. The application state is still passed in the cofx map as a coeffect associated to the :db key, it's just another coeffect. This is how the previous pure event handler gets registered:
(ns visual-spelling.play.answer.register-handlers | |
(:require | |
[re-frame.core :as re-frame] | |
[visual-spelling.play.answer.handlers :as handlers])) | |
(re-frame/reg-event-fx | |
:word-ready-to-check | |
[(re-frame/inject-cofx :timestamp)] ;; <- interceptor injects coeffect | |
handlers/word-ready-to-check-handler) |
In this example, we are passing an interceptor created using re-frame's inject-cofx function which returns an interceptor that will load a key/value pair (coeffect id/coeffect value) into the coeffects map just before the event handler is executed.
Second, we factor out the coeffect handler, and then register it using re-frame's reg-cofx. This function associates a coeffect id with the function that injects the corresponding key/value pair into the coeffects map. This function is known as the coeffect handler. For this example, we have:
(ns visual-spelling.play.answer.register-handlers | |
(:require | |
[re-frame.core :as re-frame])) | |
(re-frame/reg-cofx | |
:timestamp | |
(fn [cofx _] | |
(assoc cofx :timestamp (js/Date.now)))) |
(deftest test-word-ready-to-check-handler | |
(testing "when answer is correct, saves current word and updates status" | |
(let [right-answered-word (make-word | |
:correct-word "o" | |
:status :all-gaps-answered | |
:parts (parts {:correct-answer "o" | |
:options ["a" "o" "e" "u"] | |
:user-answer "o"})) | |
cofx {:db (build-db :current-word right-answered-word) | |
:timestamp :any-timestamp} | |
resulting-fx (answer-handlers/word-ready-to-check-handler | |
cofx :no-event)] | |
(check-status | |
resulting-fx :all-right-answers) | |
(check-results-contains | |
resulting-fx (results/correct "o" :any-timestamp))))) |
More importantly, we've also regained the advantages of local reasoning and events replay-ability that comes with having pure event handlers.
In future posts, we'll see how we can do something similar using effects to keep events handlers pure in the presence of side-effects.
Wednesday, October 12, 2016
Interesting Talk: "ClojureScript Made Easy"
Friday, October 7, 2016
Interesting Talk: "Leaving side effects aside"
Refactoring tests using builder functions in Clojure/ClojureScript
While this is true when the data are simple, it's less so when the data are nested, complex structures. In that case, using literals can hinder refactoring and thus become an obstacle to adapting to changes.
The problem with using literals for complex, nested data is that the knowledge about how to build such data is spread all over the tests. There are many tests that know about the representation of the data.
In that scenario, nearly any change in the representation of those data will have a big impact on the tests code because it will force us to change many tests.
This is an example of a test using literals, (from a ClojureScript application using re-frame, I'm developing with Francesc), to prepare the application state (usually called db in re-frame):
(deftest test-right-and-wrong-word-handlers | |
(let [co-fx {:db | |
{:language :catalan | |
:words-in-use {:current-word {:correct-word "ess" | |
:answer-ts :any-ts | |
:parts [(text/make "e") | |
(tests-helpers/answered-gap "ss" [] 1 "ss")]} | |
:current-word-index 1 | |
:visited-words {} | |
:words {"first-word" {} | |
"current-word" {:description "current-word" | |
:parts ["e" | |
{:correct-answer "ss" | |
:options ["c", "ç", "k", "s", "ss", "z", "q"]}]} | |
"next-word" {:description "next-word" | |
:parts ["e" | |
{:correct-answer "ss" | |
:options ["c", "ç", "k", "s", "ss", "z", "q"]}]}} | |
:words-keys ["first-word" "current-word" "next-word"]}}}] | |
(testing "that right word handler selects the next word" | |
(check-current-word-is-new | |
(answer-handlers/right-word-handler co-fx :no-event) | |
:description "next-word" :current-word-index 2)) | |
(testing "that wrong word handler repeats the same word" | |
(check-current-word-is-new | |
(answer-handlers/wrong-word-handler co-fx :no-event) | |
:description "current-word" :current-word-index 1)))) |
There were many other tests doing something similar at some nesting level of the db.
To make things worse, at that moment, we were still learning a lot about the domain, so the structure of the db was suffering changes with every new thing we learned.
The situation was starting to be painful, since any refactoring provoke many changes in the tests, so we decided to fix it.
What we wanted was a way to place all the knowledge about the representation of the db in just one place (i.e., remove duplication), so that, in case we needed to change that representation, the impact of the change would be absorbed by changing only one place.
A nice way of achieving this goal in object-oriented code, and at the same time making your tests code more readable, is by using test data builders which use the builder pattern, but how can we do these builders in Clojure?
Option maps or function with keyword arguments are a nice alternative to traditional builders in dynamic languages such as Ruby or Python.
In Clojure we can compose functions with keyword arguments to get very readable builders that also hide the representation of the data.
We did TDD to write these builder functions. These are the tests for one of them, the db builder:
(ns visual-spelling.test-helpers.db-builder-test | |
(:require | |
[cljs.test :refer-macros [deftest testing is]] | |
[visual-spelling.test-helpers.db-helpers :as db-helpers] | |
[visual-spelling.test-helpers.db-builder :refer [make-db]] | |
[visual-spelling.db :as db])) | |
(deftest test-db-builder | |
(is (= (make-db) db/default-db)) | |
(is (= (make-db :results :some-results) | |
(assoc db/default-db :results :some-results))) | |
(is (= (make-db :language :some-language) | |
(assoc db/default-db :language :some-language))) | |
(let [db (make-db :words {:some-word {} :another-word {}})] | |
(is (= (db-helpers/extract-from-db db :words) | |
{:some-word {} :another-word {}})) | |
(is (= (set (db-helpers/extract-from-db db :words-keys)) | |
(set [:some-word :another-word])))) | |
(is (= (db-helpers/extract-from-db | |
(make-db :current-word-index :some-index) :current-word-index) | |
:some-index)) | |
(is (= (db-helpers/extract-from-db | |
(make-db :current-word :some-word) :current-word) | |
:some-word)) | |
(is (= (make-db :words-in-use :some-words-in-use) | |
(assoc db/default-db :words-in-use :some-words-in-use))) | |
(is (= (make-db :words-per-session :some-words-per-session) | |
(assoc db/default-db :words-per-session :some-words-per-session))) | |
(is (= (make-db :visited-words :some-:visited-words) | |
(assoc db/default-db :visited-words :some-:visited-words)))) |
(ns visual-spelling.test-helpers.db-builder | |
(:require | |
[visual-spelling.db :as db])) | |
(defn- property-used? [v] | |
(not= v ::not-used)) | |
(defn- assoc-if-used [m k v] | |
(if (property-used? v) | |
(assoc m k v) | |
m)) | |
(defn make-words-in-use | |
[default-word-in-use words-in-use words current-word-index current-word] | |
(let [words-keys (if (property-used? words) (keys words) ::not-used)] | |
(if (property-used? words-in-use) | |
words-in-use | |
(-> default-word-in-use | |
(assoc-if-used :words-keys words-keys) | |
(assoc-if-used :words words) | |
(assoc-if-used :current-word-index current-word-index) | |
(assoc-if-used :current-word current-word))))) | |
(defn make-db | |
[& {:keys [language words current-word-index current-word | |
visited-words results words-in-use words-per-session] | |
:or {language ::not-used | |
words ::not-used | |
current-word-index ::not-used | |
current-word ::not-used | |
visited-words ::not-used | |
results ::not-used | |
words-in-use ::not-used | |
words-per-session ::not-used}}] | |
(let [words-in-use (make-words-in-use | |
(:words-in-use db/default-db) | |
words-in-use | |
words current-word-index current-word)] | |
(-> db/default-db | |
(assoc :words-in-use words-in-use) | |
(assoc-if-used :language language) | |
(assoc-if-used :results results) | |
(assoc-if-used :words-per-session words-per-session) | |
(assoc-if-used :visited-words visited-words)))) |
After creating builder functions for some other data used in the project, our test started to read better and to be robust against changes in the structure of db.
For instance, this is the code of the test I showed at the beginning of the post, but now using builder functions instead of literals:
(deftest test-right-and-wrong-word-handlers | |
(let [db (make-db | |
:language :catalan | |
:current-word (make-word | |
:correct-word "ess" | |
:answer-ts :any-ts | |
:parts (parts "e" | |
{:correct-answer "ss" | |
:user-answer "ss"})) | |
:current-word-index 1 | |
:words {"first-word" (make-raw-word) | |
"current-word" (make-raw-word | |
:description "current-word" | |
:parts (raw-parts | |
"e" | |
{:correct-answer "ss" | |
:options ["c", "ç", "k", "s", "ss", "z", "q"]})) | |
"next-word" (make-raw-word | |
:description "next-word" | |
:parts (raw-parts | |
"e" | |
{:correct-answer "ss" | |
:options ["c", "ç", "k", "s", "ss", "z", "q"]}))}) | |
co-fx {:db db}] | |
(testing "that right word handler selects the next word" | |
(check-current-word-is-new | |
(right-word-handler co-fx :no-event) :description "next-word" :current-word-index 2)) | |
(testing "that wrong word handler repeats the same word" | |
(check-current-word-is-new | |
(wrong-word-handler co-fx :no-event) :description "current-word" :current-word-index 1)))) |
We have seen how, by composing builder functions and using them in our tests, we managed to reduce the surface of the impact that changes in the representation of data might have on our tests. Builder functions absorb the impact of those changes, and enable faster refactoring, and, by doing so, enable adapting to changes faster.
Interesting Talk: "Dependency management in Clojure"
Thursday, October 6, 2016
Interesting Talk: "Growing Clojure systems. From functions to microservices"
Monday, October 3, 2016
Interesting Interview: "Objects, Functions, Virtual Machines, IDEs and More"
Interesting Talk: "Native mobile apps with ClojureScript"
Friday, September 30, 2016
Kata: Variation on Cellular Automata, animating automaton evolution using re-frame
We managed to make a rough version but we didn't have time to test it and make it nice.
When I got home I redid the exercise from scratch using a mix of TDD and REPL Driven Development.
First, I coded a couple of evolution rules. These are their tests:
(ns cellular-animation.rules-test | |
(:require | |
[cljs.test :refer-macros [deftest testing is]] | |
[cellular-animation.rules :as rules])) | |
(deftest rules-tests | |
(testing "rule 90" | |
;+-----------------------------------------------------------------+ | |
;| Neighborhood | 111 | 110 | 101 | 100 | 011 | 010 | 001 | 000 | | |
;+-----------------------------------------------------------------+ | |
;| New Center Cell | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 0 | | |
;+-----------------------------------------------------------------+ | |
(is (= (rules/rule-90 [1 1 1]) 0)) | |
(is (= (rules/rule-90 [1 1 0]) 1)) | |
(is (= (rules/rule-90 [1 0 1]) 0)) | |
(is (= (rules/rule-90 [1 0 0]) 1)) | |
(is (= (rules/rule-90 [0 1 1]) 1)) | |
(is (= (rules/rule-90 [0 1 0]) 0)) | |
(is (= (rules/rule-90 [0 0 1]) 1)) | |
(is (= (rules/rule-90 [0 0 0]) 0))) | |
(testing "rule 30" | |
;+-----------------------------------------------------------------+ | |
;| Neighborhood | 111 | 110 | 101 | 100 | 011 | 010 | 001 | 000 | | |
;+-----------------------------------------------------------------+ | |
;| New Center Cell | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | | |
;+-----------------------------------------------------------------+ | |
(is (= (rules/rule-30 [1 1 1]) 0)) | |
(is (= (rules/rule-30 [1 1 0]) 0)) | |
(is (= (rules/rule-30 [1 0 1]) 0)) | |
(is (= (rules/rule-30 [1 0 0]) 1)) | |
(is (= (rules/rule-30 [0 1 1]) 1)) | |
(is (= (rules/rule-30 [0 1 0]) 1)) | |
(is (= (rules/rule-30 [0 0 1]) 1)) | |
(is (= (rules/rule-30 [0 0 0]) 0)))) |
(ns cellular-animation.rules) | |
(def rule-90 | |
{[1 1 1] 0 | |
[1 1 0] 1 | |
[1 0 1] 0 | |
[1 0 0] 1 | |
[0 1 1] 1 | |
[0 1 0] 0 | |
[0 0 1] 1 | |
[0 0 0] 0}) | |
(def rule-30 | |
{[1 1 1] 0 | |
[1 1 0] 0 | |
[1 0 1] 0 | |
[1 0 0] 1 | |
[0 1 1] 1 | |
[0 1 0] 1 | |
[0 0 1] 1 | |
[0 0 0] 0}) |
(ns cellular-animation.evolution-test | |
(:require | |
[cljs.test :refer-macros [deftest testing is]] | |
[cellular-animation.evolution :as evolution] | |
[cellular-animation.rules :as rules])) | |
(deftest cellular-automaton-evolution | |
(testing "evolution from an initial state" | |
(is (= (evolution/evolve rules/rule-90 [[1]]) | |
[[0 1 0] | |
[1 0 1]])) | |
(is (= (evolution/evolve rules/rule-90 [[0 1 0] [1 0 1]]) | |
[[0 0 1 0 0] | |
[0 1 0 1 0] | |
[1 0 0 0 1]])))) |
(ns cellular-animation.evolution) | |
(defn- extract-neighborhoods [state] | |
(partition 3 1 (repeat 0) (cons 0 state))) | |
(defn- evolve-once [rule state] | |
(mapv rule (extract-neighborhoods state))) | |
(defn- pad-with-zeroes [state] | |
(vec (cons 0 (conj state 0)))) | |
(defn- next-state [rule state] | |
(->> state | |
pad-with-zeroes | |
(evolve-once rule))) | |
(defn evolve [rule states] | |
(->> (last states) | |
(next-state rule) | |
(conj (mapv pad-with-zeroes states)) | |
vec)) |
With that working, I played on the REPL to create a subscriber that reacted to changes on the cellular automaton states making the view render.
This is the resulting code of the view:
(ns cellular-animation.views | |
(:require | |
[re-frame.core :as re-frame])) | |
(def ^:private cell-representations | |
{0 "\u00A0" 1 "*"}) | |
(defn- render-state [state-index cell-index cell-state] | |
(with-meta | |
[:span.cell (cell-representations cell-state)] | |
{:key (str "cell-state-" state-index "-cell-" cell-index)})) | |
(defn- render-states [state-index state] | |
(with-meta | |
[:div | |
(map-indexed (partial render-state state-index) state)] | |
{:key (str "state-" state-index)})) | |
(defn- automaton-component [] | |
(let [automaton-states (re-frame/subscribe [:automaton-states])] | |
(fn [] | |
[:div | |
{:on-click #(re-frame/dispatch [:evolution-started-or-stopped])} | |
(map-indexed render-states @automaton-states)]))) | |
(defn main-panel [] | |
[automaton-component]) |
(ns cellular-animation.subs | |
(:require-macros | |
[reagent.ratom :refer [reaction]]) | |
(:require | |
[re-frame.core :as re-frame])) | |
(re-frame/reg-sub | |
:automaton-states | |
(fn [db] | |
(:automaton-states db))) |
(ns cellular-animation.db | |
(:require | |
[cellular-animation.rules :as rules])) | |
(def default-db | |
{:automaton-states [[1]] | |
:rule rules/rule-90 | |
:evolving false}) |
The use of effects keeps re-frame handlers pure. They allow us to avoid making side effects. We just have to describe as data the computation that will be made instead of doing it. re-frame takes care of that part.
These are the handlers tests:
(ns cellular-animation.handlers-test | |
(:require | |
[cljs.test :refer-macros [deftest testing is]] | |
[cellular-animation.handlers :as handlers] | |
[cellular-animation.rules :as rules])) | |
(deftest evolve-handler-test | |
(testing "when the automaton is evolving it calls itself after changing its state" | |
(let [initial-states [[1]] | |
expected-states [[0 1 0] [1 0 1]] | |
db {:automaton-states initial-states :rule rules/rule-90 :evolving true}] | |
(is (= (handlers/evolve-handler {:db db} []) | |
{:db (merge db {:automaton-states expected-states}) | |
:dispatch-later [{:ms 100 :dispatch [:evolve]}]})))) | |
(testing "when the automaton is not evolving it does not change its state" | |
(let [db {:automaton-states :not-used-initial-states :rule :not-used-rule :evolving false}] | |
(is (= (handlers/evolve-handler {:db db} []) {:db db}))))) | |
(deftest start-stop-evolution-test | |
(testing "when the automaton is evolving it stops the evolution" | |
(let [db {:evolving true}] | |
(is (= (handlers/start-stop-evolution {:db db} []) | |
{:db (assoc db :evolving false) | |
:dispatch [:evolve]})))) | |
(testing "when the automaton is not evolving it starts the evolution" | |
(let [db {:evolving false}] | |
(is (= (handlers/start-stop-evolution {:db db} []) | |
{:db (assoc db :evolving true) | |
:dispatch [:evolve]}))))) |
(ns cellular-animation.handlers | |
(:require | |
[cellular-animation.evolution :as evolution])) | |
(defn evolve-handler [{:keys [db]} _] | |
(if (:evolving db) | |
{:db (update db :automaton-states | |
(partial evolution/evolve (:rule db))) | |
:dispatch-later [{:ms 100 :dispatch [:evolve]}]} | |
{:db db})) | |
(defn start-stop-evolution [{:keys [db]} _] | |
{:db (update db :evolving not) | |
:dispatch [:evolve]}) |
To see the whole picture this is the code that registers the handlers:
(ns cellular-animation.register-handlers | |
(:require | |
[re-frame.core :as re-frame] | |
[cellular-animation.db :as db] | |
[cellular-animation.handlers :as handlers])) | |
(re-frame/reg-event-db | |
:initialize-db | |
(fn [_ _] | |
db/default-db)) | |
(re-frame/reg-event-fx | |
:evolution-started-or-stopped handlers/start-stop-evolution) | |
(re-frame/reg-event-fx | |
:evolve handlers/evolve-handler) |
They make testing handlers logic very easy (since handlers keep being pure functions) and avoid having to use test doubles.
Finally, everything is put together in the core namespace:
(ns cellular-animation.core | |
(:require | |
[reagent.core :as reagent] | |
[re-frame.core :as re-frame] | |
[devtools.core :as devtools] | |
[cellular-animation.register-handlers] | |
[cellular-animation.subs] | |
[cellular-animation.views :as views] | |
[cellular-animation.config :as config])) | |
(defn dev-setup [] | |
(when config/debug? | |
(enable-console-print!) | |
(println "dev mode") | |
(devtools/install!))) | |
(defn mount-root [] | |
(reagent/render [views/main-panel] | |
(.getElementById js/document "app"))) | |
(defn ^:export init [] | |
(re-frame/dispatch-sync [:initialize-db]) | |
(dev-setup) | |
(mount-root)) |
You can find the code on this GitHub repository.
In this post I've tried to, somehow, describe the process I followed to write this code, but the previous gists just reflect the final version of the code. If you want to follow how the code evolved, have a look to all the commits on Github.
I really love using ClojureScript and re-frame with Figwheel.
Thursday, September 29, 2016
Interesting Talk: "The Clockwork Gardener: Growing an Elm App With Templates"
Wednesday, September 28, 2016
Kata: Variation on Lights Out, a bit of FRP using reagi library
reagi is an FRP library for Clojure and ClojureScript which is built on top of core.async. I discovered it while reading Leonardo Borges' wonderful Clojure Reactive Programming book.
I started from the code of the version using the component library I posted recently about and introduced reagi. Let's see the code.
Let's start with the lights-gateway name space:
(ns kata-lights-out.lights-gateway | |
(:require | |
[cljs-http.client :as http] | |
[com.stuartsierra.component :as component] | |
[reagi.core :as reagi] | |
[cljs.core.async :as async]) | |
(:require-macros | |
[cljs.core.async.macros :refer [go]])) | |
(defn- extract-lights [response] | |
(->> response | |
:body | |
(.parse js/JSON) | |
.-lights | |
js->clj)) | |
(defn- post [lights-stream uri params] | |
(go | |
(when-let [response (async/<! | |
(http/post uri | |
{:with-credentials? false | |
:form-params params}))] | |
(reagi/deliver lights-stream | |
(extract-lights response))))) | |
(defprotocol LightsGateway | |
(reset-lights! [this m n]) | |
(flip-light! [this pos])) | |
(defrecord ApiLightsGateway [config] | |
component/Lifecycle | |
(start [this] | |
(println ";; Starting ApiLightsGateway component") | |
(assoc this :lights-stream (reagi/events))) | |
(stop [this] | |
(println ";; Stopping ApiLightsGateway component") | |
(reagi/dispose (:lights-stream this)) | |
this) | |
LightsGateway | |
(reset-lights! [this m n] | |
(post (:lights-stream this) | |
(:reset-lights-url config) | |
{:m m :n n})) | |
(flip-light! [this [x y]] | |
(post (:lights-stream this) | |
(:flip-light-url config) | |
{:x x :y y}))) | |
(defn make-api-gateway [config] | |
(->ApiLightsGateway config)) |
I also use, reagi's deliver function to feed the lights-stream with the response that is taken from the channel that cljs-http returns when we make a post request to the server.
Next,the lights name space:
(ns kata-lights-out.lights | |
(:require | |
[reagent.core :as r] | |
[com.stuartsierra.component :as component] | |
[kata-lights-out.lights-gateway :as lights-gateway] | |
[reagi.core :as reagi])) | |
(def ^:private light-off 0) | |
(defn light-off? [light] | |
(= light light-off)) | |
(defn- listen-to-lights-updates! [{:keys [lights-stream lights]}] | |
(->> lights-stream | |
(reagi/map #(reset! lights %)))) | |
(defprotocol LightsOperations | |
(reset-lights! [this]) | |
(flip-light! [this pos])) | |
(defrecord Lights [lights-gateway m n] | |
component/Lifecycle | |
(start [this] | |
(println ";; Starting lights component") | |
(let [this (assoc this | |
:lights-stream (:lights-stream lights-gateway) | |
:lights (r/atom [])) | |
this (assoc this :lights-gateway lights-gateway)] | |
(listen-to-lights-updates! this) | |
(reset-lights! this) | |
this)) | |
(stop [this] | |
(println ";; Stopping lights component") | |
this) | |
LightsOperations | |
(reset-lights! [this] | |
(lights-gateway/reset-lights! (:lights-gateway this) m n)) | |
(flip-light! [this pos] | |
(lights-gateway/flip-light! (:lights-gateway this) pos))) | |
(defn all-lights-off? [lights] | |
(every? light-off? (flatten lights))) | |
(defn make-lights [m n] | |
(map->Lights {:m m :n n})) |
Now the lights-view name space:
(ns kata-lights-out.lights-view | |
(:require | |
[reagent.core :as r] | |
[kata-lights-out.lights :as lights] | |
[reagi.core :as reagi] | |
[com.stuartsierra.component :as component])) | |
(defn- all-lights-off-message-content [config lights] | |
(if (lights/all-lights-off? lights) | |
(:success-message config) | |
{:style {:display :none}})) | |
(defn- all-lights-off-message-component [config lights] | |
[:div#all-off-msg | |
(all-lights-off-message-content config lights)]) | |
(defn- render-light [{:keys [light-on light-off]} light] | |
(if (lights/light-off? light) | |
light-off | |
light-on)) | |
(defn- light-component [config clicked-light-positions i j light] | |
^{:key (+ i j)} | |
[:button | |
{:on-click #(reagi/deliver clicked-light-positions [i j])} | |
(render-light config light)]) | |
(defn- row-lights-component [config clicked-light-positions i row-lights] | |
^{:key i} | |
[:div (map-indexed (partial light-component config clicked-light-positions i) row-lights)]) | |
(defn- home-page [config clicked-light-positions lights-component] | |
(fn [] | |
(let [lights (:lights lights-component)] | |
[:div [:h2 (:title config)] | |
(map-indexed (partial row-lights-component config clicked-light-positions) @lights) | |
[all-lights-off-message-component config @lights]]))) | |
(defn- flip-light-when-clicked [lights-component clicked-light-positions] | |
(->> clicked-light-positions | |
(reagi/map #(lights/flip-light! lights-component %)))) | |
(defprotocol View | |
(mount [this])) | |
(defrecord LightsView [lights-component config] | |
component/Lifecycle | |
(start [this] | |
(println ";; Starting LightsOutView component") | |
(let [this (assoc this :clicked-light-positions (reagi/events))] | |
(mount this) | |
this)) | |
(stop [this] | |
(println ";; Stopping LightsOutView component") | |
(reagi/dispose (:clicked-light-positions this)) | |
this) | |
View | |
(mount [{:keys [clicked-light-positions]}] | |
(flip-light-when-clicked | |
lights-component clicked-light-positions) | |
(r/render | |
[home-page config clicked-light-positions lights-component] | |
(.getElementById js/document "app")))) | |
(defn make [config] | |
(map->LightsView {:config config})) |
Another change to notice is that we made the view a component (LightsView component) in order to properly create and dispose of the clicked-light-positions stream.
Finally, this is the core name space where all the components are composed together:
(ns kata-lights-out.core | |
(:require | |
[kata-lights-out.lights-view :as lights-view] | |
[com.stuartsierra.component :as component] | |
[kata-lights-out.lights :as lights] | |
[kata-lights-out.lights-gateway :as lights-gateway])) | |
(enable-console-print!) | |
(defn init! [m n] | |
(component/start | |
(component/system-map | |
:lights-gateway (lights-gateway/make-api-gateway | |
{:reset-lights-url "http://localhost:3000/reset-lights" | |
:flip-light-url "http://localhost:3000/flip-light"}) | |
:lights-component (component/using | |
(lights/make-lights m n) | |
[:lights-gateway]) | |
:lights-view (component/using | |
(lights-view/make | |
{:success-message "Lights out, Yay!" | |
:light-on "1" | |
:light-off "0" | |
:title "Kata Lights Out"}) | |
[:lights-component])))) | |
(init! 3 3) |
You can find my code in these two GitHub repositories: the server and the client (see the master branch).
You can check the changes I made to use reagi here (see the commits made from the commit d51d2d4 (using a stream to update the lights) on).
Ok, that's all, next I'll start posting a bit about re-frame.
Tuesday, September 20, 2016
Kata: Variation on Lights Out, introducing component library
First, we put the code in charge of talking to the back end and updating the lights atom in a separated component, ApiLightsGateway:
(ns kata-lights-out.lights-gateway | |
(:require | |
[cljs-http.client :as http] | |
[cljs.core.async :as async] | |
[com.stuartsierra.component :as component])) | |
(defn- extract-lights [response] | |
(->> response | |
:body | |
(.parse js/JSON) | |
.-lights | |
js->clj)) | |
(defn- post [lights-channel uri params] | |
(async/pipeline | |
1 | |
lights-channel | |
(map extract-lights) | |
(http/post uri {:with-credentials? false :form-params params}) | |
false)) | |
(defprotocol LightsGateway | |
(reset-lights! [this m n]) | |
(flip-light! [this pos])) | |
(defrecord ApiLightsGateway [config] | |
component/Lifecycle | |
(start [this] | |
(println ";; Starting ApiLightsGateway component") | |
this) | |
(stop [this] | |
(println ";; Stopping ApiLightsGateway component") | |
this) | |
LightsGateway | |
(reset-lights! [this m n] | |
(post (:lights-channel this) | |
(:reset-lights-url config) | |
{:m m :n n})) | |
(flip-light! [this [x y]] | |
(post (:lights-channel this) | |
(:flip-light-url config) | |
{:x x :y y}))) | |
(defn make-api-gateway [config] | |
(->ApiLightsGateway config)) |
(ns kata-lights-out.lights | |
(:require | |
[reagent.core :as r] | |
[cljs.core.async :as async] | |
[com.stuartsierra.component :as component] | |
[kata-lights-out.lights-gateway :as lights-gateway]) | |
(:require-macros | |
[cljs.core.async.macros :refer [go-loop]])) | |
(def ^:private light-off 0) | |
(defn light-off? [light] | |
(= light light-off)) | |
(defn- listen-to-lights-updates! [{:keys [lights-channel lights]}] | |
(go-loop [] | |
(when-let [new-lights (async/<! lights-channel)] | |
(reset! lights new-lights) | |
(recur)))) | |
(defprotocol LightsOperations | |
(reset-lights! [this m n]) | |
(flip-light! [this pos])) | |
(defrecord Lights [lights-gateway] | |
component/Lifecycle | |
(start [this] | |
(println ";; Starting lights component") | |
(let [this (assoc this | |
:lights-channel (async/chan) | |
:lights (r/atom [])) | |
lights-channel (:lights-channel this) | |
lights-gateway (assoc lights-gateway | |
:lights-channel lights-channel) | |
this (assoc this :lights-gateway lights-gateway)] | |
(listen-to-lights-updates! this) | |
this)) | |
(stop [this] | |
(println ";; Stopping lights component") | |
(async/close! (:lights-channel this)) | |
this) | |
LightsOperations | |
(reset-lights! [this m n] | |
(lights-gateway/reset-lights! (:lights-gateway this) m n)) | |
(flip-light! [this pos] | |
(lights-gateway/flip-light! (:lights-gateway this) pos))) | |
(defn all-lights-off? [lights] | |
(every? light-off? (flatten lights))) | |
(defn make-lights [] | |
(map->Lights {})) |
Next, we used the Lights component from the view code:
(ns kata-lights-out.lights-view | |
(:require | |
[reagent.core :as r] | |
[kata-lights-out.lights :as lights])) | |
(def ^:private light-on "1") | |
(def ^:private light-off "0") | |
(defn- all-lights-off-message-content [lights] | |
(if (lights/all-lights-off? lights) | |
"Lights out, Yay!" | |
{:style {:display :none}})) | |
(defn- all-lights-off-message-component [lights] | |
[:div#all-off-msg | |
(all-lights-off-message-content lights)]) | |
(defn- on-light-click [lights-component pos] | |
(lights/flip-light! lights-component pos)) | |
(defn- render-light [light] | |
(if (lights/light-off? light) | |
light-off | |
light-on)) | |
(defn- light-component [lights-component i j light] | |
^{:key (+ i j)} | |
[:button | |
{:on-click #(on-light-click lights-component [i j])} (render-light light)]) | |
(defn- row-lights-component [lights-component i row-lights] | |
^{:key i} | |
[:div (map-indexed (partial light-component lights-component i) row-lights)]) | |
(defn- home-page [lights-component] | |
(fn [] | |
[:div [:h2 "Kata Lights Out"] | |
(map-indexed (partial row-lights-component lights-component) @(:lights lights-component)) | |
[all-lights-off-message-component @(:lights lights-component)]])) | |
(defn mount [lights-component] | |
(r/render | |
[home-page lights-component] | |
(.getElementById js/document "app"))) |
(ns kata-lights-out.core | |
(:require | |
[kata-lights-out.lights-view :as lights-view] | |
[com.stuartsierra.component :as component] | |
[kata-lights-out.lights :as lights] | |
[kata-lights-out.lights-gateway :as lights-gateway])) | |
(enable-console-print!) | |
;; ------------------------- | |
;; Initialize app | |
(defrecord MainComponent [lights-component m n] | |
component/Lifecycle | |
(start [this] | |
(println ";; Starting main component") | |
(lights/reset-lights! lights-component m n) | |
(lights-view/mount lights-component) | |
this) | |
(stop [this] | |
(println ";; Stopping lights component") | |
this)) | |
(defn main-component [m n] | |
(map->MainComponent {:n n :m m})) | |
(defn init! [m n] | |
(component/start | |
(component/system-map | |
:lights-gateway (lights-gateway/make-api-gateway | |
{:reset-lights-url "http://localhost:3000/reset-lights" | |
:flip-light-url "http://localhost:3000/flip-light"}) | |
:lights-component (component/using | |
(lights/make-lights) | |
[:lights-gateway]) | |
:main (component/using | |
(main-component m n) | |
[:lights-component])))) | |
(init! 3 3) |
You can find the code we produced in these two GitHub repositories: the server and the client (see the componentization branch).
You can check the changes we made to componentize the code here (see the commits made on Aug 30, 2016).
As usual it was a great pleasure to do mob programming and learn with the members of Clojure Developers Barcelona.
Wednesday, September 14, 2016
Kata: Scrabble sets in Clojure
I used a mix of a bit of TDD, a lot of REPL-driven development (RDD) following this cycle:
- Write a failing test (using examples that a bit more complicated than the typical ones you use when doing only TDD).
- Explore and triangulate on the REPL until I made the test pass with some ugly but complete solution.
- Refactor the code to make it more readable.
These are the tests I wrote using Midje:
(ns scrabble-sets.core-test | |
(:require | |
[midje.sweet :refer :all] | |
[scrabble-sets.core :as scrabble])) | |
(facts | |
"about scrabble" | |
(fact | |
"it shows the tiles that are left in the bag | |
in descending order of the quantity of each tile left. | |
In cases where more than one letter has the same quantity remaining, | |
those letters appear in alphabetical order, with blank tiles at the end" | |
(fact | |
"when no tiles are in play" | |
(scrabble/tiles-left | |
"") => (str "12: E\n" | |
"9: A, I\n" | |
"8: O\n" | |
"6: N, R, T\n" | |
"4: D, L, S, U\n" | |
"3: G\n" | |
"2: B, C, F, H, M, P, V, W, Y, _\n" | |
"1: J, K, Q, X, Z"))) | |
(fact | |
"when some tiles are in play" | |
(scrabble/tiles-left | |
"PQAREIOURSTHGWIOAE_") => (str "10: E\n" | |
"7: A, I\n" | |
"6: N, O\n" | |
"5: T\n" | |
"4: D, L, R\n" | |
"3: S, U\n" | |
"2: B, C, F, G, M, V, Y\n" | |
"1: H, J, K, P, W, X, Z, _\n" | |
"0: Q") | |
(scrabble/tiles-left | |
"LQTOONOEFFJZT") => (str "11: E\n" | |
"9: A, I\n" | |
"6: R\n" | |
"5: N, O\n" | |
"4: D, S, T, U\n" | |
"3: G, L\n" | |
"2: B, C, H, M, P, V, W, Y, _\n" | |
"1: K, X\n" | |
"0: F, J, Q, Z")) | |
(fact | |
"when trying to put in play too many tiles of some kind" | |
(scrabble/tiles-left | |
"AXHDRUIOR_XHJZUQEE") | |
=> "Invalid input. More X's have been taken from the bag than possible.")) |
(ns scrabble-sets.core | |
(:require | |
[clojure.string :as string])) | |
(def ^:private tiles-in-bag | |
{"E" 12 "A" 9 "I" 9 "O" 8 "N" 6 "R" 6 "T" 6 | |
"L" 4 "S" 4 "U" 4 "D" 4 "G" 3 "_" 2 "B" 2 | |
"C" 2 "M" 2 "P" 2 "F" 2 "H" 2 "V" 2 "W" 2 | |
"Y" 2 "K" 1 "J" 1 "X" 1 "Q" 1 "Z" 1}) | |
(def ^:private group-by-frequency | |
(partial group-by second)) | |
(defn- format-tile [[freq tiles]] | |
(str freq ": " (string/join ", " (map str tiles)))) | |
(defn- format-tiles [sorted-tiles] | |
(->> sorted-tiles | |
(map format-tile) | |
(string/join "\n"))) | |
(defn- sort-by-frequency [tiles-in-bag] | |
(map (fn [[freq & [tiles]]] | |
[freq (sort (map first tiles))]) | |
(sort-by key > (group-by-frequency tiles-in-bag)))) | |
(defn- consume-tile [tiles-in-bag tile-in-play] | |
(update tiles-in-bag tile-in-play dec)) | |
(defn- consume [tiles-in-play tiles-in-bag] | |
(reduce consume-tile tiles-in-bag tiles-in-play)) | |
(defn- format-error-message [consumed-tiles] | |
(str "Invalid input. More " | |
(string/join ", " (map first consumed-tiles)) | |
"'s have been taken from the bag than possible.")) | |
(defn- display-distribution [tiles-in-bag] | |
(let [overconsumed-tiles (filter #(neg? (second %)) tiles-in-bag)] | |
(if (empty? overconsumed-tiles) | |
(format-tiles (sort-by-frequency tiles-in-bag)) | |
(format-error-message overconsumed-tiles)))) | |
(defn tiles-left [tiles-in-play] | |
(->> tiles-in-bag | |
(consume (map str tiles-in-play)) | |
display-distribution)) |
You can find all the code on GitHub.
Monday, September 12, 2016
Kata: Parrot Refactoring in Clojure
This is the initial code of the kata that have to be refactored:
(ns parrot-refactoring.core) | |
(def ^:private load-factor 9.0) | |
(def ^:private base-speed 12.0) | |
(defn- compute-base-speed-for-voltage [voltage] | |
(min 24.0 (* voltage base-speed))) | |
(defn speed [parrot] | |
(case (:type parrot) | |
:european-parrot | |
base-speed | |
:african-parrot | |
(max 0.0 (- base-speed (* load-factor (:num-coconuts parrot)))) | |
:norwegian-blue-parrot | |
(if (:nailed parrot) | |
0.0 | |
(compute-base-speed-for-voltage (:voltage parrot))) | |
(throw (Exception. "Should be unreachable!")))) |
- One using protocols:
(ns parrot-refactoring.core) | |
(def ^:private base-speed 12.0) | |
(def ^:private minimum-speed 0.0) | |
(defprotocol Parrot | |
(speed [this])) | |
(defrecord EuropeanParrot [] | |
Parrot | |
(speed [_] | |
base-speed)) | |
(defrecord AfricanParrot [num-coconuts] | |
Parrot | |
(speed [_] | |
(let [load-factor 9.0] | |
(max minimum-speed (- base-speed (* load-factor num-coconuts)))))) | |
(defrecord NorwegiaBlueParrot [voltage] | |
Parrot | |
(speed [_] | |
(let [maximum-speed 24.0] | |
(min maximum-speed (* voltage base-speed))))) | |
(defrecord NailedNorwegiaBlueParrot [] | |
Parrot | |
(speed [_] minimum-speed)) | |
(defn european [] | |
(->EuropeanParrot)) | |
(defn african [num-coconuts] | |
(->AfricanParrot num-coconuts)) | |
(defn norwegian-blue-parrot [voltage] | |
(->NorwegiaBlueParrot voltage)) | |
(defn nailed-norwegian-blue-parrot [] | |
(->NailedNorwegiaBlueParrot)) |
If you feel like following the refactoring process, check the baby steps in the commits. You can find the code in this GitHub repository.
- And another one using multimethods:
(ns parrot-refactoring.core) | |
(def ^:private base-speed 12.0) | |
(def ^:private minimum-speed 0.0) | |
(defmulti speed :type) | |
(defmethod speed :european-parrot [_] | |
base-speed) | |
(defmethod speed :african-parrot [{:keys [num-coconuts]}] | |
(let [load-factor 9.0] | |
(max minimum-speed (- base-speed (* load-factor num-coconuts))))) | |
(defmethod speed :norwegian-blue-parrot [{:keys [nailed voltage]}] | |
(let [maximum-speed 24.0] | |
(if nailed | |
minimum-speed | |
(min maximum-speed (* voltage base-speed))))) | |
(defmethod speed :default [_] | |
(throw (Exception. "Should be unreachable!"))) |
Again, check the commits of the refactoring baby steps here, if you feel like following the refactoring process. You can find the code of this second version in this GitHub repository. I'd like to thank Emily Bache for sharing this kata.
Saturday, September 10, 2016
Interesting Talk: "How the web is democratizing science (join in!)"
Thursday, September 8, 2016
Mob Kata: Cellular Automata in Clojure
We did mob programming and used a mix of TDD and RDD.
This is the code we wrote:
First, the tests using Midje for the rules 30 and 90:
(ns cellular-automata.rules-test | |
(:require | |
[midje.sweet :refer :all] | |
[cellular-automata.rules :as rules])) | |
(facts | |
"about rules" | |
(fact | |
;+-----------------------------------------------------------------+ | |
;| Neighborhood | 111 | 110 | 101 | 100 | 011 | 010 | 001 | 000 | | |
;+-----------------------------------------------------------------+ | |
;| New Center Cell | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 0 | | |
;+-----------------------------------------------------------------+ | |
"rule 90" | |
(rules/rule-90 [1 1 1]) => 0 | |
(rules/rule-90 [1 1 0]) => 1 | |
(rules/rule-90 [1 0 1]) => 0 | |
(rules/rule-90 [1 0 0]) => 1 | |
(rules/rule-90 [0 1 1]) => 1 | |
(rules/rule-90 [0 1 0]) => 0 | |
(rules/rule-90 [0 0 1]) => 1 | |
(rules/rule-90 [0 0 0]) => 0) | |
(fact | |
;+-----------------------------------------------------------------+ | |
;| Neighborhood | 111 | 110 | 101 | 100 | 011 | 010 | 001 | 000 | | |
;+-----------------------------------------------------------------+ | |
;| New Center Cell | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | | |
;+-----------------------------------------------------------------+ | |
"rule 30" | |
(rules/rule-30 [1 1 1]) => 0 | |
(rules/rule-30 [1 1 0]) => 0 | |
(rules/rule-30 [1 0 1]) => 0 | |
(rules/rule-30 [1 0 0]) => 1 | |
(rules/rule-30 [0 1 1]) => 1 | |
(rules/rule-30 [0 1 0]) => 1 | |
(rules/rule-30 [0 0 1]) => 1 | |
(rules/rule-30 [0 0 0]) => 0)) |
(ns cellular-automata.core-test | |
(:require | |
[midje.sweet :refer :all] | |
[cellular-automata.core :as automata] | |
[cellular-automata.rules :as rules])) | |
(facts | |
"an elementary cellular automaton" | |
(facts | |
"evolves from an initial state for some time steps | |
following a given rule" | |
(automata/evolve | |
rules/rule-90 | |
[1 1 0 1 0 1 0] | |
5) => [[1 1 0 1 0 1 0] | |
[1 1 0 0 0 0 1] | |
[1 1 1 0 0 1 0] | |
[1 0 1 1 1 0 1] | |
[0 0 1 0 1 0 0] | |
[0 1 0 0 0 1 0]]) | |
(facts | |
"can be rendered as text lines" | |
(automata/render | |
[[1 1 0 1 0 1 0] | |
[1 1 0 0 0 0 1] | |
[1 1 1 0 0 1 0]]) => "xx x x \nxx x\nxxx x ")) |
(ns cellular-automata.rules) | |
(def rule-90 | |
{[1 1 1] 0 | |
[1 1 0] 1 | |
[1 0 1] 0 | |
[1 0 0] 1 | |
[0 1 1] 1 | |
[0 1 0] 0 | |
[0 0 1] 1 | |
[0 0 0] 0}) | |
(def rule-30 | |
{[1 1 1] 0 | |
[1 1 0] 0 | |
[1 0 1] 0 | |
[1 0 0] 1 | |
[0 1 1] 1 | |
[0 1 0] 1 | |
[0 0 1] 1 | |
[0 0 0] 0}) |
(ns cellular-automata.core | |
(:require | |
[clojure.string :as string])) | |
(def ^:private representations | |
{0 " " | |
1 "x"}) | |
(defn- extract-neighborhoods [state] | |
(partition 3 1 (repeat 0) (cons 0 state))) | |
(defn- evolve-once [rule state] | |
(mapv rule (extract-neighborhoods state))) | |
(defn evolve [rule initial-state time-steps] | |
(->> initial-state | |
(iterate (partial evolve-once rule)) | |
(take (inc time-steps)))) | |
(defn- render-state [line] | |
(apply str (map representations line))) | |
(defn render [states] | |
(->> states | |
(map render-state) | |
(string/join "\n"))) |
(doseq | |
[line (render | |
(evolve | |
rules/rule-90 | |
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] | |
15))] | |
(print line)) | |
x | |
x x | |
x x | |
x x x x | |
x x | |
x x x x | |
x x x x | |
x x x x x x x x | |
x x | |
x x x x | |
x x x x | |
x x x x x x x x | |
x x x x | |
x x x x x x x x | |
x x x x x x x x | |
x x x x x x x x x x x x x x x x => nil |
As usual, it was a pleasure to meet and program with Clojure Developers Barcelona members.
Friday, September 2, 2016
Kata: Varint in Clojure using Midje and test.check
Since I was facilitating it, I couldn't code during the event.
Once I got home, I did it using Clojure.
These are the tests using Midje and Clojure test.check libraries:
(ns varint.core-test | |
(:require | |
[varint.core :refer :all] | |
[midje.sweet :refer :all] | |
[clojure.test.check.clojure-test :refer [defspec]] | |
[clojure.test.check.generators :as gen] | |
[clojure.test.check.properties :as prop])) | |
(facts | |
"about varint" | |
(facts | |
"encoding numbers under 128" | |
(encode 1) => "00000001" | |
(encode 8) => "00001000" | |
(encode 127) => "01111111") | |
(facts | |
"encoding numbers greater or equal than 128" | |
(encode 300) => "1010110000000010") | |
(facts | |
"decoding varints" | |
(decode "1010110000000010") => 300)) | |
(defspec coding-and-decoding | |
1000 | |
(prop/for-all [num (gen/large-integer* {:min 0})] | |
(= (-> num encode decode) num))) |
(ns varint.core) | |
(defn- pad-left [length element bin-num] | |
(concat (repeat (- length (count bin-num)) element) bin-num)) | |
(defn- add-most-significat-bits [bytes] | |
(flatten (concat (map #(cons "1" %) (butlast bytes)) | |
(cons "0" (last bytes))))) | |
(defn- partition-in-blocks-of [block-size coll] | |
(partition-all block-size block-size coll)) | |
(defn- bin-str->bytes [bin-str] | |
(->> bin-str | |
reverse | |
(partition-in-blocks-of 7) | |
(map reverse) | |
(map (partial pad-left 7 "0")))) | |
(defn- int->bin-str [num] | |
(Long/toBinaryString num)) | |
(def ^:private drop-most-significat-bits (partial map rest)) | |
(defn- varint->bytes [varint] | |
(->> varint | |
(partition-in-blocks-of 8) | |
drop-most-significat-bits)) | |
(defn- bytes->bin-str [bytes] | |
(-> bytes | |
reverse | |
flatten)) | |
(defn- int-pow [b exp] | |
(reduce * (repeat exp b))) | |
(defn- char->int [ch] | |
(Integer/parseInt (str ch))) | |
(def ^:private bin-str->bits (partial map char->int)) | |
(defn- bits->int [bits] | |
(->> bits | |
reverse | |
(map-indexed #(* %2 (int-pow 2 %1))) | |
(reduce +))) | |
(defn- bin-str->int [bin-str] | |
(-> bin-str | |
bin-str->bits | |
bits->int)) | |
(defn- bytes->varint [bytes] | |
(->> bytes | |
add-most-significat-bits | |
(apply str))) | |
(defn encode [num] | |
(-> num | |
int->bin-str | |
bin-str->bytes | |
bytes->varint)) | |
(defn decode [varint] | |
(-> varint | |
varint->bytes | |
bytes->bin-str | |
bin-str->int)) |
Basically, the cycle I followed was like this:
- Write a failing test (using more complicated examples than the typical ones you use when doing only TDD).
- Explore and triangulate on the REPL until I made the test pass with some ugly complete solution.
- Refactor the code to make it more readable.
This way I wrote both encode and decode functions.
Once I had then working with a few examples, I added property-based tests using Clojure test.check library (a QuickCheck like library for Clojure) to automatically explore more examples by checking that the property of "decoding and encoded number returns the number" held for 1000 randomly generated examples.
I think that these three techniques are complementary and its combination makes me more productive.
To document the development process I commit after each TDD green step and after each refactoring. I also committed the REPL history.
See all the commits here if you want to follow the process.
You can find all the code on GitHub.
Thursday, August 25, 2016
Books I read (January - August 2016)
- El Eternauta, Héctor Germán Oesterheld and Francisco Solano López
- Barcelona. Los vagabundos de la chatarra, Jorge Carrión and Sagar Fornies
- Rip Van Winkle, Washington Irving
- La guerra interminable (The Forever War), Joe Haldeman
- Maintanable JavaScript, Nicholas C. Zakas
- Ve y pon un centinela (Go Set a Watchman), Harper Lee
- El nombre del viento (The Name of the Wind), Patrick Rothfuss
- You Don't Know JS: Async & Performance, Kyle Simpson
- Sapiens: A Brief History of Humankind, Yuval Noah Harari
- The Principles of Object Oriented JavaScript, Nicholas C. Zakas
February
- The Leprechauns of Software Engineering, Laurent Bossavit
- The Wise Man's Fear, Patrick Rothfuss
- Fundamentals of Object-Oriented Design in UML, Meilir Page-Jones
- Old Man's War, John Scalzi
- Cevdet Bey e hijos (Cevdet Bey ve Oğulları), Orhan Pamuk
- Ringworld, Larry Niven
- Why Nations Fail: The Origins of Power, Prosperity, and Poverty, Daron Acemoglu and James A. Robinson
March
- The Grumpy Programmer's Guide To Building Testable PHP Applications, Chris Hartjes
- The Grapes of Wrath, John Steinbeck
- The Coding Dojo Handbook, Emily Bache
- Fahrenheit 451, Ray Bradbury
April
- Implementation Patterns, Kent Beck (3rd time)
- The Power of Habit: Why We Do What We Do in Life and Business, Charles Duhigg
- How to Win Friends and Influence People, Dale Carnegie
- Understanding the Four Rules of Simple Design, Corey Haines (2nd time)
- La llamada de Cthulhu (The Call of Cthulhu), El horror de Dunwich (The Dunwich Horror) and Las ratas en las paredes (The Rats in the Walls), H. P. Lovecraft
- The Tales of Beedle the Bard, J. K. Rowling
May
- The Slow Regard of Silent Things, Patrick Rothfuss
- Building Microservices, Designing Fine-Grained Systems, Sam Newman
- Man's Search for Meaning, Viktor Frankl
- Manifiesto del Partido Comunista (Das Kommunistiche Manifest), K. Marx and F. Engels
- Web Development with Clojure, Build Bulletproof Web Apps with Less Code, Dimitri Sotnikov
- Patterns of Enterprise Application Architecture, Martin Fowler
- The Checklist Manifesto: How to Get Things Right, Atul Gawande
- How to Fail at Almost Everything and Still Win Big: Kind of the Story of My Life, Scott Adams
June
- Pragmatic Thinking and Learning, Refactor Your Wetware, Andy Hunt
- Testable JavaScript, Mark Ethan Trostler
- The Secrets of Consulting: A Guide to Giving & Getting Advice Successfully, Gerald M. Weinberg
- La desfachatez intelectual. Escritores e intelectuales ante la política, Ignacio Sánchez-Cuenca
- REST in Practice, Jim Webber, Savas Parastatidis and Ian Robinson
- Mindset, Carol Dweck
July
- The Call of the Wild, Jack London
- Weinberg on Writing: The Fieldstone Method, Gerald M. Weinberg
- Dinner at the Homesick Restaurant, Anne Tyler
- How to Solve It: A New Aspect of Mathematical Method, G. Polya
- Object Oriented Software Engineering: A Use Case Driven Approach, Ivar Jacobson
- A Far Cry from Kensington, Muriel Spark
August
- The Difference Engine, William Gibson and Bruce Sterling
- Clojure Applied, From Practice to Practitioner, Ben Vandgrift and Alex Miller
- Vampir, Joann Sfar
- Los mares del Sur, Manuel Vázquez Montalbán
- El océano al final del camino (The Ocean at the End of the Lane), Neil Gaiman
- La Guerra Civil Española, Paul Preston and José Pablo García
- Wiser: Getting Beyond Groupthink to Make Groups Smarter, Cass R. Sunstein and Reid Hastie
MOOCs: Machine Learning on Coursera
I would like to thank Coursera and Andrew Ng for this great course!
Wednesday, August 24, 2016
Kata: Variation on Lights Out, flipping behavior on the backend
First, we did the kata in ClojureScript using Reagent and Figwheel (described in this previous post).
In the last two meetups (this one and this other one), we redid the kata.
This time instead of doing everything on the client, we used the Compojure library to develop a back end service that reset and flipped the lights, and a simple client that talked to it using cljs-http and core.async libraries.
As we've been doing lately, we did mob programming and REPL-driven development.
First, let's see the code we did for the server:
These are the tests:
(ns lights-out-server.handler-test | |
(:require [clojure.test :refer :all] | |
[ring.mock.request :as mock] | |
[lights-out-server.handler :refer :all])) | |
(deftest test-lights-out | |
(testing "resetting lights" | |
(let [response (app (mock/request :post "/reset-lights" {:m 3 :n 3}))] | |
(is (= (:status response) 200)) | |
(is (= (:body response) "{\"lights\":[[1,1,1],[1,1,1],[1,1,1]]}")))) | |
(testing "flipping lights" | |
(app (mock/request :post "/reset-lights" {:m 3 :n 3})) | |
(let [response (app (mock/request :post "/flip-light" {:x 0 :y 0}))] | |
(is (= (:status response) 200)) | |
(is (= (:body response) "{\"lights\":[[0,0,1],[0,1,1],[1,1,1]]}")))) | |
(testing "flipping lights twice" | |
(app (mock/request :post "/reset-lights" {:m 3 :n 3})) | |
(app (mock/request :post "/flip-light" {:x 1 :y 1})) | |
(let [response (app (mock/request :post "/flip-light" {:x 0 :y 0}))] | |
(is (= (:status response) 200)) | |
(is (= (:body response) "{\"lights\":[[0,1,1],[1,0,0],[1,0,1]]}"))))) |
(ns lights-out-server.handler | |
(:require | |
[compojure.core :refer :all] | |
[ring.middleware.cors :as cors-middleware] | |
[ring.middleware.json :as json-middleware] | |
[ring.middleware.defaults :refer [wrap-defaults api-defaults]] | |
[lights-out-server.lights :as lights])) | |
(defroutes app-routes | |
(POST "/reset-lights" [m n] | |
(let [m (Integer/parseInt m) | |
n (Integer/parseInt n)] | |
(lights/reset-lights! m n) | |
{:status 200 :body {:lights @lights/lights}})) | |
(POST "/flip-light" [x y] | |
(let [x (Integer/parseInt x) | |
y (Integer/parseInt y)] | |
(lights/flip-light! [x y]) | |
{:status 200 :body {:lights @lights/lights}}))) | |
(def app | |
(-> app-routes | |
(wrap-defaults api-defaults) | |
(json-middleware/wrap-json-response {:keywords? true :bigdecimals? true}) | |
(cors-middleware/wrap-cors | |
:access-control-allow-origin #"http://0.0.0.0:3449" | |
:access-control-allow-methods [:post]))) |
Finally, this is the code that flips and resets the lights (it's more or less the same code we wrote for the client-only version of the kata we did previously):
(ns lights-out-server.lights) | |
(def lights (atom nil)) | |
(def ^:private light-on 1) | |
(def ^:private light-off 0) | |
(defn- neighbors? [[i0 j0] [i j]] | |
(or (and (= j0 j) (= 1 (Math/abs (- i0 i)))) | |
(and (= i0 i) (= 1 (Math/abs (- j0 j)))))) | |
(defn- neighbors [m n pos] | |
(for [i (range m) | |
j (range n) | |
:when (neighbors? pos [i j])] | |
[i j])) | |
(defn light-off? [light] | |
(= light light-off)) | |
(defn- flip-light [light] | |
(if (light-off? light) | |
light-on | |
light-off)) | |
(defn- flip [lights pos] | |
(update-in lights pos flip-light)) | |
(defn- flip-neighbors [m n pos lights] | |
(->> pos | |
(neighbors m n) | |
(cons pos) | |
(reduce flip lights))) | |
(defn- all-lights-on [m n] | |
(vec (repeat m (vec (repeat n light-on))))) | |
(defn- num-rows [] | |
(count @lights)) | |
(defn- num-colums [] | |
(count (first @lights))) | |
(defn reset-lights! [m n] | |
(reset! lights (all-lights-on m n))) | |
(defn flip-light! [pos] | |
(swap! lights (partial flip-neighbors (num-rows) (num-colums) pos))) |
Now, let's see the code we wrote for the client:
First, the core namespace where everything is initialized (notice the channel creation on line 12):
(ns kata-lights-out.core | |
(:require | |
[kata-lights-out.lights-view :as light-view] | |
[cljs.core.async :as async])) | |
(enable-console-print!) | |
;; ------------------------- | |
;; Initialize app | |
(def m 4) | |
(def n 4) | |
(def lights-channel (async/chan)) | |
(defn init! [] | |
(light-view/mount lights-channel m n)) | |
(init!) |
(ns kata-lights-out.lights-view | |
(:require | |
[reagent.core :as r] | |
[kata-lights-out.lights :as l])) | |
(def ^:private light-on "1") | |
(def ^:private light-off "0") | |
(defn- all-lights-off-message-content [lights] | |
(if (l/all-lights-off? lights) | |
"Lights out, Yay!" | |
{:style {:display :none}})) | |
(defn- all-lights-off-message-component [lights] | |
[:div#all-off-msg (all-lights-off-message-content lights)]) | |
(defn- on-light-click [lights-channel pos] | |
(l/flip-light! lights-channel pos)) | |
(defn- render-light [light] | |
(if (l/light-off? light) | |
light-off | |
light-on)) | |
(defn- light-component [lights-channel i j light] | |
^{:key (+ i j)} | |
[:button | |
{:on-click #(on-light-click lights-channel [i j])} | |
(render-light light)]) | |
(defn- row-lights-component [lights-channel i row-lights] | |
^{:key i} | |
[:div (map-indexed | |
(partial light-component lights-channel i) | |
row-lights)]) | |
(defn- home-page [lights-channel lights] | |
(fn [] | |
[:div [:h2 "Kata Lights Out"] | |
(map-indexed | |
(partial row-lights-component lights-channel) | |
@lights) | |
[all-lights-off-message-component @lights]])) | |
(defn mount [lights-channel m n] | |
(l/listen-lights-updates! lights-channel) | |
(l/reset-lights! lights-channel m n) | |
(r/render | |
[home-page lights-channel l/lights] | |
(.getElementById js/document "app"))) |
And finally the lights namespace which is in charge of talking to the back end and updating the lights atom:
(ns kata-lights-out.lights | |
(:require | |
[reagent.core :as r] | |
[cljs-http.client :as http] | |
[cljs.core.async :as async]) | |
(:require-macros | |
[cljs.core.async.macros :refer [go go-loop]])) | |
(def ^:private lights (r/atom [])) | |
(def ^:private light-off 0) | |
(defn light-off? [light] | |
(= light light-off)) | |
(defn- extract-lights [response] | |
(->> response | |
:body | |
(.parse js/JSON) | |
.-lights | |
js->clj)) | |
(defn listen-to-lights-updates! [lights-channel] | |
(go-loop [] | |
(when-let [response (async/<! lights-channel)] | |
(reset! lights (extract-lights response)) | |
(recur)))) | |
(defn flip-light! [lights-channel [x y]] | |
(async/pipe | |
(http/post "http://localhost:3000/flip-light" | |
{:with-credentials? false | |
:form-params {:x x :y y}}) | |
lights-channel | |
false)) | |
(defn reset-lights! [lights-channel m n] | |
(async/pipe | |
(http/post "http://localhost:3000/reset-lights" | |
{:with-credentials? false | |
:form-params {:m m :n n}}) | |
lights-channel | |
false)) | |
(defn all-lights-off? [lights] | |
(every? light-off? (flatten lights))) |
You can find the code we produced in these two GitHub repositories: the server and the client (see the flip-lights-in-backend branch).
As usual it was a great pleasure to do mob programming and learn with the members of Clojure Developers Barcelona.
MOOCs: Networking for Web Developers on Udacity
After the course, I've started to read Ilya Grigorik's High Performance Browser Networking book bit by bit.
Thanks a million Udacity!
Monday, August 22, 2016
Interesting Talk: "Taming Asynchronous Workflows with Functional Reactive Programming"
Tuesday, August 16, 2016
Kata: Lights Out in ClojureScript using Reagent and Figwheel
We did mob programming and REPL-driven development.
Once at home I went on working on the kata.
This is the resulting code:
First, the core namespace where everything is initialized:
(ns kata-lights-out.core | |
(:require | |
[reagent.core :as r] | |
[kata-lights-out.lights :as l] | |
[kata-lights-out.lights-view :as light-view])) | |
(enable-console-print!) | |
;; ------------------------- | |
;; Initialize app | |
(def m 3) | |
(def n 3) | |
(def lights (r/atom [])) | |
(defn init! [] | |
(l/reset-lights! lights m n) | |
(light-view/mount lights)) | |
(init!) |
(ns kata-lights-out.lights-view | |
(:require | |
[reagent.core :as r] | |
[kata-lights-out.lights :as l])) | |
(def ^:private light-on "1") | |
(def ^:private light-off "0") | |
(defn- all-lights-off-message-content [lights] | |
(if (l/all-lights-off? lights) | |
"Lights out, Yay!" | |
{:style {:display :none}})) | |
(defn- all-lights-off-message-component [lights] | |
[:div#all-off-msg | |
(all-lights-off-message-content lights)]) | |
(defn- on-light-click [pos lights] | |
(swap! lights (partial l/flip-lights! lights pos))) | |
(defn- render-light [light] | |
(if (l/light-off? light) | |
light-off | |
light-on)) | |
(defn- light-component [lights i j light] | |
^{:key (+ i j)} | |
[:button | |
{:on-click #(on-light-click [i j] lights)} | |
(render-light light)]) | |
(defn- row-lights-component [lights i row-lights] | |
^{:key i} | |
[:div (map-indexed (partial light-component lights i) row-lights)]) | |
(defn- home-page [lights] | |
[:div [:h2 "Kata Lights Out"] | |
(map-indexed (partial row-lights-component lights) @lights) | |
[all-lights-off-message-component @lights]]) | |
(defn mount [lights] | |
(r/render | |
[home-page lights] | |
(.getElementById js/document "app"))) |
(ns kata-lights-out.lights) | |
(def ^:private light-on 1) | |
(def ^:private light-off 0) | |
(defn- neighbors? [[i0 j0] [i j]] | |
(or (and (= j0 j) (= 1 (Math/abs (- i0 i)))) | |
(and (= i0 i) (= 1 (Math/abs (- j0 j)))))) | |
(defn- neighbors [m n pos] | |
(for [i (range m) | |
j (range n) | |
:when (neighbors? pos [i j])] | |
[i j])) | |
(defn light-off? [light] | |
(= light light-off)) | |
(defn- flip-light [light] | |
(if (light-off? light) | |
light-on | |
light-off)) | |
(defn- flip [lights pos] | |
(update-in lights pos flip-light)) | |
(defn- num-rows [lights] | |
(count lights)) | |
(defn- num-colums [lights] | |
(count (first lights))) | |
(defn- flip-neighbors [pos lights] | |
(->> pos | |
(neighbors (num-rows lights) (num-colums lights)) | |
(cons pos) | |
(reduce flip lights))) | |
(defn- all-lights-on [m n] | |
(vec (repeat m (vec (repeat n light-on))))) | |
(defn reset-lights! [lights m n] | |
(reset! lights (all-lights-on m n))) | |
(defn flip-lights! [lights pos] | |
(swap! lights (partial flip-neighbors pos))) | |
(defn all-lights-off? [lights] | |
(every? zero? (flatten lights))) |
As usual it was a great pleasure to do mob programming with the members of Clojure Developers Barcelona.
Thanks!
Monday, August 15, 2016
Kata: Bank Account in Clojure using outside-in TDD with Component and Midje
I started the kata in a Barcelona Clojure Developers event.
The truth is that, since I was learning how to use the Component library, I didn't use TDD.
Instead I worked on the REPL to get everything in place and make it work.
Then I wrote the tests I would have liked to write if I had used outside-in TDD with Midje.
I find that, when I'm learning something new, it works better for me what Brian Marick describes in these tweets:
Now I'll show you the tests I actually wrote afterwards, in the order I would have written them doing outside-in TDD.(6) the immediately-following writing of tests to capture what I've learned counts as Schön-style reflection on just-prior practice.
— Brian Marick (@marick) July 26, 2016
This is the acceptance test I would have started with:
(ns bank-account.acceptance-test | |
(:require | |
[midje.sweet :refer :all] | |
[bank-account.test-helpers :refer [output-lines make-dates]] | |
[com.stuartsierra.component :as component] | |
[bank-account.factories :as factories] | |
[bank-account.account :as account])) | |
(unfinished date-fn) | |
(facts | |
"printing an account statement" | |
(let [config {:format {:date-format "dd/MM/yyyy" | |
:separator "||" | |
:num-decimals 2 | |
:header "date || credit || debit || balance"}} | |
dates (partial make-dates "dd/MM/yyyy") | |
account-system (assoc (factories/make-system config) | |
:transactions | |
(factories/in-memory-transactions #(date-fn))) | |
account (-> account-system component/start :account)] | |
(do | |
(account/deposit! account 1000) | |
(account/deposit! account 2000) | |
(account/withdraw! account 500) | |
(output-lines | |
account/print-statement account)) | |
=> ["date || credit || debit || balance" | |
"14/01/2012 || || 500.00 || 2500.00" | |
"13/01/2012 || 2000.00 || || 3000.00" | |
"10/01/2012 || 1000.00 || || 1000.00"] | |
(provided | |
(date-fn) =streams=> (dates ["10/01/2012" "13/01/2012" "14/01/2012"])))) |
(ns bank-account.account-test | |
(:require | |
[midje.sweet :refer :all] | |
[midje.open-protocols :refer [defrecord-openly]] | |
[com.stuartsierra.component :as component] | |
[bank-account.account :as account] | |
[bank-account.factories :as factories] | |
[bank-account.transactions-operations.transactions-operations :as transactions-operations] | |
[bank-account.statement-printing.statement-printer :as statement-printer])) | |
(unfinished register!) | |
(unfinished balanced-transactions) | |
(unfinished print-statement) | |
(defrecord-openly FakeTransactions [] | |
transactions-operations/TransactionsOperations | |
(register! [this amount]) | |
(balanced-transactions [this])) | |
(defrecord-openly FakePrinter [] | |
statement-printer/StatementPrinter | |
(print-statement [this balanced-transactions])) | |
(defn new-account [transactions printer] | |
(-> (factories/account) | |
(merge {:transactions transactions | |
:printer printer}) | |
component/start)) | |
(facts | |
"about account operations" | |
(fact | |
"it registers deposit transactions" | |
(let [fake-transactions (->FakeTransactions) | |
an-account (new-account fake-transactions :not-used)] | |
(account/deposit! an-account 50) => irrelevant | |
(provided | |
(register! fake-transactions 50) => irrelevant :times 1))) | |
(fact | |
"it registers withdrawals transactions" | |
(let [fake-transactions (->FakeTransactions) | |
an-account (new-account fake-transactions :not-used)] | |
(account/withdraw! an-account 100) => irrelevant | |
(provided | |
(register! fake-transactions -100) => irrelevant :times 1))) | |
(fact | |
"it prints the transactions in the statement" | |
(let [fake-transactions (->FakeTransactions) | |
fake-printer (->FakePrinter) | |
an-account (new-account fake-transactions fake-printer)] | |
(account/print-statement an-account) => irrelevant | |
(provided | |
(balanced-transactions | |
fake-transactions) => ...some-balanced-transactions... :times 1 | |
(print-statement | |
fake-printer | |
...some-balanced-transactions...) => irrelevant :times 1)))) |
(ns bank-account.in-memory-transactions-test | |
(:require | |
[midje.sweet :refer :all] | |
[midje.open-protocols :refer [defrecord-openly]] | |
[com.stuartsierra.component :as component] | |
[bank-account.factories :as factories] | |
[bank-account.transactions-operations.transactions-operations :as transactions] | |
[bank-account.test-helpers :refer [make-date]])) | |
(unfinished date-fn) | |
(defn new-in-memory-transactions [date-fn] | |
(component/start (factories/in-memory-transactions date-fn))) | |
(fact | |
"about transactions" | |
(facts | |
"in memory" | |
(fact | |
"returns balanced transactions lines for all registered transactions" | |
(let [date (partial make-date "dd/MM/yyyy") | |
first-transaction {:amount 1000 :date (date "10/02/2016")} | |
second-transaction {:amount 1500 :date (date "13/05/2016")} | |
third-transaction {:amount -500 :date (date "14/08/2016")} | |
in-memory-transactions (new-in-memory-transactions #(date-fn))] | |
(do | |
(transactions/register! in-memory-transactions (:amount first-transaction)) | |
(transactions/register! in-memory-transactions (:amount second-transaction)) | |
(transactions/register! in-memory-transactions (:amount third-transaction)) | |
(transactions/balanced-transactions in-memory-transactions)) | |
=> [(assoc first-transaction :balance 1000) | |
(assoc second-transaction :balance 2500) | |
(assoc third-transaction :balance 2000)] | |
(provided (date-fn) =streams=> [(:date first-transaction) | |
(:date second-transaction) | |
(:date third-transaction)]))))) |
(ns bank-account.console-statement-printer-test | |
(:require | |
[midje.sweet :refer :all] | |
[midje.open-protocols :refer [defrecord-openly]] | |
[com.stuartsierra.component :as component] | |
[bank-account.factories :as factories] | |
[bank-account.statement-formatting.statement-format :refer [StatementFormat]] | |
[bank-account.statement-printing.statement-printer :as printer] | |
[bank-account.test-helpers :refer [output-lines]])) | |
(unfinished format-statement-lines) | |
(unfinished header) | |
(defrecord-openly FakeFormat [] | |
StatementFormat | |
(header [this]) | |
(format-statement-lines [this statement-lines])) | |
(defn new-console-printer | |
([format print-fn] | |
(component/start (merge (factories/console-printer) | |
{:format format | |
:print-fn print-fn}))) | |
([format] | |
(new-console-printer format identity))) | |
(fact | |
"about printing statements" | |
(fact | |
"it asks the format for the header" | |
(let [fake-format (->FakeFormat) | |
a-printer (new-console-printer fake-format)] | |
(printer/print-statement | |
a-printer :not-used-in-this-test) => irrelevant | |
(provided | |
(header fake-format) => irrelevant :times 1))) | |
(fact | |
"it asks the format to format all balanced transactions" | |
(let [fake-format (->FakeFormat) | |
a-printer (new-console-printer fake-format)] | |
(printer/print-statement | |
a-printer | |
...some-balanced-transactions...) => irrelevant | |
(provided | |
(format-statement-lines | |
fake-format | |
...some-balanced-transactions...) | |
=> ...some-formatted-statement-lines... :times 1))) | |
(fact | |
"it prints the header and formatted lines" | |
(let [some-header "some-header" | |
some-formatted-statement-lines ["statement-line-1" "statement-line-2"] | |
expected-output-lines (cons some-header some-formatted-statement-lines) | |
fake-format (->FakeFormat) | |
a-printer (new-console-printer fake-format println)] | |
(output-lines | |
printer/print-statement | |
a-printer | |
...some-balanced-transactions...) => expected-output-lines | |
(provided | |
(header fake-format) => some-header | |
(format-statement-lines | |
fake-format | |
...some-balanced-transactions...) => some-formatted-statement-lines)))) |
(ns bank-account.nice-reverse-statement-format-test | |
(:require | |
[midje.sweet :refer :all] | |
[midje.open-protocols :refer [defrecord-openly]] | |
[com.stuartsierra.component :as component] | |
[bank-account.factories :as factories] | |
[bank-account.statement-formatting.statement-format :as statement-format] | |
[bank-account.test-helpers :refer [make-date]])) | |
(defn- new-nice-reverse-format [config] | |
(component/start (factories/nice-reverse-statement-format config))) | |
(fact | |
"about formatting statements" | |
(facts | |
"using NiceReverseStatementFormat" | |
(let [config {:date-format "dd/MM/yyyy" | |
:separator "||" | |
:num-decimals 2 | |
:header "date || credit || debit || balance"} | |
date (partial make-date (:date-format config)) | |
nice-reverse-format (new-nice-reverse-format config)] | |
(fact | |
"it returns the configured header" | |
(statement-format/header nice-reverse-format) => (:header config)) | |
(fact | |
"it formats the statement lines" | |
(let [balanced-transactions [{:balance 1000 :amount 1000 :date (date "10/02/2016")} | |
{:balance 2500 :amount 1500 :date (date "13/05/2016")} | |
{:balance 2000 :amount -500 :date (date "14/08/2016")}]] | |
(statement-format/format-statement-lines | |
nice-reverse-format | |
balanced-transactions) | |
=> ["14/08/2016 || || 500.00 || 2000.00" | |
"13/05/2016 || 1500.00 || || 2500.00" | |
"10/02/2016 || 1000.00 || || 1000.00"]))))) |
Doing this kata I learned and practiced how to use Component.
I also learned how to use Midje's defrecord-openly and provided macros to mock protocols which helped me correct something I did wrong in another kata.