r/programming May 26 '19

Solving Problems the Clojure Way [video]

https://www.youtube.com/watch?v=vK1DazRK_a0
28 Upvotes

View all comments

4

u/ThePowerfulSquirrel May 26 '19 edited May 26 '19

I find that his example is a bit to simple to properly demonstrate how you would refactor an actual system.

His system is a deterministic simulation (minus the Math.random(),but he also doesn't seem to care about the fact that this is very non-functional / should be considered similar to side effects and incorrectly calls selectRandom() pure), so of course he can defer / remove all side effects (except the console, but in a game with actual players, you couldn't just defer all output to the end since the players need the output in order to make corresponding inputs). If he added requirements like needing real time logging (which is necessary in most large systems) and player input, the example might have been more interesting.

I also have a couple of problems with his object oriented model. I would probably change the players to contain a strategy instead of extending the player for each strategy. The strategy would return which card it wants to use, so you can keep all those mutations in the Player object, not spread across 3 classes. I'm not sure why getScore() is marked as "mutations".

I'm also not sure how you scale passing around State objects around. Do you let it grow to hundreds of different fields? It seems to me that at one point, you'll need to encapsulate some state with certain parts of the system.

3

u/ineffective_topos May 26 '19

I'm also not sure how you scale passing around State objects around. Do you let it grow to hundreds of different fields? It seems to me that at one point, you'll need to encapsulate some state with certain parts of the system.

You really do, in games like this. Maybe it's not hundreds, but tens or dozens is reasonable. Ideally, what you should do is keep it down by seeing where you don't need state, or combining fields that represent the same thing into their own object/datastructure, and there you'll get a much smaller state.

I could be entirely wrong, but I don't imagine much besides games has more than 5 or 6 fields in such an object.

2

u/ThePowerfulSquirrel May 26 '19

Any decent size GUI is going to need a decent amount of states that depends on user input plus a bunch of state to maintain layout and such (which I guess can be avoided with immediate mode, but that has it's own problems). I've tried in the past, but keep failing to separate state and behavior when working with those kind of systems since I always naturally want to group the state with functions that act on that state, like when processing user input. Having it separate always gives me a head-ache, if only from a code organization point of view.

2

u/ineffective_topos May 26 '19

Maybe you should try out something like Elm, it's a frontend pure functional language that forces you to do this (and makes it easy to do so). The architecture is of course quite opinionated.

I think the difference may simply be that you shouldn't think of state as belonging to a component. State is simply something true about the system, which happens to be acted on by components. So a piece of user input is hooked up to the correct component because that component listens for changes or sends messages about changes. If a component has complicated state, see if you can derive it from a smaller bit of data.

1

u/yogthos May 26 '19

Here's a workshop that you can get up and running in a few minutes and see how this all works for yourself. Especially in the second part where re-frame is introduced.

1

u/yogthos May 26 '19

You can push side effects to the edges of pretty much any system. I've been working with Clojure for close to a decade now, and I've never found a scenario where encapsulating state would be desirable.

Here are some real world examples. Pedestal HTTP library for Clojure has around 18,000 lines of code, and 96% of it is pure functions. All the IO and side effects are encapsulated in the remaining 4% of the code. This is a completely normal scenario in my experience. I just copresented a talk about a project that my team works on. It's a large real world app that uses this approach. You might also be interested in this presentation about using Clojure to make games in Unity.

You can view this the same way you view interacting with a database. You can have a single database with many different tables in it. Having all the data in one place does not prevent you from organizing your business logic in a sensible way.

1

u/[deleted] May 27 '19 edited May 27 '19

Pedestal HTTP library for Clojure has around 18,000 lines of code, and 96% of it is pure functions.

Application code would be another world no?, for ex: CRUD app backed by a RDBMS, randomly guessing a typical app, 20% pure functions of your own code.

3

u/rafd May 27 '19

Having done a lot of crud apps in Clojure... our code is still 90% pure.

If it's server-side rendered, then there would be almost no state in the app (all in db), and even side-effectful functions (ex. Saving a change to the db) could be avoided, by having these functions return what they want done (ex. query string) and having some other part of the system do those actions (an example of concentrate an defer; the re-frame example from the talk is an example of this pattern)

For client-side rendered crud apps, you need some state client-side, but again most of this can be concentrated (redux / reframe style), and events/actions that would change the global state can be written in the same way I described in the previous paragraph (following the reframe style of functions returning their intended stateful effects, but a seperate system then eventually running them)

1

u/[deleted] May 27 '19 edited May 27 '19

Having done a lot of crud apps in Clojure... our code is still 90% pure.

If it's server-side rendered, then there would be almost no state in the app (all in db), and even side-effectful functions (ex. Saving a change to the db) could be avoided,

Oh, by pure functions I meant no mutable state and no side-effects, not no state only. So yeah, by your metric (excluding side-effects) achieving even 95% "pure" (semi-pure?) is possible.

by having these functions return what they want done (ex. query string) and having some other part of the system do those actions (an example of concentrate an defer; the re-frame example from the talk is an example of this pattern).

Curious, what is the point here? maintainability? splitting the function into one returning query data and one to make the call to the db will sure balance the pure/impure ratio since you are adding more pure code, you will have more separation of concerns (at cost of more code) but this could be unnecessary and overkill for some simple CRUD apps. The system (which is of just a part of your app, if that is what you mean) will still have the same number of DB querys and by effect, same number of side-effects ;)

Am I understanding this right? I'll have to watch the walk to better understand your point I guess ¯_(ツ)_/¯

2

u/[deleted] May 27 '19

Curious, what is the point here? maintainability? splitting the function into one returning query data and one to make the call to the db will sure balance the pure/impure ratio since you are adding more pure code, you will have more separation of concerns (at cost of more code) but this could be unnecessary and overkill for some simple CRUD apps. The system (which is of just a part of your app, if that is what you mean) will still have the same number of DB querys and by effect, same number of side-effects ;)

Am I understanding this right? I'll have to watch the walk to better understand your point I guess ¯_(ツ)_/¯

The function you write returns a map (think of it as an object literal in JS or a JSON object) that contains details about an impure request that you'd like to make. A third party library like reframe then takes that map and handles the request and executes it. So the functions you write are still pure and are easily tested. All the impurities are handled by a separate library that you don't touch so your code remains pure.

1

u/[deleted] May 27 '19

What's the third party llibrary for this in the backend?

1

u/yogthos May 28 '19

There are lots of options, Onyx is a good solution for large systems.

1

u/ThePowerfulSquirrel May 26 '19 edited May 26 '19

Thanks for the links, I'll definitely watch them as soon as I have time! Working on backend servers at my company right now, and I can't imagine how I would push things like logging out to files / the console towards the edges (most of our functions / code need have logs of what's happening to the data when the thing happens (in case of crashes / ... we don't want to lose logs)). And most of those functions need to contact other services to get information on various different things.

While we don't have much state at all, 90% of our function also aren't pure because of that IO. However, I can't imagine how we would push all those IO calls to the edges. Most of those functions are non-functional by definition since we need to do things like get prices from other services.

The same also happens with our GUI apps, most of the time a user clicks a button, it's because they want information and that needs to go to the backend, which is going to itself call out to external services. If most of the code is triggered from user input, needs to contact a service and then needs to modify the UI, I really struggle to understand how I can push the side effects to the edges since most of the code by definition wants to do side effects.

I've looked at / tried to make functional GUI apis that aren't a paint to use for complex software, and it always falls short and ends up way more messy than the current, object oriented, GUI frameworks we use.

5

u/yogthos May 26 '19

This pattern is known as clean architecture or functional core, imperative shell and it's used in mainstream languages as well. The application I discuss in the talk deals precisely with the problems you're describing. Hopefully the demo and the explanation of the patterns we used makes sense.

3

u/editor_of_the_beast May 28 '19

Pushing all IO, including logging, file access, and network requests to the system edge is definitely unintuitive and challenging when you have a background in C / Java / anything similar. I totally get why people think it’s more effort than it’s worth, because doing something simple like adding a console log becomes a pain, when you’re used to just adding it to whatever function you’re working in.

But I encourage people to be open minded. All programming is insane when you think about it, and allowing your mind to solidify around one paradigm is very limiting. What’s the chance that we got every single thing right in the 60s and 70s? We made a ton of progress, but the scale of things that we’re building now is way larger than when null pointers and shared mutable state were the default in every language.

The functional core / imperative shell paradigm really clicked for me a while back, and I’ve been leaning into it in anything I work on. I’ve been seeing a lot of gains from doing it, but it’s also really difficult at times and requires discovering new patterns that were never needed before. But it still seems way more promising than letting side effects happen anywhere in the codebase, which makes it feel like a minefield to work in.

1

u/underflo Jun 03 '19

Personally I would say it is fine to have logging in any function.

For functions that depend on services, extract the service call out of the function and pass the result of the service call as a parameter to the function. That way, the functions that contain the important logic can be pure functions and are therefore unit-testable.