Fork me on GitHub
#ring
<
2016-04-13
>
zane16:04:50

@weavejester: I'm curious. Do you prefer to use a closure for that because the lifetime of the connection spans multiple requests? I'm trying to understand when to use that method vs attaching values to the request itself.

weavejester16:04:33

@zane it has a few advantages

weavejester16:04:56

The first is that it means you’re only destructuring or setting things up once.

weavejester16:04:32

So (fn [config] (let [x (expensive-operation config)] (fn [request] …)))

weavejester16:04:12

The second advantage is that it’s less complex. You’re not tying the configuration to your request map.

weavejester16:04:59

Ideally you should only be passing around the information you need.

zane16:04:32

I think that makes sense.

weavejester16:04:41

Finally, closures fit in well with architecture like Stuart’s component library

zane16:04:12

How so? (We're using component already.)

zane16:04:20

I mean, the handler is going to have to get a database value at some point. And if I'm understanding Ring correctly the way to do that is through the request map.

weavejester16:04:12

The request map is just for requests. I don’t think you need to tie your database to your request map.

weavejester16:04:43

There’s no inherent connection between the information in HTTP request, and a database connection

zane16:04:00

But the handler needs both in order to do its work.

zane16:04:11

And my understanding is that Ring handlers are single-argument functions?

weavejester16:04:15

The request map is really designed for data from the HTTP request, or data derived from the HTTP request, like sessions, cookies, etc.

zane16:04:24

I feel like I'm missing something here. Maybe I should take a closer look at the doc.

weavejester16:04:25

Ring handlers are single-argument functions.

weavejester16:04:31

But you can use a closure

weavejester16:04:44

(fn [config] (fn [request] response))

zane16:04:45

Okay, so generate different handlers for each request with a database value in scope.

zane16:04:00

That's more idiomatic?

weavejester16:04:03

In other words you write a function that takes some configuration, and returns a handler.

zane16:04:12

Right, right.

weavejester16:04:37

I think so. It means that the configuration (e.g. database connections, etc.) is separate from your request map.

weavejester16:04:48

It’s how I do it in Duct.

zane16:04:18

Oh, I forgot about Duct!

zane16:04:28

Thank you for reminding me. I'll take a look at how it's done there.

weavejester16:04:15

I also did a presentation that covers this topic: https://skillsmatter.com/skillscasts/7229-duct-covered

zane16:04:27

Awesome!

weavejester16:04:29

I think you need to sign up to see the video, but it’s free.

zane16:04:34

Just what I was looking for.

zane16:04:51

I'm a bit wary of using components for so many things.

weavejester16:04:19

I don’t think components should be overused… in fact I’m more conservative than Stuart on that point.

zane16:04:30

Right. Dependency injection is kind of insidious in that it kind of wants to take over your whole program.

weavejester16:04:53

My rule of thumb is that components are for things that have a start and a stop.

weavejester16:04:09

So anything that needs to be initialised or cleaned up

weavejester16:04:22

Sometimes cleanup can be left to the GC

weavejester16:04:35

Anything else shouldn’t be a component IMO.

zane16:04:01

That's what I thought too, but I see you're using a component for the Ring handler in Duct, so I was confused.

weavejester16:04:37

That’s because they have a setup part to them.

weavejester16:04:56

Endpoints are essentially constructors for handlers.

zane17:04:03

Ah, I see.

weavejester17:04:21

They also have a cleanup - deleting the handler - but this is handled by the GC

zane17:04:36

My initial thought was that things that only had setup (and no teardown) would be simpler to represent just using functions, closures, records, etc.

weavejester17:04:25

Well, endpoints are functions, but they’re promoted into components in order to integrate with the system.

weavejester17:04:16

I think you want to minimise how many components you have, but if you have something with a setup, even if the cleanup is just to let the GC handle it, then often it makes sense to either wrap it in a component, or hang it off a component somehow.

zane17:04:46

Yeah, the "in order to integrate with the system" part is what I meant when I said that dependency injection wants to eat the world.

weavejester17:04:29

Often you can draw a hard line between the system and everything else.

zane17:04:45

It's like the IO monad in Haskell. Once one part of your program becomes componentized there's a strong pull to componentize everything else so it all plays well together.

weavejester17:04:46

For instance, an endpoint component goes down into an endpoint function.

weavejester17:04:26

I think it is a little like the IO monad, but the same principle applies; you should aim to minimise things the IO monad touches.

zane17:04:10

I agree!

weavejester17:04:25

I tend to find there’s not much initial setup I need to do, outside of IO, threads and things that seem a natural fit for components.

zane17:04:38

So, yeah. It's interesting to me that you drew the line after the handler / middleware stuff. I was going to draw it before.

weavejester17:04:56

I’d draw the line at the endpoints.

weavejester17:04:09

So the endpoints represent moving from the system into a more functional realm.

zane17:04:15

I see, I see.

weavejester17:04:25

I also make use of what I’ve taken to calling “boundary protocols"

weavejester17:04:37

So if I want to interact with a component, I’ll do so through a protocol

weavejester17:04:00

That gives me a clean border between the messy outside world of components and IO

weavejester17:04:08

And my cleaner functional world inside.

weavejester17:04:24

It’s still better to have pure functions

weavejester17:04:40

But accessing a component through a protocol is better than accessing it directly

weavejester17:04:48

Not least because that makes it much easier to test.

zane17:04:31

I like that pattern.

zane17:04:46

When you say "through a protocol" I'm assuming you don't mean through the component protocol itself. You mean through a more abstract protocol that hides the underlying componentness of the implementation?

weavejester17:04:10

So if I have a database component

weavejester17:04:25

Then I would wrap it in a protocol that abstracts the access

weavejester17:04:50

Allowing me to substitute a mock using something like Shrubbery https://github.com/bguthrie/shrubbery

weavejester17:04:08

The other advantage of it is that it forces me to define a clear boundary.

weavejester17:04:26

So I know that the database component will only be accessed in these ways.

weavejester17:04:54

It’s not as good as a pure function, but it’s better than nothing.

weavejester17:04:00

I tend to view complexity as something that needs to first be destroyed, and if it can’t be destroyed, it should be contained. So immutable data structures and pure functions first, and then some form of encapsulation if you absolutely have to access some form of I/O or side-effects.

zane17:04:31

Yeah, I mean. I like to think that one of the nice things about the functional approach in general and Clojure in particular is that you don't generally need things like stubs and mocks.

weavejester17:04:03

Right. Though I think mocks and stubs still have their place, they’re just much less common.

weavejester17:04:53

One of the problems with OOP is that it overuses niche tools, like inheritance and encapsulation. They have their place, but most of the time they’re not the right tool.

zane17:04:35

Well said.

zane17:04:59

I'm curious where you feel like inheritance is best used in Clojure programs!

weavejester17:04:00

I’ve had opportunity to use it once simple_smile

weavejester17:04:34

I’m currently using it in a game where entities have common behaviour

weavejester17:04:28

But it’s pretty niche. Maybe inheritance has a purpose in validations? Not sure.

zane17:04:07

Hmm. That felt better than pulling the shared behavior out into a function and just passing it to the other functions that needed it?

weavejester17:04:01

Right, that’s the way I’d normally do it… but the way I have it set up happens to favour inheritance.

weavejester17:04:33

To give some more background, I have a Datomic-like database, Ittyon, that works based around behaviour of aspects keywords.

weavejester17:04:04

So an aspect like, I don’t know, :player/health, has certain behaviours associated with it.

weavejester17:04:25

That naturally makes one think about multimethods, right?

weavejester17:04:03

But with Intentions and inheritance, combining behaviour (which is common) is easier than trying to call multimethods with different keys.

weavejester17:04:08

Because aspects have a lot of behaviour in common, the inheritance model happens to make a lot of sense.

weavejester17:04:50

Especially if you have a way of combining inherited behaviour automatically.

weavejester17:04:38

Not sure if that makes sense? I’d explain it more but that would mean explaining more about Ittyon.

zane17:04:35

I think I kind of get it.

weavejester17:04:50

It’s a really niche use, though simple_smile

weavejester17:04:01

And I’ve never had opportunity to use it before.

weavejester17:04:21

There are a few things in Clojure that seem pretty niche, until you need to use them.

weavejester17:04:46

I don’t think I’ve used refs or agents in a real program yet simple_smile

zane17:04:05

Me either.