118 avsnitt • Längd: 25 min • Månadsvis
Each week, we discuss a software design problem and how we might solve it using functional principles and the Clojure programming language.
The podcast Functional Design in Clojure is created by Christoph Neumann and Nate Jones. The podcast and the artwork on this page are embedded on this page using the public podcast feed (RSS).
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "parts of a pure data model". We look at pure data models we've created and see what they have in common.
And when there's a pure data model, a pure data model is something that is both wide enough to handle an actual use case and be useful, but it's shallow enough that you can understand it and trust the function calls that it has. All of the operations on that data are in the same namespace, so it's easier to understand.
I know they're predicates because they end in a question mark.
All of the necessary changes and views are encapsulated in a namespace. That means the rest of your application can rely on its higher-level operations when working with the data model. These are a higher-level vocabulary for your application, instead of just Clojure core's vocabulary.
Everything that can be done is all co-located in a namespace.
Am I multiplying by 0.10 or 0.15? Or am I calculating a tip? One of those statements has more information.
A pure data model lets you, as a programmer, think at a higher level in the rest of your application. When you think at a higher level that's trusted, it's a lower cognitive load. You can come back to the code later, read a function, and know what it means in the context of your application.
In every pure data model, you have to know what the data looks like.
Don't underestimate the value of being able to find places where a predicate is used. It tells you what the code cares about this situation. When you have to nuance the situation, you can look at the call sites and take them all into account.
Once you've made the HTTP call, all the information about the request, the response, the body, and all that is pure data. You can do a pure transform from the domain of raw, external HTTP information into the internal domain of the pure data model.
But because it's a pure function, it's a lot easier to test. All things are easier to test when they're pure. I/O is a very, very thin layer—both on the way in and the way out.
Instead of mixing I/O and logic, do as much I/O as you can, at once, to get a big bag of pure information to work with. And then on the way out, do a pure transform to generate everything you need for the I/O, like the full requests.
You can have a big bag of extra context that's there for you as the programmer—even though the program doesn't need it.
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "pure data models". We find a clear and pure heart in our application, unclouded by side effects.
It's functional programming, so we're talking about pure data models! That is our core, core, core business logic.
A pure data model is pure data and its pure functions. No side effects!
We already have a whole set of Clojure core functions to operate on data, so why would we have functions that are associated with just this pure data? Because you want to name the operations, the predicates, and all the other things to do with this data, so that you, as a human, understand.
Those functions are high-level vocabulary that you can use to think about your core model. They are business-level functions. They are super-important, serious functions.
We don't like side effects, so we define an immutable data structure and functions that operate on that data. They cannot update that data. They can't change things in place. They always have to return a new version of it.
At a basic level, you have functions that take the data. They give you a new data tree or find something and return it.
We like having the app.model
namespace. You can just go into the app/model
folder and see all of the core models for the whole application. Any part of the application can have access to the model.
The functions are the interface. All you can do is call functions with pure data and get pure data back. You can't mess anything up except your own copy.
It's just a big pool of files that are each a cohesive data model. They're a resource to the whole application, so anything that needs to work with that data model will require it and have all the functions to work with it.
With pure models, there's no surprise!
In OO, the larger these object trees get, the more risk there is. Any new piece of code, in the entire codebase, has access to the giant tree of objects and can mess it up for everything else.
Pure models lower your cognitive load. The lower the load is, the more your brain can focus on the actual problem.
You can read the code and take it at face value because the function is 100% deterministic given its inputs. If it's a pure function, you don't have to wonder what else is happening.
The model directory is an inventory of the most important things in the entire application. Here are all the things that matter. As much code as possible should be in pure models.
Look at the unit tests for each pure model to understand how the application reasons and represents things. It's the very essence of the application.
A lot of times in functional communities, we say "keep I/O at the edges." Imagine one of these components is like a bowl. At the first edge, there's I/O. In the middle is the pure model goodness. On the other side is I/O again.
None of the I/O is hidden. That's the best part. Because I/O isn't hidden behind a function, it's easier to understand. Cognitive load is lower. You can read the code and understand it when you get back into it and you're fixing a bug.
The shallower your I/O call stacks are, the easier they are to understand.
Where there are side effects, you want very, very shallow call stacks, and where there are no side effects, and you can unit test very thoroughly, you don't have to worry about the call stack as much.
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "frontend matters". We turn our attention to the frontend, and our eyes burn from the complexity.
You don't usually go into a code base just to browse it, or just to have fun. You go there with a purpose. You need to work. You need to get something done fast.
We like rich frontends. We're able to do a lot more interactivity. There's less interruption when the page has to load. There are a lot of advantages to SPAs.
With a SPA, it's really, really fast to switch between everything. It feels almost instantaneous because there is almost nothing to load each time.
The counterpart is that a SPA is more sophisticated, so it ends up being more complicated. It's almost like a process that's running continuously. There's more code that's present in a SPA than any individual page load.
From the browsers point of view, the "main" is the markup, and you have to tell it to run some code.
It's just one blob of code to the browser. You can't look at that code because it's transpiled, minified JavaScript.
I do think it's interesting that we've gotten several minutes into this episode, and we're still talking about how things get made into the final sausage. It's reflective of how much effort it takes to set up the JavaScript ecosystem.
We make a "main.cljs" file, and that is the top of the application. It's a signpost. "Hey! Hey! Look here first!"
The tab's not going to go away, so all we need to do is start up all the event listeners because JavaScript is a very event-driven language.
I want "main" to be a table of contents of everything that matters in the app: the views, the routes, the URLs, browser hooks, web sockets, etc.
The worst kind of "main" is no "main" at all. There are frameworks where you make a whole bunch of separate files for each of your routes.
I love how many times we said the word "react" in this episode. It's all very event driven. That's just the model of the whole browser. It's the water that you swim in, so you must swim the right way in order for the application to succeed.
User Interaction → Event → Callback → Reactive Model → Re-Render
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "the main function". We look for a suitable place to dig into the code and find an entry point.
Be friendly to people that come into your code base.
I didn't trust myself as much as I should.
"You just start at the top and write from the top to the bottom." "That's how I code everything. It's just one really large file."
So all parts of your code are reachable from "main"—or should be.
A great "main" is where you can see all the major parts of the application and how they fit together with each other.
A terrible "main" is a system that doesn't have any "main" at all! It has a thousand different entry points that are all over the place.
A great "main" is very compact. You can scan it. It's a very high level recipe of what's going on.
Component has a system map. You can just look at the data structure and see all of the different components—the major players.
The alternative is components that declare dependencies on each other. It's a kind of nightmare. Everything running independently, calling or referencing each other. What's using what? What's calling what? How does information flow through?
When dependency information gets spread all over the place, you have to go to all the different places to even understand what you need. Having it all in one place is essential for understanding.
It really helps when you can see the interdependency between things really easily. Each component should only get what it actually needs. It shouldn't just get the whole map of every dependency.
Common kinds of components: shared resources, integrations in, integrations out, pure models, state holders, and amalgamations. It all comes together in the amalgamations.
Pure models are the core of the application. They are higher level than just data manipulation.
All of the actual nuts and bolts is in the pure models, and that makes the components relatively light.
The goal is to make the pure model as fat as possible without introducing any non-determinism, AKA side effects.
Amalgamations: it's where the "in", the "out", and the pure model all come together—where the real work gets done.
The amalgamation components end up being at the middle of the application. They're a kind of orchestrator.
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "what's old is new again". We find ourselves staring at code for the first time—even though we wrote some of it!
It's always fun to start something new. You don't have to worry about all those other things from the past!
I was a little nervous about that other guy who made all the code: me fourteen months ago! I wasn't sure how good of a teammate he was going to be.
The word "legacy" is like a big bad word, but I feel like it should be a badge of honor! It's software that is running right now and paying your salary! You should have a reverence for code that exists and is running.
At some point in time, you or a new team member, is diving into a code base with fresh eyes. It brings up so many important issues that we face as developers.
We spend so much time reading code and forming mental models about what is going on.
A fundamental challenge in software development is understanding, comprehending and reasoning about the code base.
Comprehending and reasoning about the code is one of the primary drivers behind the "why" of a lot of the so-called "best practices" of the industry. Why do you write tests? Why do you write documentation? Why do you try to have a good design in your application?
There's this constant learning that we have to do, and so try to make that easier.
He moved on to better projects in the sky. We've lost him to a better project in the Cloud. He moved to a better project upstate!
It's easy to say: "We have great documentation! Our code is super readable! Decoupled? Absolutely! Our pure data models? Totally comprehensible!" It's easy to say that, but you really find out if those things are true when somebody new joins the team or when you have to revisit the code after a long time.
Always trying to teach someone else about your code. There's always some future person.
What can we do now as we're setting up the situation for new people in the future?
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "highlight highlights". We highlight the highlights of the Sportify! series.
"Let's put it all together into one context to rule them all and in the darkness bind them!" "But we're trying to spread the light of sports highlights across the Internet!"
There's nothing like actually seeing real data come from real APIs. No amount of talking to your boss or talking to the intern or reading documentation can replace what you get from touching the real-world situation.
Clojure helps you figure out how to bring the pieces together because you can just run the pieces in an ad-hoc way. You can just work on each of the parts without having to unify them into some kind of global proof system that's being foisted on you by static analysis.
It's like whiplash-driven development: you're moving so fast, you have to take a break just to take a breath!
The bottom-up way of constructing in Clojure has two properties: you're grounded in the real world, and you're just making what you need as you go along. It's very efficient.
Some of that code is going to find its way into your final working solution.
You're always making progress. You're always grounded in reality. You're just building what you need as you go along. It's not wasteful. It's very iterative. Very lean. Always forward motion.
If your system exploration is in Clojure, you can cross information streams a lot easier than if you're using separate tools. In Clojure, it's all data. You can just hand data back and forth.
You're not only discovering the properties of each information silo you're working with, but you're discovering the properties of how that data might fit and merge together. It's a grounded, incremental process for each of the parts, but also as the parts come together.
Sometimes you don't know what the final solution is going to be even though you have all the necessary parts. It's greater than the sum of its parts.
It isn't until you start running things over and over more frequently that you begin to discover the smaller percentage reliability issues.
The way you increase reliability is by minimizing things with side effects and maximizing things that are pure.
You're learning, at every step, what point needs more reliability.
The more pure data you have, the more visibility you have. The more pure functions you have, the more testability you have. So, reliability and pureness are definitely related.
It is amazing how much opportunity there is to move things into pure functions. The actual fetching or querying of the thing ends up constituting a pretty small part of your application. Working with the data tends to dominate.
The think-do-assimilate pattern allows you to maximize the testable surface in your application by factoring all the I/O out.
You start minimizing the I/O parts because your domain has emerged.
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "pure data, pure simplicity". We loop back to our new approach and find more, and less, than we expected!
Let's get some Hammock Time. We're big fans of Hammock Time!
We make a function for each of those: think, do, assimilate. A function to figure out the next thing, a function to do it, and a function to integrate the results back into the context. ("determine-operation", "execute-operation", and "update-context".)
It's not a single point of failure! It's a single point of context.
Where you have a binding in a let
block, you have a key in a context map. There's a symmetry there.
You can make the operation map as big, fat, and thick as you want, so "execute-operation" has 100% of everything it needs for that operation.
The "determine-operation" function can decide anything because it has the full context—the full world at its disposal!
Clojure has structural sharing, so all the information is cheap. We can keep a bunch of references to it in memory. We're not going to run out of memory if we keep information about every step.
The "update-context" is a reducer, so we can make a series of fake results in our test and run through different scenarios using "determine-operation" and "update-context". We're able to test all of our logic in our test cases because we can just pass in different types of data.
Your tests are grounded in reality. They're grounded in what has happened.
We've aggressively minimized the side effects down to the tiniest thing possible!
Data is inert. Mocks are not. Mocks are behavior.
You can just literally copy from the exception and put it in your test. There's no need transform it. It is already useful.
It's very testable. It's very inspectable. It's very repeatable. It creates a really simple overall loop.
You want those I/O implementations so small and dumb that the only way to mess them up is if you're calling the wrong function or you're passing the wrong args. Once it works, it will always work, and you no longer have to test it.
We need to build into context every little bit of information we need to know to make a decision.
Context takes anything that is implicit and makes it 100% explicit, because you can't get data across the boundaries without putting it in the context. You have no option but to put everything in the context, so you know everything that's going on.
We're in this machine, and there's no exit. We're on the freeway, and there's no off-ramp. We're in the infinite loop! How do we know we're done?
How do we know we're done?
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "trying again". We throw our code in a loop, and it throws us for a loop.
recur
from catch
?recur
?It's a lot like having a project on a workbench. You have all of the tools and all the information laid out before you on that workbench. Nothing is tucked in a drawer or inside a cabinet.
That's a very important lesson for any developer: you can always stop—at least after it's working.
Nothing in the world is solved except by adding another level of abstraction.
I was not expecting that level of mutation! I was expecting a Kafka log written in stone!
The positive is it has everything. The negative is it has everything.
We would like more loop-native code inside of our cloud-native application.
Are you suggesting that just because we can, it doesn't mean we should? We're programmers! If the language lets us do it, it must be a good idea!
One of the reasons why I like Clojure is because it specifically tells me that I can't do some things that are bad to do.
All of the context is in one map. It has everything in it. One map to rule them all!
Might this be the fabled "single application state"?
We have the thinking function, the doing function, and the assimilate function.
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "gathering debugging context". Our downloads fail at random, but our dead program won't give us any answers.
We just need to fill out our support ticket and say, "Hey! Fix your service!" It couldn't possibly be our code!
So there is an error happening, but what happened just before that error?
It is dead. There's no way to ask it any questions. It will not give us any answers.
The only way to know what the program was doing, is to know what the program was doing. If you're trying to figure out what the program was doing by reverse engineeringing it, you're going to get it wrong.
I love hiding side effects with macros! That's one of my favorite things to do in Clojure! It makes me feel like I'm using Scala again!
We don't want the I/O function to do any thinking of any kind. It's a grunt. We fully specify the bits it needs to know. It's 100% a boring outcome of what we passed into it.
Those I/O functions end up being ruthlessly simple. They're often just one line!
We remove the thinking, so we remove the information. It's not because we don't like pure functions. We put them in a place where we can have all the information in one place.
We're getting to the point where our let
block is getting really long really—maybe too long. We're really letting ourself go!
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "separating data from I/O". We need to test our logic, but the I/O is getting in the way.
We're using Clojure. Everything should be perfect, right?!
I love Hammock Time for figuring out hard problems, but in this case, I think we have a simple problem of testing.
You got to have the right amount of celebration after all those "line crossings" and "goal scorings" and stuff.
We're doing a relatively simple process: we're downloading things and compiling them together into a file. But, it's amazing just how much logic is all throughout this process.
As soon as you make a process, there's always going to be people who want to do it differently!
If experience is any indicator, you always need more information.
One of the reasons why you test is, when you make this kind of logic change, you want to make sure that everything continues to function.
You need to write tests so that when you make future changes, your old self is there sitting right next to you making sure that the old use cases are all covered, so that you only have to think about the new use cases.
With REPLing, you're figuring it out. With tests, you're locking it down and making sure that you have coverage in different situations.
Our biggest obstacle here is that logic and I/O are mixed up together.
Wait! Wait! We want to test our code. We don't want to spend our life writing code. Did you write the mock correctly? How do you write a test for the mock?
I think we need to completely pivot our approach here.
The problem is that we have I/O, logic, I/O, logic, I/O, logic. We have those two things right next to each other. What we should do instead is completely invert our thinking.
Let's gather information and then we can do pure logic on that data. Separate those two things.
We're going to extract from those POJOs. [Groan] I've got to use these terms every now and again or else I'm going to forget them all.
So we do an I/O call, collect information, and create our own internal representation. We just need a few bits of it, so we create a working representation of that.
It's our representation. It's our program's way of looking at the world. Craft the different scenarios in data that represent all the real life situations we found.
One of the problems of using built-ins is: what parts matter?
We're accreting working information into a larger and larger context.
You're setting the table with all the pieces that are defined in your working world and then creating unit tests in terms of those.
The world was like, "Hold my beer!"
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "testing around I/O". We start testing our code only to discover we need the whole world running first!
The tracer bullet misfires every now and again.
Now you're going from a tracer bullet to a silver bullet—apparently trying to solve all the problems at once!
The REPL lets you figure out the basics of the process and your own way of thinking about it and modeling it, and the tests let you start handling more and more cases.
Exploration early, testing later.
Are you just supposed to log everything all the time? Always run your code with a profiler attached?
If you look between each I/O step, there is pure connective tissue that holds those things together. We remove the logic and leave just the I/O by itself.
With pure functions, we don't have to worry about provisioning the AWS cluster for the tests to run!
It's really tempting to use the external data as your working data.
What is the data that this application reasons on?
By creating an extractor function, you pull all of the parts that matter into a single place. It returns a map for that entity that you can reason on and schema check.
We've distilled out the sea of information into a drinkable cupful. We've gone from the mountain spring to bottled water.
I guess you could always take all the raw data and shove them off in an Elasticsearch instance for massive debugging later—in some super-sophisticated implementation.
Not how do we accomplish it, but how do we test it?
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "handling endless errors". We discover when giving up is the way to get ahead.
We don't need a time zone offset because we know it's in UTC!
In the spirit of building up the language to meet our domain, we can write a pure function!
We want a deterministic way to go from this kind of common information into all the other bits of derived information.
I was really hoping that we would finally be done with the errors and we could just get a highlight clip, but yet again, the world has conspired against us to make our life difficult as a programmer!
Hold on! Hold on! The first thing we should do is run our process again because maybe the error will just go away!
The problem does not go away in this instance. The problem just goes back to being hidden!
The frustrating thing about programming is that code will do exactly what you told it to do.
Clojure is already positive about nothing, that's what we call nil
, so why not be positive about bad stuff too?
Now I'm personifying the function as myself!
It's better to reference information by its main identifier as opposed to some derived identifier.
The context is all over the place: some context in memory, some context in the file system, and some intermediate context in the imperative function.
That's the last problem we encounter, right? You never know what the world's going to throw at you!
Why don't we just run it again? Let's just run it again! Maybe it'll be ready now... Maybe now... Maybe it will be ready now...
And then we hit another error which is: the MAM rejects us for making too many requests!
How long do you wait? Should you back off? That adds a lot of complexity to the code at that level.
Because adding idempotency increases the complexity of that part of the solution, we only want to add it where it's necessary.
The error condition is happening right now, so let's write the code to fix it right now.
That's the way automation is at some point in time: a human needs to do it.
You can find yourself in a situation where you're trying to do too many automatic things. Is it really worth doing? You cannot solve every possible permutation once you're interacting with the real world.
It's okay to put up the guardrails, and if I'm outside them, robot me throws up my hands and says, "Human please!"
But what happens when you run it again and the clip is ready? Now your retry logic doesn't get tested.
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "building up reliability". We push our software to reach out to the real world and the real world pushes back.
This is very incremental. We are rapidly accreting functionality. We're bringing it together with a very interactive REPL-driven way of getting things done.
There's no reason why we would ever need to modify this code ever again because everything will go smoothly! I'm sure nobody will ever change their mind on functionality and nobody will ever make a mistake in any of the other systems either!
You always have to deal with the uncertainties through time, not just the uncertainties of requirements.
One of the great things about this interactive REPL-driven way is that we are exploring the real world. We're not exploring somebody's documentation or somebody's article or somebody's representation of the real world. We're actually interacting with real systems, and we're looking at real data, and we're figuring out the real situation.
We're not trying to make the ideal version for production. We are trying to get a fully automated solution end to end to understand all the specific situations we have to handle.
That S3 function is built on a tower of abstractions. Some of those abstractions involve the network, and other ones involve other companies. There's a variety of reasons why those might fail—whether for geopolitical or network-based reasons.
The human retry loop is a completely valid solution at this point in time. It's actually a valid solution for a lot of things.
Every time a human has to retry (and the human being is us) we learn every time. We're learning how the systems fail, and that's just as important as the happy path.
Over time, we're going to accrete more and more reliability in the system by handling more and more things.
Deleting the temporary files is a return to known state. It's a pretty harsh return to known state, but it is a return to known state. The initial state of having nothing is a sound state to return to.
I/O is the greatest source of failures when you're automating processes.
Nobody asked your program for permission to turn off the power.
The boss man says, "Make some more! Make some more!"
Let's reduce the recovery time as opposed to trying to avoid the need to recover.
We can convince ourselves that work is needed because we have evidence of failure over time. We're growing functionality on demand as needed. It's very lean.
We're building the right software just in time. Not only are you iterating quickly, so it's not a long time between each change in each rerun, you're also building the right thing every time. It is in the realm of the world, not in the realm of what you think the world is going to do. The actual world is there.
The situation we're in is the real world. It's not something that could happen or maybe happen or "what if" happened.
I/O is the source of pain in our lives, but it's the source of actually making useful software, so it's worth it.
Even if you're in a programming language like Haskell that tries to do proof systems around your logic for handling I/O, it still can't save you from the fact that I/O is going to blow up on you! I/O failures happen.
What happens if you need to retry the retry and then retry that retry? There's only so far you can go with this imperative assembling of the application.
We're letting the reality of our situation dictate where we apply our effort.
We still have learning to do.
Well, that was exceptionally fun!
We're still having fun even though we're encountering errors. Both of those things can happen at the same time!
It's just delightful to see progress. You're always feeling progress! That's a big goal: feel the wind at your back!
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "building up a solution". We grow beyond our REPL-driven pieces toward an end-to-end solution.
The learning is complete. Now it's time to get to the programming!
We didn't sign up to be a robot. We signed up to be a programmer.
We want fighting teams. We're not going to have very many highlights if it's the Badgers versus the Doves.
A tracer bullet is a minimalist solution where we try to get something working end to end.
It's interesting that you would say "imperative decomposition", because in this case, we have the parts, so we are doing an imperative composition. We're putting them together.
You get your learning, and you get a little bit of code out of it at the same time. Sure, that code may not be what you want to use in production, but you certainly have more actual code to work with than you did if you just opened up your database explorer and ran SQL statements.
Just because it's a silver bullet doesn't mean all human intervention is no longer needed. A silver bullet has to be fired by someone!
We're growing a function a piece at a time.
So by naming that and giving it a function name, it makes it more readable. It helps document the information.
It's like a mini language here in the let
. ... It makes this process—that you're now documenting in code—a little more readable.
You can launch this whole thing using a comment block!
You're exploring your way toward a solution. Even though it's very imperative in this case, you're just continuing to explore your way towards the solution as you build it up.
You're on your second rewrite of the code. You're not writing this code for the very first time. You're writing it for the second time, which means you're going to be picking variable names and function names better than the first time because you understand the concepts. You're not learning the concepts and writing the code at same time.
Exploration was important, but this second step (of making the code again) is valuable, because you're learning what level of abstraction you want.
With command line clients and browsing tools, you still learn a lot about the domain, but you don't learn a lot about how you want to represent it.
It's a coding-first approach, but it's also a coding-light approach. Clojure doesn't make you model the universe in a proof system that you have to try to get right and revise and revise.
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "exploration cessation". We realize we're done exploring when all of the pieces fall into place.
That's how I approach problems: you just keep going. Keep moving.
A fiddle is like a random access REPL.
A fiddle is just a file. We can put all the different bits of information—including data—in that one file. There's only one place to go back and look at when we pick up the project the next day.
Well, the MAM actually doesn't store the media. I feel like we were shortchanged!
It's the manager. It doesn't do the heavy lifting. When else have you seen the manager do the heavy lifting?!
Will our troubles never cease?!
There's constructing the request and then there's doing the request.
We're here to explore. We're not here to create bike sheds with bike sheds inside. Yes! This is not the place for abstractions.
We're building up enough language to help us with our exploration. Only write the functions that you need to help you learn more. As soon as you've learned enough, stop writing functions and move on. The point is to keep learning, not to keep abstracting.
You can use the command line, but it's worth doing in the REPL, because you're starting to actually explore the library too.
Utilitarian tends to win in the end. Things that let you get things done quickly. Those solutions are great solutions.
It's good to fiddle because you can play around with it. You can write it the wrong way four times, and get those out of your system before arriving at what you want to use.
The fiddle is here to help you figure it out.
We were doing all this exploring, and we stumbled into doing some actual work!
There's no better way to know you've arrived at the end of your exploring then when all of a sudden, the thing you're trying to do, is now finished!
Over the course of your exploration, you go from exploring to doing actual work. There's no seam between the two activities. Exploring dwindles down as you know more.
Your fiddle is turning into your recipe: a semi-automated way by hand. It's like you've stumbled into a working program.
You're still learning as you go through the process again and again. You're always learning.
You cannot overstate how important it is to experience the variance of the data by hand.
You think, "Oh! I see the pattern!", and then it's example seven that blows up your pattern. Then you think, "Oh, but now I know the pattern!", and then example fifteen blows it up.
You're getting a lot of direct experience with the process. That's going to help you make better abstractions for the application.
You're going to learn it sometime: either now or in the future. It's better to learn it now when you're in exploring mode than later when you're on the hook and your boss is breathing down your neck!
We've sent the asynchronous notification back to the work giver via the Outlook message queue.
It's fun to do the first 10, but after that, you probably want the computer to do all the heavy lifting for you.
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "exploring new data and APIs". We peruse APIs to uncover the data hidden beneath.
"This is a situated problem. It's a real-world problem. Like many real-world problems, there are parts we control and parts we don't control. We have to figure out those parts."
"What do we need to know to figure out the information?"
"I like to start with the way people naturally talk about these things. If you start building up your language, it helps to describe things in either the innate input the system must have or the way people talk about it."
"[Honey SQL] will do all the interpolation for you. It's just wonderful! That one feature alone would be enough, but there's plenty more."
"That's a great thing about Hiccup and Honey SQL and other formats that use Clojure data structures to describe things: you get the power of structural editing."
"It's yet another example of building up the vocabulary of the system so that you're able to talk about it at a higher level."
"Practicality is the name of the game here. Imagine you're exploring in a forest and you're trying to figure out what you want to put in your backpack to take along with you. You don't want to take a lot of heavy, complicated tools. You want to only bring along the stuff that is actually useful to you!"
"You only need to build up what is necessary to keep exploring because that's the point. The point is to learn. The point isn't to pre-optimize and make the abstractions."
"My activity is centered around this fiddle file as I'm exploring."
"We have all this cool information, but we're not making database table highlight reels. We're making sports highlight reels in Sportify!"
"How do you make sense of a brand new JSON API that you have never dealt with before?" "By using it."
"This is a good opportunity to mention we have a series called 'Web of Complexity'. The title should give you a sense of what we think of HTTP in general."
"The name web should already be a bad omen! We never use the name 'web' in a positive sense anywhere else."
"Perfect time to use our nonlinear history, aka the fiddle!"
"We've made several really composable ingredients that we're now mixing together as we're learning more about the system."
"Most of my time was spent sifting through the data, not actually making the calls!"
"I'm communicating to myself tomorrow, because I want to forget all this context, go home, do something else, not think about work, and come back to work and pick it all up again."
"One of the benefits of using Clojure to explore is you have a full, rich programming language—one with a full syntax including comments. By doing it all in one fiddle with these comments, you can pick it up in the morning."
"Our lives are nonlinear. We get interrupted. We take a break. We have the audacity to go home and not think about work!"
"It's like a workbench. We're laying out our tools. We're laying out our pieces. Our fiddle file is our workbench and we leave little post-it notes on that workbench to remind us of things."
Here is an example that uses the Cloudflare Streams API. It requires authentication, so we want to factor that out.
First, define some endpoints to work with. Create pure functions for just the part unique to each endpoint.
(defn check-status-req [id]
{:method :get
:path id})
(defn delete-req [id]
{:method :delete
:path id})
Then create a function to expand the request with the "common" parts:
(defn full-req
[cloudflare api-req]
(let [{:keys [api-key account-id]} cloudflare
{:keys [method path], throw? :throw} api-req]
{:async true
:method method
:uri (format "https://api.cloudflare.com/client/v4/accounts/%s/stream/%s" account-id path)
:headers {"Authorization" (str "Bearer " api-key)
"Accept" "application/json"}
:throw throw?}))
For the purposes of illustration, the cloudflare configuration is:
(def cloudflare
{:api-key "super-secret"
:account-id "42424242"})
See the full request like so:
(full-req cloudflare (check-status-req "abcdefg123456789"))
Which returns:
{:async true
:method :get
:uri "https://api.cloudflare.com/client/v4/accounts/42424242/stream/abcdefg123456789"
:headers {"Authorization" "Bearer super-secret"
"Accept" "application/json"}
:throw nil}
Use [babashka.http-client :as http]
, and call the endpoints like so:
@(http/request (full-req cloudflare (check-status-req "abcdefg123456789")))
@(http/request (full-req cloudflare (delete-req "abcdefg123456789")))
Note, that in a REPL-connected editor, you evaluate each form, so you can see just the check-status-req
part, or move out one level and see the full-req
part, or move out one more level and actually run it. That lets you iron out the details to make sure you have them right.
Finally, you can make some helpers to use in the REPL or imperative code. The helpers stitch the process together:
(defn request!
[cloudflare req]
(-> @(http/request (full-req cloudflare req))
(update :body #(json/parse-string % true))))
(defn check-status!
[cloudflare id]
(request! cloudflare (check-status-req id)))
(defn delete-stream!
[cloudflare id]
(request! cloudflare (delete-req id)))
They can be called like:
(check-status! cloudflare "abcdefg123456789")
(delete-stream! cloudflare "abcdefg123456789")
The helpers should do nothing except compose together other parts for convenience. All the real work is in the pure functions.
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "using the REPL to explore". We find ourselves in a murky situation, so we go to our REPL-connected editor to shine some light on the details.
"We often want to see highlights more than we want to see the game."
"Like so many things with interns, there is really low supervision, so no one's really worried about how we get it done."
"Having something done is better than not, so the bar is 0."
"It sounds like a natural integration with an asynchronous message queue called 'Outlook'."
"I like to call it 'streaming specification'. I'm not going to tell you everything. I'm coming up with this off the top my head."
"We call them situated problems. They're problems that are set in the real world, so you need to know about all the things in the real world. It's a learning problem."
"You say, 'the things we can't control.' I think of it as, 'the things that are foisted upon us!'"
"We're going to find a way to use Clojure to solve this problem. I bet you didn't see that coming!"
"The first thing I usually do is go for the completely correct and entirely comprehensive documentation that describes all of the different use cases that I might need, and in fact, has a sample code." (Ha! If only!)
"It can be said that programming is debugging an empty file." "The first bug is my application does nothing!"
"You want to get to your first rewrite as fast as possible, so write the messy version first. Then you can learn what you want the next version to be—even if the next version is also messy. The sooner you learn the better."
"I'm starting to build up little pieces to help me explore."
"We're going to figure it out interactively, using the REPL. We're getting our bearings."
"Get in the code right away—hitting an external service as soon as possible—so we can begin to learn, so that we're solving the right problem instead of some problem of our imagination."
"Once it's Clojure data structures, the whole world of Clojure opens up. All the power of mixing and matching and analyzing that data is open to you."
"We're getting into the real thing. We're getting real data, real code. This is going to begin to grow up into something, but for now, we just want to see what's there—begin to explore—get some working code so that we can."
"It's hard to imagine the workflow, because it's not a workflow that you do in other languages. If you haven't done this before, it's hard to just imagine it."
"It's not just REPL first, but REPL-connected editor first, and there's a distinction."
If you are unfamilar with the Clojure command line, check out the Deps and CLI Guide. You can set up global aliases in $HOME/.clojure/deps.edn
. For example, this is one Christoph uses:
{:aliases
{:nrepl {:extra-deps {nrepl/nrepl {:mvn/version "RELEASE"}
cider/piggieback {:mvn/version "RELEASE"}}
:jvm-opts ["-server" "-XX:MaxMetaspaceSize=256m" "-Xmx1280m"]
:main-opts ["-m" "nrepl.cmdline"]}}}
Run it with:
clojure -M:nrepl
You can see more examples in Nate's dotfiles and in Sean Corfield's dot-clojure project.
#_
to ignore the next formEach week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "introducing Sportify!". We tackle a new application, thinking it'll be an easy win—only to discover that our home run was a foul, and the real world is about to strike us out!
Our discussion includes:
Selected quotes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "thankfulness". We reflect on Clojure, the community, and how much we have to be thankful for.
Our discussion includes:
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "taking the REPL beyond your application". We free our REPL to explore and automate the world around us.
Our discussion includes:
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "composition is life". We reflect on keeping the necessary mess at the edges so our core can be composed together with beauty and simplicity.
Our discussion includes:
Selected quotes:
Common kinds of functions:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "composing your application". We get a handle on bringing I/O resources together in an application.
Our discussion includes:
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "playing games with data." We go back to start and play through a composition strategy to see where we land.
Our discussion includes:
Selected quotes:
"I think the lack of reusability comes in object-oriented languages, not functional languages. Because the problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.
If you have referentially transparent code, if you have pure functions all the data comes in its input arguments and everything goes out and leave no state behind it’s incredibly reusable."
~ Joe Armstrong in "Coders at Work"
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "core and composition." We venture toward the core of a solution.
Our discussion includes:
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "decomposition." We help our code through a breakup so it can find its true colors.
Our discussion includes:
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "effective composition." We search for that sweet spot between full-featured mixes and simple ingredients when crafting your software recipes.
Our discussion includes:
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "freedom through constraints." We bump into limiting constraints and learn to love their freedoms.
Our discussion includes:
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "effective expressiveness." We compose our thoughts on why Clojure expressiveness is so effective but can be so hard to learn.
Our discussion includes:
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "cond-> and cond->>." We devote some time to two functions that are indispensable when computations require variation.
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "if, when, case, condp, and cond." We wander through the myriad ways of making decisions and listing choices in Clojure.
Selected quotes:
Links:
Related Episodes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "doall, dorun, doseq, and run!." We eagerly discuss the times we need to interact with the messy world from our nice clean language.
Selected quotes:
Links:
Related Episodes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "multimethods." We discuss polymorphism and how we tackle dynamic data with families of functions.
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "let." Let us share some tricks to reduce nesting and make your code easier to understand.
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "for." We talk about this data generating macro, while we remember situations when it was useful.
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "sort and sort-by." We lay out a list of ways to sort your data, ordered by their relative power.
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "partition-by and group-by." We get a handle on big buckets of data by sifting elements into smaller buckets.
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "filter, filterv, remove, keep, and keep-indexed." We talk about sifting data and marvel at the simple function that can turn two steps into one.
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "first, second, key, and val." We talk about positive nothing and the proliferation of tuples.
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "apply." We take time to unroll some examples of this function.
Selected quotes:
third
function?"Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "comp." We create a whole episode by combining examples of useful uses of comp.
Selected quotes:
Links:
Related Episodes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "partial." We cover some of the ways we use partial, without getting too literal.
Selected quotes:
Links:
Episodes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "some-> and some->>." We spend some time going through how these macros help keep our code nil-safe.
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "juxt." We take a turn with juxt, looking at all the ways it can help line up data.
Selected quotes:
Links:
Episodes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "merge-with." We focus in on merge-with, a powerful function for aggregating data.
Selected quotes:
Links:
Episodes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Deploying Clojure." We survey the myriad ways we've used to launch our code into production, and laugh about the various complexities we've found.
Selected quotes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Checking websocket health." We worry about the health of our websockets and, after looking for help from the standards bodies, roll up our sleeves and handle it ourselves.
Selected quotes:
Related episodes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Organizing our websocket code." We switch to using a component to manage our websockets, enabling ease of development and future growth.
Selected quotes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Using websockets for notification." We wander into the weeds, jumping through the myriad hoops required to deliver a simple notification.
Selected quotes:
Related episodes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Websockets." We talk about spontaneously sending data from the server to the client to address our users' insecurities.
Selected quotes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Request handler pitfalls." We examine our history writing web handlers and talk about all the ways we've broken them.
Selected quotes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Serving static assets." We tease apart the layers involved in serving static assets and are surprised by how many we find.
Selected quotes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Resource management for web handlers." We manage to find a way to get our handlers the resources they need to get real work done.
Selected quotes:
Links:
Related episodes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Making middleware to handle JSON." We reinvent the wheel, and along the way discover a few reasons why you might want to do so as well.
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Ring middleware." We find that the middle is a very good place to start when almost everything is composed functions.
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Ring, the foundation of Clojure HTTP" We focus on the bedrock abstraction for all Clojure web applications and marvel at its simplicity.
Selected quotes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "HTTP and the Web of Complexity." We launch a new series and immediately get tangled in the many layers that make up the web.
Selected quotes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "2019." We look back at the past year and highlight our favorite episodes.
Selected quotes:
Related episodes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Transducers." We unpack transducers and find a familiar pattern that enables freedom from collections.
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Reducers." We look at clojure.core.reducers and how it extracts performance by composing reducing functions.
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Sequences." We examine the sequence abstraction and then ponder how it helps and hinders our data transformation.
Selected quotes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Reduce and reducing functions" We take a long hard look at reduce and find the first of many generally useful nuggets inside.
Selected quotes:
Related episodes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Clojure/Conj 2019 Recap" We go through our notes and recall the most memorable talks from the Conj last week.
Selected quotes:
Talks:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Opt-In Complexity" We discuss complexity and try to come up with a simple explanation for why Clojurians avoid it so ruthlessly.
Selected quotes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Sets! What are they good for?" We examine one of the lesser used data structures in Clojure and talk about its unique characteristics and uses.
Selected quotes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "Working with heavily nested trees." We discuss three powerful libraries (Specter, Spectacles, and clojure.walk) and where they might fit into our Clojure programs.
Selected quotes:
Related episodes:
Links:
Each week, we answer a different question about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the question is: "How can I save my data from serialization?" We record our thoughts on the many trade-offs we have encountered preserving our data when it leaves our programs.
Selected quotes:
Related episodes:
Links:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, our topic is: "Functions! Functions! Functions!" We wonder how we could function without these critical building blocks, so we catagorize their varied uses.
Selected quotes:
Related episodes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, our topic is: "Maps! Maps! Maps!" We discuss maps and their useful features, including a key distinction that we couldn't live without.
Selected quotes:
Related episodes:
Links:
Code sample:
;; Player records: one nested, one with rich keys.
(def players-nested
[{:player {:id 123
:name "Russell"
:position :point-guard}
:team {:id 432
:name "Durham Denizens"
:division :eastern}}
{:player {:id 124
:name "Frank"
:position :midfield}
:team {:id 432
:name "Durham Denizens"
:division :eastern}}])
(def players-rich
[{:player/id 123
:player/name "Russell"
:player/position :point-guard
:team/id 432
:team/name "Durham Denizens"
:team/division :eastern}
{:player/id 124
:player/name "Frank"
:player/position :midfield
:team/id 432
:team/name "Durham Denizens"
:team/division :eastern}])
;; Extract player and team id, along with team name
; Nested
(defn extract
[player]
(let [{:keys [player team]} player]
{:player (select-keys player [:id])
:team (select-keys team [:id :name])}))
#_(map extract players-nested)
; ({:player {:id 123}, :team {:id 432, :name "Durham Denizens"}}
; {:player {:id 124}, :team {:id 432, :name "Durham Denizens"}})
; Rich
#_(map #(select-keys % [:player/id :team/id :team/name]) players-rich)
; ({:player/id 123, :team/id 432, :team/name "Durham Denizens"}
; {:player/id 124, :team/id 432, :team/name "Durham Denizens"})
;; Sort by team name and then player name
; Nested
#_(sort-by (juxt #(-> % :team :name) #(-> % :player :name)) players-nested)
; ({:player {:id 124, :name "Frank", :position :midfield},
; :team {:id 432, :name "Durham Denizens", :division :eastern}}
; {:player {:id 123, :name "Russell", :position :point-guard},
; :team {:id 432, :name "Durham Denizens", :division :eastern}})
; Rich
#_(sort-by (juxt :team/name :player/name) players-rich)
; ({:player/id 124,
; :player/name "Frank",
; :player/position :midfield,
; :team/id 432,
; :team/name "Durham Denizens",
; :team/division :eastern}
; {:player/id 123,
; :player/name "Russell",
; :player/position :point-guard,
; :team/id 432,
; :team/name "Durham Denizens",
; :team/division :eastern})
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, our topic is: "Parentheses! Parentheses! Parentheses!" We defend the lowly parentheses, and discuss the benefits of having this stalwart shepherd dutifully organizing our code.
Selected quotes:
Each week, we discuss a different topic about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, our topic is: "Keywords! Keywords! Keywords!" We examine all the fascinating properties of keywords, how to use them, and why they're so much better than strings and enums.
Selected quotes:
identical?
because Clojure gives us value semantics!"Each week, we answer a different question about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the question is: "Help! How do I fix my REPL?" We catalog the many ways we've broken our REPLs and talk through our strategies for getting back on track.
Selected quotes:
Each week, we answer a different question about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the question is: "What is 'nil punning'?" We gaze into the nil and find a surprising number of things to talk about.
Selected quotes:
Each week, we answer a different question about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the question is: "When is Clojure not the right tool for the job?" We look at the varied forms that Clojure can assume and consider where it might not fit.
Selected quotes:
Related episodes
Each week, we answer a different question about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the question is: "Why have derived fields in data when I can just calculate derived data as needed with a function?" We take a focused look at the balance of using functions or derived fields and where each is preferable.
Selected quotes:
Each week, we answer a different question about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the question is: "What's so different about Clojure's REPL?" We evaluate what a REPL really is and show that it's much more about the developer experience than simply calculating values.
Selected quotes:
Related episodes
Each week, we answer a different question about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the question is: "What is 'faking' a resource?" We talk about the virtues of faking and then outline several real techniques for getting work done.
Selected quotes:
Related episodes
Each week, we answer a different question about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the question is: "What does it mean to be 'data-oriented'?" We merge together different aspects of Clojure's data orientation, and specify which of those help make development more pleasant.
Selected quotes:
Each week, we answer a different question about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the question is: "Why do Clojurians make such a big deal about immutability?" We cover several practical side effects of immutability and why we've become such big fans of data that doesn't let us down.
Selected quotes:
Related episodes:
Each week, we answer a different question about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the question is: "Should I use lein, boot, or tools.deps?" We assemble a list of build tool characteristics and talk about how each tool stacks up before giving our recommendations.
Selected quotes:
Each week, we answer a different question about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the question is: "Why use Clojure over another functional language?". We examine the different categories of functional programming languages and distill out what differentiates Clojure and why we prefer it.
Selected quotes:
Related episodes:
Each week, we answer a different question about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the question is: "How do I convince my coworkers to use Clojure?". We recall our own experiences evangelizing Clojure and give practical advice from the trenches.
Selected quotes:
Related episodes:
Each week, we answer a different question about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign, send an email to [email protected], or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the question is: "What advice would you give to someone getting started with Clojure?". We trade off giving practical tips for intrepid learners while we reminisce about our own paths into Clojure.
Selected quotes:
Related episodes:
It's summertime, and that means it's time for something new. Each week, we will answer a different question about Clojure and functional programming.
If you have a question you'd like us to discuss, tweet @clojuredesign or send an email to [email protected].
This week, we're starting off with "Why do you recommend Clojure?". We take turns sharing our favorite reasons, and we can't help but have fun riffing on how enjoyable Clojure is to use. Come along for the ride.
Selected quotes:
Christoph and Nate lift concepts from the raw log-parsing series.
with-open
, fully lazy processing results in an I/O error, because the file has been closed already.lazy-seq
.
lazy-seq
and return either nil
(to indicate the end) or a sequence created by calling cons
on a real value and a recursive call to itself.Related episodes:
Clojure in this episode:
lazy-seq
, cons
with-open
Christoph finds exceptional log lines and takes a more literal approach.
map
and reduce
!"parse-line
on the inputs before doing their specific parsing.parse-line
is :raw/line
, which is the entire line, and that's always there.parse-line
will always return a map for each line.:raw/line
:log/date
), that means it didn't parse, and is probably a continuation of a previous line.some->>
, which shortcuts on nil
.take-while
to find all the lines that are bare.:log/message
key.Related episodes:
Clojure in this episode:
some->>
, take-while
Code sample from this episode:
(ns devops.week-06
(:require
[clojure.string :as string]
[devops.week-02 :refer [process-log]]
[devops.week-05 :refer [parse-357-error parse-sprinkle]]
))
(def general-re #"(\d\d\d\d-\d\d-\d\d)\s+(\d\d:\d\d:\d\d)\s+\|\s+(\S+)\s+\|\s+(\S+)\s+\|\s+(\S+)\s+\|\s(.*)")
(defn parse-line
[line]
(if-let [[whole dt tm thread-name level ns message] (re-matches general-re line)]
{:raw/line whole
:log/date dt
:log/time tm
:log/thread thread-name
:log/level level
:log/namespace ns
:log/message message}
{:raw/line line
:log/message line}))
(defn bare-line?
[line]
(nil? (:log/date line)))
(defn parse-exception-info
[lines]
(let [first-line (first lines)
[_whole classname] (some->> first-line :log/message (re-matches #"([^ ]+) #error \{"))]
(when classname
(let [error-lines (cons first-line (take-while bare-line? (rest lines)))
error-str (string/join "\n" (map :log/message error-lines))]
(merge first-line
{:kind :error
:error/class classname
:log/message error-str})))))
(defn parse-next
[lines]
(or (parse-357-error lines)
(parse-sprinkle lines)
(parse-exception-info lines)))
(defn parse-all
[lines]
(lazy-seq
(when (seq lines)
(if-some [found (parse-next lines)]
(cons found (parse-all (rest lines)))
(parse-all (rest lines))))))
(comment
(process-log "sample.log" #(->> % (map parse-line) parse-all doall))
)
Log file sample:
2019-05-14 16:48:57 | process-Poster | INFO | com.donutgram.poster | transaction failed while updating user joe: code 357
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | failed to add sprinkle to donut 50493
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | sprinkle fail reason: unknown state
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | Poster #error {
:cause "Failed to lock the synchronizer"
:data {}
:via
[{:type clojure.lang.ExceptionInfo
:message "Failed to lock the synchronizer"
:data {}
:at [process.poster$eval50560 invokeStatic "poster.clj" 40]}]
:trace
[[process.poster$eval50560 invokeStatic "poster.clj" 40]
[clojure.lang.AFn run "AFn.java" 22]
[java.lang.Thread run "Thread.java" 748]]}
Nate needs to parse two different errors and takes some time to compose himself.
lazy-seq
.if
s or maybe an infinite cond
. Perhaps a complex state machine with backtracking?nil
if no error was found at the head of the sequence.or
block.or
block will try each one in turn until one matches and then that is the result.parse-next
, and the function that manages the lazy sequence is parse-all
.parse-all
has a call to lazy-seq
.parse-next
on the sequence.
cons
to add that value to the beginning of a recursive call to itself.nil
, it will recursively call itself with the rest
of the sequence, thus advancing the parsing forward one step.parse-all
function.Related episodes:
Clojure in this episode:
seq
, cons
, rest
lazy-seq
or
, cond
Code sample from this episode:
(ns devops.week-05
(:require
[devops.week-01 :refer [parse-line]]
[devops.week-02 :refer [process-log]]
[devops.week-03 :refer [sprinkle-errors-by-type]]
))
(defn parse-sprinkle
[lines]
(let [[first-line second-line] lines
[_whole donut-id] (some->> first-line :log/message (re-matches #"failed to add sprinkle to donut (\d+)"))
[_whole error] (some->> second-line :log/message (re-matches #"sprinkle fail reason: (.*)"))]
(when (and donut-id error)
(merge first-line
{:kind :sprinkle
:sprinkle/donut-id donut-id
:sprinkle/error error}))))
(defn parse-357-error
[lines]
(let [[first-line] lines
[_whole user] (some->> first-line :log/message (re-matches #"transaction failed while updating user ([^:]+): code 357"))]
(when user
(merge first-line
{:kind :code-357
:code-357/user user}))))
(defn parse-next
[lines]
(or (parse-357-error lines)
(parse-sprinkle lines)))
(defn parse-all
[lines]
(lazy-seq
(when (seq lines)
(if-some [found (parse-next lines)]
(cons found (parse-all (rest lines)))
(parse-all (rest lines))))))
(defn kind?
([kind]
#(kind? % kind))
([line kind]
(= kind (:kind line))))
(comment
(process-log "sample.log" #(->> % (map parse-line) parse-all (map :kind) doall))
(process-log "sample.log" #(->> % (map parse-line) parse-all (filter (kind? :sprinkle)) sprinkle-errors-by-type))
)
Log file sample:
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | transaction failed while updating user joe: code 357
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | failed to add sprinkle to donut 23948
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | sprinkle fail reason: should never happen
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | failed to add sprinkle to donut 94238
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | sprinkle fail reason: timeout exceeded threshold
2019-05-14 16:48:56 | process-Poster | INFO | com.donutgram.poster | transaction failed while updating user sally: code 357
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | failed to add sprinkle to donut 24839
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | sprinkle fail reason: too many requests
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | failed to add sprinkle to donut 19238
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | sprinkle fail reason: should never happen
2019-05-14 16:48:57 | process-Poster | INFO | com.donutgram.poster | transaction failed while updating user joe: code 357
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | failed to add sprinkle to donut 50493
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | sprinkle fail reason: unknown state
Christoph finds map doesn't let him be lazy enough.
partition
.map
to convert lines into sprinkle errors anymore.map
and filter
.recur
with the tail until found.take-while
to find the second half of the error.(seq lines)
is not nil.lazy-seq
function.lazy-seq
is a sequence, but it hasn't been realized yet.cons
it onto the head of an invocation of lazy-seq
to make a new sequence.lazy-seq
.delay
, because it wraps the code in something that will only be evaluated when it is first accessed.nil
, which indicates that the sequence is complete.cons
on a value and a call to lazy-seq
.(when (seq lines) ...
, to ensure that the sequence terminates when there is no data left.lazy-seq
, we can cons
the found value onto a recursive call to the function.rest
of the sequence to try parsing from there.lazy-seq
.Related episodes:
Clojure in this episode:
partition
seq
, cons
, rest
lazy-seq
, delay
map
, filter
, take-while
recur
Code sample from this episode:
(ns devops.week-04
(:require
[devops.week-01 :refer [parse-line]]
[devops.week-02 :refer [process-log]]
[devops.week-03 :refer [sprinkle-errors-by-type]]
))
(defn sprinkle-error-seq
[lines]
(lazy-seq
(when (seq lines)
(let [[first-line second-line & tail] lines
[_whole donut-id] (some->> first-line :log/message (re-matches #"failed to add sprinkle to donut (\d+)"))
[_whole error] (some->> second-line :log/message (re-matches #"sprinkle fail reason: (.*)"))]
(if (and donut-id error)
(cons (merge first-line
{:kind :sprinkle
:sprinkle/donut-id donut-id
:sprinkle/error error})
(sprinkle-error-seq tail))
(sprinkle-error-seq (next lines)))))))
(comment
(process-log "sample.log" #(->> % (map parse-line) sprinkle-error-seq doall))
(process-log "sample.log" #(->> % (map parse-line) sprinkle-error-seq sprinkle-errors-by-type))
)
Nate finds that trouble comes in pairs.
failed to add sprinkle to donut 23948
.sprinkle fail reason: db timeout exceeded
.parse-details
function goes out the window, what can we do to get more context for each parsing?partition
function.step
argument varies how far you reach into the collection for the next chunk.(partition 2 1 (range 1 7))
yields ((1 2) (2 3) (3 4) (4 5) (5 6))
. Each element is paired with it's following element.nil
is returned, so our list becomes a sequence of sprinkle errors or nil
s.nil
s and summarize the sprinkle errors.partition
is lazy. It constructs chunks on demand.(partition 10 1 lines)
or (partition 100 1 lines)
?partition-infinity
?"Related episodes:
Clojure in this episode:
partition
->>
map
, filter
group-by
, frequencies
Code sample from this episode:
(ns devops.week-03
(:require
[devops.week-01 :refer [parse-line]]
[devops.week-02 :refer [process-log]]
))
(defn parse-sprinkle-error
[line-pairs]
(let [[first-line second-line] line-pairs
[_whole donut-id] (some->> first-line :log/message (re-matches #"failed to add sprinkle to donut (\d+)"))
[_whole error] (some->> second-line :log/message (re-matches #"sprinkle fail reason: (.*)"))]
(when (and donut-id error)
(merge first-line
{:kind :sprinkle
:sprinkle/donut-id donut-id
:sprinkle/error error}))))
(defn sprinkle-errors
[lines]
(->> lines
(partition 2 1)
(map parse-sprinkle-error)
(remove nil?)))
(defn sprinkle-errors-by-type
[errors]
(->> errors
(map :sprinkle/error)
(frequencies)))
(comment
(process-log "sample.log" #(->> % (map parse-line) sprinkle-errors sprinkle-errors-by-type))
)
Log file sample:
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | failed to add sprinkle to donut 23948
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | sprinkle fail reason: should never happen
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | failed to add sprinkle to donut 94238
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | sprinkle fail reason: timeout exceeded threshold
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | failed to add sprinkle to donut 24839
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | sprinkle fail reason: too many requests
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | failed to add sprinkle to donut 19238
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | sprinkle fail reason: should never happen
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | failed to add sprinkle to donut 50493
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | sprinkle fail reason: unknown state
Christoph's eagerness to analyze the big production logs shows him the value of being lazy instead.
map
of a lazy sequence needs more data, it gets it on demand.lines
function slurps in the file and splits on newline.line-seq
.with-open
macro, which closes the file handle after the body is complete.lines
function, why don't we just bring that code into the summary
function?with-open
so that all steps including summary complete before the file is closed.with-open
and line-seq
for each new summary.with-open
? To separate that idiom into just one place.line-seq
, is there a way we can hand in the logic we need all at once?with-open
and takes a function.process
. That's nice and generic."->>
(the thread-last macro) in this case.process
function.group-by
or frequencies
will suffice, but if you don't have one of those, reach for doall
.doall
."doall
at the beginning or at the end of the thread? We like it at the end.Message Queue discussion:
parse-details
that doesn't use macros.or
macro.or
, each regex is matched in a when-let
, the body of which uses the matches to construct the detailed data.nil
is returned and the or
will move on to the next block.or
as only for booleans, but it works well for controlling program flow as well.Related episodes:
Clojure in this episode:
slurp
, with-open
, line-seq
->>
or
, when-let
map
, filter
group-by
, frequencies
doall
clojure.string/split-lines
Code sample from this episode:
(ns devops.week-02
(:require
[clojure.java.io :as io]
[devops.week-01 :refer [parse-line parse-details]]
))
; Parsing and summarizing
(defn parse-log
[raw-lines]
(->> raw-lines
(map parse-line)
(filter some?)
(map parse-details)))
(defn code-357-by-user
[lines]
(->> lines
(filter #(= :code-357 (:kind %)))
(map :code-357/user)
(frequencies)))
; Failed Attempt: returning from with-open
(defn lines
[filename]
(with-open [in (io/reader filename)]
(line-seq in)))
(defn count-by-user
[filename]
(->> (lines filename)
(parse-log)
(code-357-by-user)))
; Throws IOException "Stream closed"
#_(count-by-user "sample.log")
; Works, but I/O is coupled with the logic.
(defn count-by-user
[filename]
(with-open [in (io/reader filename)]
(->> (line-seq in)
(parse-log)
(doall)
(code-357-by-user))))
#_(count-by-user "sample.log")
; Separates out I/O. Allows us to compose the processing.
(defn process-log
[filename f]
(with-open [in (io/reader filename)]
(->> (line-seq in)
(f))))
; Look at the first 10 lines that parsed
#_(process-log "sample.log" #(->> % parse-log (take 10) doall))
; Count up all the "code 357" errors by user
(defn count-by-user
[filename]
(process-log filename #(->> % parse-log code-357-by-user)))
#_(count-by-user "sample.log")
Log file sample:
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | transaction failed while updating user joe: code 357
2019-05-14 16:48:56 | process-Poster | INFO | com.donutgram.poster | transaction failed while updating user sally: code 357
2019-05-14 16:48:57 | process-Poster | INFO | com.donutgram.poster | transaction failed while updating user joe: code 357
Nate is dropped in the middle of a huge log file and hunts for the source of the errors.
parse-line
that separates out all the common elements for each line:
kind
field to identify which kind of log line it is.constantly
. It's not just juxt
."cond
:
includes?
in the condition to detect the kind and re-matches
to parse.def
.cond
. No "cond-let" macro.cond-let
macro, but should we?parse-details
which goes through each regex until one matches and then invokes the handler for that one. (See below.)filter
and group-by
to get our user count.slurp
, split
, map
, filter
, and then aggregate.Related episodes:
Clojure in this episode:
#""
re-matches
case
cond
if-let
->>
slurp
filter
group-by
def
constantly
juxt
clojure.string/
includes?
split
Related links:
Code sample from this episode:
(ns devops.week-01
(:require
[clojure.java.io :as io]
[clojure.string :as string]
))
;; General parsing
(def general-re #"(\d\d\d\d-\d\d-\d\d)\s+(\d\d:\d\d:\d\d)\s+\|\s+(\S+)\s+\|\s+(\S+)\s+\|\s+(\S+)\s+\|\s(.*)")
(defn parse-line
[line]
(when-let [[whole dt tm thread-name level ns message] (re-matches general-re line)]
{:raw/line whole
:log/date dt
:log/time tm
:log/thread thread-name
:log/level level
:log/namespace ns
:log/message message
}))
(defn general-parse
[lines]
(->> lines
(map parse-line)
(filter some?)))
;; Detailed parsing
(def detail-specs
[[#"transaction failed while updating user ([^:]+): code 357"
(fn [[_whole user]] {:kind :code-357 :code-357/user user})]
])
(defn try-detail-spec
[message [re fn]]
(when-some [matches (re-matches re message)]
(fn matches)))
(defn parse-details
[entry]
(let [{:keys [log/message]} entry]
(if-some [extra (->> detail-specs
(map (partial try-detail-spec message))
(filter some?)
(first))]
(merge entry extra)
entry)))
;; Log analysis
(defn lines
[filename]
(->> (slurp filename)
(string/split-lines)))
(defn summarize
[filename calc]
(->> (lines filename)
(general-parse)
(map parse-details)
(calc)))
;; data summarizing
(defn code-357-by-user
[entries]
(->> entries
(filter #(= :code-357 (:kind %)))
(map :code-357/user)
(frequencies)))
(comment
(summarize "sample.log" code-357-by-user)
)
Log file sample:
2019-05-14 16:48:55 | process-Poster | INFO | com.donutgram.poster | transaction failed while updating user joe: code 357
2019-05-14 16:48:56 | process-Poster | INFO | com.donutgram.poster | transaction failed while updating user sally: code 357
2019-05-14 16:48:57 | process-Poster | INFO | com.donutgram.poster | transaction failed while updating user joe: code 357
Christoph has gigs of log data and he's looking to Clojure for some help.
Clojure in this episode:
nil
Nate and Christoph reflect on what they learned during the Twitter series.
invoke
function. (Inspired by aws-api.)Message Queue discussion:
Related episodes:
Clojure in this episode:
nil
Related links:
Christoph thinks goals are data, not function names.
invoke
. At least only one that gets work done.{:command :twitter/fetch-timeline :twitter/last-tweet-id 1234}
(handle/fetch-timeline handle {:twitter/last-tweet-id 1234})
{:url "https://twitter.com/fetch-timeline" :last-tweet 1234}
{:command :twitter/operation :twitter/command :fetch-timeline :twitter/last-tweet-id 1234}
Message Queue discussion:
Related episodes:
Clojure in this episode:
defprotocol
defrecord
Related links:
Nate wants to experiment with the UI, but Twitter keeps getting the results.
Related episodes:
Clojure in this episode:
defprotocol
defrecord
component/system-map
Related projects:
Christoph needs to test his logic, but he must pry it from the clutches of side effects.
meta
or a nested key (like :raw
) to attach raw data for debugging or the "attempt" log (see Ep 022)loop
+ recur
command
to worker and bind response
command
+ response
to decider and bind new-command
(when new-command (recur new-command))
Message Queue discussion:
Related episodes:
Clojure in this episode:
loop
recur
meta
when
Nate gets messy finding ingredients for his algorithm cake.
component
to organize our app.start
method is a natural place to do migrations, indexing, etc.start
and stop
.start
: initialize internal data, open connection, prep work, etc.stop
: finishing work, closing connections, etc.system
.[updated-handle, result]
. The handle will only change when it has to re-auth.atom
for the handle. Request function mutates the handle when it has to re-auth.at-at
to schedule an interval for calling a functionScheduledThreadPoolExecutor
.at-at/stop
in your component/stop
method so component.repl/reset
doesn't make more and more timers!Message Queue discussion:
tools.namespace.repl/refresh
will find those dangling references.refresh
to check it all at once.Related episodes:
Related projects:
Clojure in this episode:
atom
component/
start
stop
system-map
component.repl/
reset
at-at/
mk-pool
every
stop
Christoph questions his attempts to post to Twitter.
Message Queue discussion:
:twitter/id, :twitter/status
vs :local/id, :local/text
Related episodes:
Related projects:
Clojure in this episode:
pr-str
Nate wants to tweet regularly, so he asks Clojure for some help.
Related episodes:
Related projects:
Clojure in this episode:
nil
Christoph and Nate discuss the flavor of pure data.
filter
the points and reduce
the good ones."loop
+ recur
for data transform is a code smell.
loop
+ recur
for recursion or blocking operations (like core.async
)
filter
+ map
+ reduce
.reduce
, loop
+ recur
filter
+ map
+ reduce
(run! println lines)
.Related episodes:
Clojure in this episode:
filter
, map
, reduce
loop
, recur
group-by
run!
println
Nate wants to see more than data structures in a REPL.
filter
then map
, then reduce
split-weeks
in the Clojure cheat sheet.":sunday
, :tuesday
, etc.group-by
the week:
(week-id starting-day-of-week day)
(week-id :sunday {:date "2019-03-08" ...}) => "2019-03-03"
println
.println
a trivial step at the endprintln
could be tested, even the formatting!Related episodes:
Clojure in this episode:
filter
, map
, reduce
group-by
sort-by
with partition-by
take-while
, drop-while
, and split-with
Code sample from this episode:
(ns time.week-05
(:require
[time.week-04 :as week-04]
[java-time :as jt]
))
; Date helpers
(defn start-of-week
"Get the the starting date of the week containing local-date. The week will
start on the named day. Eg. :sunday"
[starting-day-of-week local-date]
(jt/adjust local-date :previous-or-same-day-of-week (jt/day-of-week starting-day-of-week)))
(defn week-dates
"Create a week's worth of dates starting from the given date."
[starting-date]
(->> (range 0 7)
(map #(jt/plus starting-date (jt/days %)))))
; Aggregates
(defn sum-minutes
"Sums the minutes for all kinds: entry, day, week"
[entries]
(->> entries
(map :minutes)
(reduce +)))
; Conversions
(defn entries->days
"Convert a seq of entries to days."
[entries]
(->> (group-by :date entries)
(map (fn [[date xs]] {:date date :minutes (sum-minutes xs)}))))
(defn days->week
"Convert a seq of days into a week. Week is picked by first date."
[starting-day-of-week days]
(let [lookup (into {} (map (juxt :date identity) days))
starting-date (start-of-week starting-day-of-week (:date (first days)))
all-days (->> (week-dates starting-date)
(map #(or (get lookup %)
{:date %
:minutes 0})))]
{:starting-day-of-week starting-day-of-week
:date starting-date
:days (vec all-days)
:minutes (sum-minutes all-days)}))
(defn partition-weeks
[starting-day-of-week days]
(->> days
(sort-by :date)
(partition-by #(start-of-week starting-day-of-week (:date %)))))
(defn days->weeks
"Convert a seq of days into an ordered seq of weeks."
[starting-day-of-week days]
(->> (partition-weeks starting-day-of-week days)
(map (partial days->week starting-day-of-week))))
(comment
(->> (week-04/log-times "time-log.txt")
(entries->days))
(->> (week-04/log-times "time-log.txt")
(entries->days)
(days->weeks :sunday))
)
Christoph wants to teach filter some vocabulary.
filter
out irrelevant entries then reduce
on just thosereduce
becomes trivial.filter
.?
. Eg. some?
, contains?
, every?
(spans-midnight? start-timestamp end-timestamp)
(filter #(spans-midnight? (:start %) (:end %)) entries)
filter
.
(spans-midnight? entry)
(filter spans-midnight? entries)
(weekend? entry)
(filter weekend? entries)
weekend?
with a simpler predicate: (day-of-week? weekday entry)
partial
.weekend?
function is a simple or
of calls to day-of-week?
(day-of-week entry)
that returns the day.day-of-week?
but also for any other logic that needs to pull out the day.weekday?
predicate becomes trivial: (not (weekend? entry))
(partial day-of-week? :sunday)
, etc.(filter (partial day-of-week? :sunday) entries)
:date
key, so the same day-of-week?
predicate works on both.Related episodes:
Clojure in this episode:
filter
reduce
partial
or
Code sample from this episode:
(ns time.week-04
(:require
[time.week-03 :as week-03]
[java-time :as jt]))
; Helper for loading up time entries
(defn log-times
[filename]
(->> (week-03/lines filename)
(week-03/times)))
; Extractors
(defn day-of-week
[entry]
(jt/day-of-week (-> entry :date)))
; Predicates
(defn spans-midnight?
[entry]
(not= (jt/local-date (:start entry)) (jt/local-date (:end entry))))
(defn day-of-week?
[day entry]
(= (day-of-week entry) (jt/day-of-week day)))
(defn weekend?
[entry]
(or (day-of-week? :saturday entry)
(day-of-week? :sunday entry)))
(defn weekday?
[entry]
(not (weekend? entry)))
; Aggregations
(defn total-minutes
[entries]
(->> entries
(map :minutes)
(reduce +)))
(comment
(->> (log-times "time-log.txt")
(filter spans-midnight?))
(->> (log-times "time-log.txt")
(filter (partial day-of-week? :wednesday)))
(->> (log-times "time-log.txt")
(filter weekend?))
(->> (log-times "time-log.txt")
(filter weekday?)
(total-minutes))
)
Nate finds it easier to get a broad view without a microscope.
loop
+ recur
approach is getting complicated!
reduce
. Just write "reducer" functions.reduce
is already filtered.if
for throwing away data.filter
to just pass through Sundays.map
and filter
to get the data in shape first.Related episodes:
Clojure in this episode:
loop
, recur
map
, filter
, reduce
group-by
if
->
, ->>
Code sample from this episode:
(ns time.week-03
(:require
[clojure.java.io :as io]
[clojure.string :as string]
[java-time :as jt]))
; Functions for parsing out the time format: Fri Feb 08 2019 11:30-13:45
(def timestamp-re #"(\w+\s\w+\s\d+\s\d+)\s+(\d{2}:\d{2})-(\d{2}:\d{2})")
(defn localize [dt tm]
(jt/zoned-date-time dt tm (jt/zone-id)))
(defn parse-time [time-str]
(jt/local-time "HH:mm" time-str))
(defn parse-date [date-str]
(jt/local-date "EEE MMM dd yyyy" date-str))
(defn adjust-for-midnight
[start end]
(if (jt/before? end start)
(jt/plus end (jt/days 1))
end))
(defn parse
[line]
(when-let [[whole dt start end] (re-matches timestamp-re line)]
(let [date (parse-date dt)
start (localize date (parse-time start))
end (adjust-for-midnight start (localize date (parse-time end)))]
{:date date
:start start
:end end
:minutes (jt/time-between start end :minutes)})))
; How many minutes did I work on each day?
(defn daily-total-minutes
[times]
(->> times
(group-by :date)
(map (fn [[date entries]] (vector date (reduce + (map :minutes entries)))))
(into {})))
; How many minutes total did I work on Sundays?
(defn on-sunday?
[{:keys [date]}]
(= (jt/day-of-week date) (jt/day-of-week :sunday)))
(defn sunday-minutes
[times]
(->> times
(filter on-sunday?)
(map :minutes)
(reduce +)))
; Functions for turning the time log into a sequence of time entries
(defn lines
[filename]
(->> (slurp filename)
(string/split-lines)))
(defn times
[lines]
(->> lines
(map parse)
(filter some?)))
; Process a time log with the desired summary calculation
(defn summarize
[filename calc]
(->> (lines filename)
(times)
(calc)))
(comment
(summarize "time-log.txt" daily-total-minutes)
(summarize "time-log.txt" sunday-minutes)
)
Christoph discovers that time creates its own alternate universe.
Fri Feb 08 2019 11:30-13:45
loop
and recur
to iterate through the array and track the sum.clone
ing!Related episodes:
Clojure in this episode:
nil
re-matches
loop
, recur
Related projects:
Code sample from this episode:
(ns app.time
(:require
[clojure.java.io :as io]
[java-time :as jt]))
(def timestamp-re #"(\w+\s\w+\s\d+\s\d+)\s+(\d{2}:\d{2})-(\d{2}:\d{2})")
(defn localize [dt tm]
(jt/zoned-date-time dt tm (jt/zone-id "America/Los_Angeles")))
(defn parse-time [time-str]
(jt/local-time "HH:mm" time-str))
(defn parse-date [date-str]
(jt/local-date "EEE MMM dd yyyy" date-str))
(defn parse-for-minutes
[line]
(if-let [[whole dt start end] (re-matches timestamp-re line)]
(let [date (parse-date dt)
start (localize date (parse-time start))
end (localize date (parse-time end))]
(if (jt/before? start end)
(jt/time-between start end :minutes)
(jt/time-between start (jt/plus end (jt/days 1)) :minutes)))
0))
(defn total-time
[filename]
(with-open [rdr (io/reader filename)]
(loop [total 0
lines (line-seq rdr)]
(if lines
(recur (+ total (or (some-> (first lines) parse-for-minutes) 0)) (next lines))
total))))
Nate spends some time figuring out how to track his time.
Fri Feb 08 2019 11:30-13:45
line-seq
with clojure.java.io/reader
. That uses lazy I/O.slurp
in all the data at once and call split-lines
seq
abstraction whichever way you choose. It's Clojure's unifying abstraction.#"..."
form.re-matches
detect the match and extract the parts.when-let
to destructure and do something only if it matchestime-info
that does the match and returns either nil
or the string that matched.doseq
to just go through all the lines and test our matching.Clojure in this episode:
nil
seq
line-seq
clojure.java.io/reader
slurp
clojure.string/split-lines
#""
, re-matches
let
, when-let
doseq
Related projects:
Code sample from this episode:
(def timestamp-re #"(\w+)\s(\w+)\s(\d+)\s(\d+)\s+(\d{2}):(\d{2})-(\d{2}):(\d{2})")
(with-open [rdr (clojure.java.io/reader "time-log.txt")]
(doseq [line (line-seq rdr)]
(when-let [[whole d m dt yr hs ms he me] (re-matches timestamp-re line)]
(println whole))))
Christoph gets some work done by fiddling around.
comment
block right underneath a function you are working on, and invoke the function with test arguments.comment
blocks can be helpful in the future.comment
blocks.fiddle
namespace was born.dev
tree so it only gets loading in development.filter
, and map
component
nil
come from?"(def response (...))
to save a result, and write expressions using response
.Related episodes:
Clojure in this episode:
comment
def
, defn
->
, ->>
filter
, map
, reduce
select-keys
clojure.pprint/
pprint
print-table
Related projects:
Nate continues to explore the REPL by gluing it to his editor to see what happens.
/play?row=0&col=1
.log/info
calls in handler function for play endpoint: on-play
.defn
for on-play
app.main
namespace (where we put on-play
)defn
in the REPL, the new function replaces the old version immediately!def
, defn
, defmacro
, etc), not special syntax only the compiler understands.comment
blocks as a nifty way to provide executable examples within the code.Related episodes:
More reading:
Clojure in this episode:
ns
, in-ns
, use
*ns*
, ns-map
def
, defn
comment
let
println
clojure.tools.logging/debug
Related projects:
Christoph complicated development by misunderstanding the REPL.
(use 'the.namespace :reload)
in the REPLuse
statements!"clojure.tools.namespace
! Reloads everything and cleans out the cruft!(use '[clojure.tools.namespace.repl :only (refresh)])
, (refresh)
refresh
would purge itself!user
namespace!"use
the user
namespace.user
namespace and it will be there for me."refresh
is in the user
namespace, edit, save, (refresh)
comment
blocks in code and reading about vim-fireplace.Clojure in this episode:
use
clojure.tools.namespace.repl/refresh
clojure.pprint/pprint
Related projects:
Nate is worried about the hardcoded credentials in the code.
config.edn
reset
with component.repl.app.config
app.config
: from-env
and from-file
.merge
the config maps with that.from-env
handles defaults and we merge
the map from dev.edn
into that.environ
with Leiningen profiles. Still requires restarting the REPL.Clojure in this episode:
merge
try
, catch
slurp
clojure.edn/read-string
environ.core/env
component.repl/reset
Related projects:
Code sample from this episode:
(ns app.config
(:require
[clojure.edn :as edn]
[environ.core :refer [env]]))
(defn from-env []
{:twitter-key (or (env :twitter-key) "")
:twitter-secret (or (env :twitter-secret) "")
:initial-tweets (or (env :initial-tweets) 20))
(defn config []
(merge (from-env)
(edn/read-string (try (slurp "dev.edn") (catch Throwable e "{}")))))
Christoph can't stand the spaghetti mess in main. Time to refactor.
core.async
channel used between themLifecycle
interface allows a component to be started and stopped.
start
must return a reference to the "started" statestop
must return a reference to the "stopped" statenil
!Lifecycle
, you'll need to make your component a "record".new-system
which returns the component "system map"component.repl
to start and stop the whole system without restarting the REPL!promise
, deref
and a shutdown handler (see below).Clojure in this episode:
defrecord
promise
, deliver
deref
, @
component/
Lifecycle
system-map
using
start
, stop
component.repl/
go
stop
reset
Related projects:
Code sample from this episode:
(ns app.main
(:require
[clojure.core.async :as async]
[com.stuartsierra.component :as component]
[app.component
[fetcher :as fetcher]
[web :as web]])
(:gen-class))
(defn new-system
[]
(component/system-map
:new-search-chan (async/chan)
:web (component/using (web/new-component) [:new-search-chan])
:fetcher (component/using (fetcher/new-component) [:new-search-chan])
(defn -main
[& args]
(let [system (component/start (new-system))
lock (promise)
stop (fn []
(component/stop system)
(deliver lock :release))]
(.addShutdownHook (Runtime/getRuntime) (Thread. stop))
@lock
(System/exit 0)))
Nate can't decide what to watch on Twitter, and the app restarts are killing him.
#clojurescript
too."#clojure OR #clojurescript"
?curl
UI (like Ep 004)!curl
?
curl
itself)http://localhost:8000/search?The+search+string
main
function must:
/search
route handler function needs a way to send the new query string to the polling loop.atom
and give the handler and the polling loop a reference to it.core.async
channel.peek
function.)alt!!
to simultaneously listen to the "new search" channel and a timeout
channel.timeout
channel? A channel that closes after n
msecs have passed.curl
. Just type in the URL for the backend on your phone.core.async
lets threads coordinate without having to know about each other!Clojure in this episode:
atom
loop
, recur
core.async/
chan
<!
, <<!
, >!
, >>!
, put!
alt!
, alts!
timeout
Related projects:
Christoph tries to figure out why Twitter stopped talking about Clojure.
search
function return [updated-handle, result]
.search
can catch an auth exception, retry, and return a new auth handle.search
retrying, the fetcher can do it! Then it works for all kinds of requests.fetch-with-retry
function that uses fetch
.fetch-with-retry-forever
.assoc
on."Clojure in this episode:
loop
, recur
try
, catch
atom
assoc
Nate just wants to print the tweets, but the input wants to take over.
pprint
as our output mechanism."println
, so you can unit test output.(defn filter-new [cache tweets] ...)
filter-new
function would return [cache new-tweets]
. Cache could have changed.Parts of a data model
Parts of a wrapper
Clojure in this episode:
println
get-in
->
loop
, recur
, reduce
schema/defschema
, schema/Int
, schema/String
, ...Related projects:
Christoph tries to get a handle on his #clojure tweet-stream habit.
#clojure
tweet stream and see it print out in the terminal/search/tweets.json
endpointloop
and recur
for our main loop401 Access Denied
auth
function to call the OAuth endpoint and get an auth tokenauth
return a "handle" with the auth token. Other wrapper functions will need handle
.handle
around. Put that in the app state too.fetch
function that does the I/O work.search
function that takes handle
and query
search
into a "request description" and have fetch
operate on that.fetch
function that follows the "orders" of the "request description"Clojure in this episode:
loop
, recur
Thread.sleep
get
try
, catch
->
Related projects:
Nate tries to figure out who actually won this never-ending game of tic-tac-toe.
nil
has won...which is nobody."winner
function--should read like a process description of steps.row-winner
and column-winner
and use those.[
player case index]
{ [:x :row 0] 1, [:y :row 0] 0, [:x :diag] 2, ...}
app.game.tracker
[:x 1 0]
as the play(update game-state :tracking tracker/update [:x 1 0])
diag?
and rdiag?
to use in cond->
(see code below)Clojure in this episode:
nil
punning streak: 3 episodesget-in
update
, update-in
or
short circuits, =
does not->
, cond->
, some->
frequencies
Code sample from this episode:
(ns app.game.tracker)
(defn update
[tracking [player row column]]
(cond-> tracking
true (record row column player)
(diag? row column) (record-diag player)
(rdiag? row column) (record-rdiag player)))
Christoph tries to make tic-tac-toe work over the Internet and discovers the power of the atom.
curl
."curl
will print the text response. We have a terminal UI!/new
, /show
, /play?row=0&col=1
1337
to hide it from the Internet.!
("bang") in swap!
and reset!
indicates you're causing a side effect.swap!
should be pure. Don't throw exceptions!:success
,:invalid-coordinate
, etc.Clojure in this episode:
Nate tries to turn the tic-tac-toe "game engine" into a real application he can play with a friend.
loop
using recur
nil
. You just have one. It's the nuh-uh."Clojure in this episode:
read-line
string/split
swap!
and reset!
loop
and recur
let
vs loop
nil
punningChristoph tries to make tic-tac-toe and gets stuck on immutability.
let
block, and one line of actual function."reductions
wants to blow your mindClojure in this episode:
assoc
assoc-in
->
reduce
reductions
Nate and Christoph try to figure out how to make a podcast.
(with-meta podcast ...)
Clojure in this episode:
nil
En liten tjänst av I'm With Friends. Finns även på engelska.