Fork me on GitHub
#clojure-europe
<
2022-09-17
>
reefersleep08:09:13

What you were doing is called “good night”

reefersleep08:09:39

UGT or not! 😛

reefersleep08:09:51

nah I’m kidding. Good morning!

lemontea08:09:45

good morning! (and nice weekend!)

lemontea08:09:15

incidentally, I actually have an email account beginning with “good night” made in computer class in elementary school

lemontea08:09:23

when I can’t even spell properly lol

reefersleep09:09:35

I’m conflicted. Clojure is amazing for data transformation (`assoc`, merge, all that stuff), which means that developers are incited to do it wherever they need to. In my work application, I find it a bit confusing, though. Our central “business objects” have many variations because they appear in many contexts where it would be nice if they were tweaked just a little bit. Sometimes the tweaks accrete along the data path. So, instead of just looking at the data and realising, “ah, this is a foo coming from bar”, you’re like “this looks a bit like a foo… but also a bit like a baz… I thought this came from bar, but now I’m unsure.” And you have to dig through the code to figure out what the intended data structure is, because your concrete data example instance might be incomplete. I’m inching towards a notion that central objects should have a canonical representation which is what is transported around, and only be transformed exactly when needed, not before, to avoid confusing accretions.

reefersleep09:09:38

What do you think?

borkdude10:09:47

@reefersleep so you want a "closed spec" basically for business things?

reefersleep10:09:51

Basically, yeah! At least in spirit. I’ve considered the convention of speccing every function that takes or returns those central objects, as well; as long as it can be done in a non-obtrusive way, it might be desirable. Speccing lots of functions with lots of specs makes for lots of extra code, so that may not be that valuable in terms of readability. But speccing some functions over and over with the same business objects gives you familiarity so that you can glance over it.

reefersleep10:09:03

And I dig the idea of closed specs. I get that open specs are in the spirit of Clojure, but it leaves open a massive door of doubt. “If I put in extra keys, will that cause a side effect? Should I avoid doing that?” or, conversely; “Someone put in extra keys, and it’s causing a side effect. Is this intended, and therefore missing from the spec?” or: “Someone put in extra keys, but they have no effect. Should they be having an effect? Regardless, should they be added to the spec?” You can have conventions that make the answers to these questions more or less obvious, of course.

borkdude10:09:48

Why would an extra key cause a side effect?

reefersleep10:09:02

Might be far fetched, but I could see it happening. • You’re sending off the data structure to an API that you don’t own • Some code goes through each key-val to do something • other 🙂

borkdude10:09:55

Those are typically called "code at the edge of your system" and this is where you can do select-keys or whatever to make sure no garbage is sent out

reefersleep10:09:07

You can do the same inside each function that takes a map. “Ah, this map can potentially hold the whole world, but I’m going to select these 3 keys that I’m interested in”. I kind of like delegating that responsibility to the caller; clean up before calling.

reefersleep10:09:20

Don’t leave in-between mess. Clean boundaries.

borkdude10:09:05

I think as long as you're "in the system" you shouldn't care about extra keys, only if you're dealing with "no-clojure-philosophy" systems like foreign APIs or databases

slipset10:09:20

Agree with @U04V15CAJ here. Within the system, you should be ok with extra keys. When shipping stuff out, there might be keys that you don’t want to ship, like tokens/passwords/etc, so it might be nice doing select-keys or set/project if you have a list of things. Also, it might make sense to architect for avoiding forgetting to do select-keys, so that you split domain objects which has sensistive info on it, ie split a user into a user which is benign to ship off and subject which could contain passwords/tokens etc.

Ben Sless10:09:31

How about having an or schema to parse the data and figure out which case it really is?

Gabriel Kovacs11:09:52

Could you provide some code to exemplify the issue? As far as I understood it, you are concerned about a possible "mismatch" between the functions and the provided input data. Whenever I see "spec" I think of an xsd schema. I am new to Clojure so please excuse my ignorance 🙂 .

reefersleep13:09:13

“Not caring” is fine when you’re writing new code. I’ll take a map in my function, destructure the keys I’m interested in, and not care about any remaining keys. Callers can include the world in their map argument; I’m not worried about it. It’s different when you’re maintaining a large code base. I want to refactor a data structure foo to be slightly different for whatever reason. To do this safely, I should check and possibly refactor all usages of foo. How will I know when I’ve found all code paths that make some use of foo? In Clojure, this can be hard, as foo is not always referenced by name (and therefore greppable). There are many ways for code to treat data generically and anonymously. Allowing everyone to include the world in every map that’s sent to every function increases the search space. foo can appear in every call to every such function. Potentially, it can appear many times in the “world” maps, nested into other data. If I can limit the input and output of functions, I limit my search space greatly. Like I said, there are other conventions that can limit your search space, like good code organization (“only these namespaces are concerned with foo“). And I don’t feel certain that speccing every function, or maybe even most functions, is a good solution. But I’d love to see how it feels.

borkdude13:09:51

if you use something like clojure-lsp, it's easy to find all usages of foo

reefersleep13:09:58

And perhaps it’s not that big of a problem. I just work with a large code base with a lot of other people, where I’m often annoyed that I can’t change things with more certainty or quicker. Usually, there’s lots of digging involved, and, sometimes, even after being very thorough, I have to shrug and go “Best I could do, let’s see if any defects roll in”.

reefersleep13:09:11

@U04V15CAJ uhm… does it compile the code and exhaust all code paths? Or what do you mean?

reefersleep13:09:37

Wait, not compile the code. Analyze it somehow.

borkdude13:09:14

you weren't referring to foo as a function name?

reefersleep13:09:22

No, a data structure 🙂

borkdude13:09:07

I think this discussion has two answers: 1. Use a validation / coercion library at the critical edges (spec, schema, malli) 2. Switch to a statically typed language

reefersleep13:09:05

Yeah of course 😄

reefersleep14:09:30

I find myself weighing the pros and cons of static vs dynamic a lot when pondering this stuff. The static languages I’ve experienced were also annoying (mostly Java).

borkdude14:09:45

TypeScript is all the rage man ;)

reefersleep14:09:58

yeah I know 🙂

reefersleep14:09:17

I’ve been meaning to try it…

borkdude14:09:29

I've been in this phase myself. I explored Haskell for a year or so. But I came back to Clojure every time

borkdude14:09:36

Perhaps Typed Clojure could also help you

reefersleep14:09:35

I’m asking the impossible, really. I want dynamism, but with the certainty of static typing. opt-in like in Typescript is perhaps the best bet.

reefersleep14:09:56

Now to get the same in Clojure 🙂

reefersleep14:09:38

Aspects of the Haskell typing intrigue me, but not enough.

borkdude14:09:36

I just listened to The REPL about #jank - Jeaye want to create something like typescript (gradual typing) in his clojure dialect based on a malli-like syntax

Gabriel Kovacs14:09:42

Wouldn't Python be a better candidate then Typescript? Isn't refactoring without tests an issue in any code base? Or would tests not be able to catch breaking data structure changes?

slipset16:09:55

Here’s a fun example, which might not be related, but still:

slipset16:09:41

Deep down in our database code I wrote in a function:

(if (sequential? upsert?)
                    upsert?
                    [:_id]))]
And I”m getting upsert? from a map that’s being passed around.

slipset16:09:15

Now, who gives us a sequential upsert? I have no clue, I have to search for it, manually.

slipset16:09:35

And the place looks like:

(assoc (mongo/requested-version-ctx ctx requested-versions)
                                                 :pg-opts {:upsert? [:_id postgres-rebaser/branch-column]})

slipset16:09:48

So what I did just now, was to add this fn deep down in our database code:

(defn upsert-on-conflict [ctx cols]
  (update ctx :pg-opts merge {:upsert? cols}))
And the call site then becomes
(postgres/upsert-on-conflict (mongo/requested-version-ctx ctx requested-versions) [:_id postgres-rebaser/branch-column])
And boom, a bit more language to the system, and the usage is easier to track, because I can do find references on upsert-on-conflict

slipset16:09:36

I believe the effects of doing stuff like this are very profound.

borkdude16:09:15

@U04V5VAUN I think you gave a talk about this kind of usage at ClojureD in 2018 or so right

slipset16:09:02

I gave a talk on something around this subject, but I don’t know if this concrete “technique” was in it.

slipset16:09:34

A talk that I would really like to give, but I haven’t figured out yet is “How to write your programs so types become irrelevant”

borkdude16:09:53

well, the title would be provoking enough :)

slipset16:09:25

Where “types” are to be understood as static type types. Because we all use (and probs think in) types even though we code in a dynamic language.

slipset16:09:53

The only thing I’ve managed to come up with so far is that we should know very little about data that travels far.

slipset16:09:31

Like, within a ns, say a ns which deals with users, you can have a lot of knowledge about what a user is. And since it’s all very local, you tend to manage to remember what a user is. But, if you send that user far away (via fn calls) down the stack, the recipients should probs just care that it has an id and a name, because, that’s about what you can remember.

borkdude16:09:49

takes long drag on joint > The only thing I’ve managed to come up with so far is that we should know very little about data that travels far

😂 3
reefersleep16:09:29

That sounds reasonable. Within some context, you have lots of details, but you should avoid shovelling all of those details onto other contexts. Encapsulation, in spirit?

borkdude16:09:50

I think one of the points that slipset made in his talk was that you should access map things using functions

reefersleep16:09:51

@U04V5VAUN I've been thinking about how to use var references as well. I guess it's really a result of the tools supporting "Find usage" so well 🙂

borkdude16:09:08

and if you do that, then you can say: this function should never be used from this and that namespace

borkdude16:09:14

which would be your encapsulation

reefersleep16:09:15

It sounds a bit like the "poor man's object" koan

slipset16:09:33

Yes. And high cohesion. If your whole code base have to know every little detail about what a user is, you're in for a wold of pain. Build some langage around the user.

slipset16:09:48

@U04V15CAJ your comment about the joint. Priceless! It made my day :)

reefersleep16:09:15

The venerable master Qc Na was walking with his student, Anton. Hoping to prompt the master into a discussion, Anton said "Master, I have heard that objects are a very good thing - is this true?" Qc Na looked pityingly at his student and replied, "Foolish pupil - objects are merely a poor man's closures."

    Chastised, Anton took his leave from his master and returned to his cell, intent on studying closures. He carefully read the entire "Lambda: The Ultimate..." series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress.

    On his next walk with Qc Na, Anton attempted to impress his master by saying "Master, I have diligently studied the matter, and now understand that objects are truly a poor man's closures." Qc Na responded by hitting Anton with his stick, saying "When will you learn? Closures are a poor man's object." At that moment, Anton became enlightened.

    - Anton van Straaten

1
reefersleep16:09:55

@U02AMR8032L dno about Python vs Typescript; I know too little about both. You're right (imo) that static types and unit tests are complementary, not necessarily substitutes, though you could probably test your way out of what a type system provides (apart from editor-time verification), by being very exhaustive 😄

reefersleep16:09:32

@U04V5VAUN do you do anything in particular to signal the coupling of certain ns'es and datastructures?

slipset16:09:29

No. Just heuristics ie reading/studying the code.

reefersleep16:09:38

idk if there's something clever you could do, like using a constructor fn to add metadata that links the fn to the datastructure

reefersleep16:09:03

I'm all for conventions 🙂

borkdude16:09:00

if you create "language" around your data, some behaviors will automatically look weird to you like: huh, why does utils.logging depend on user.manager

slipset16:09:39

I've been thinking a bit about constructors lately. Like in your user ns, you could have various fns to construct valid users, rather than just typing out the maps all over the code

slipset16:09:46

I know @U050P0ACR has been advocating getters/setters rather than just using eg :name. I find that a bit too much :)

borkdude16:09:57

@U04V5VAUN I recommend watching your own ClojureD talk, I think you've been thinking about that since then already ;)

Gabriel Kovacs17:09:58

I could not find until now something that relates to patterns or principles on how to structure code in an FP context. Do you have any recommendations? Just started watching the video :)

borkdude17:09:48

This is the kind of thing we just spoke about:

slipset17:09:18

Wow, I’m smart!

😄 1
slipset17:09:25

And I agree with myself 🙂

slipset17:09:34

I’m internally consistent 🙂

Gabriel Kovacs17:09:02

@U04V15CAJ that is why I asked 🙂 I was thinking about more concise literature analogous to the Gang of Four book.

reefersleep17:09:07

I'm not aware of anything like that, but I would love to hear about it. There's this funny blog post thingy where someone recreates the patterns of Gang of Four with Clojure - it's mostly just obvious function usage.

reefersleep17:09:21

Functional Programming is somewhat ill defined, I think? Some people seem to strongly associate Haskell style type systems with functional programming. I mostly just think "a language that focuses on/enables usage of higher order functions", e.g. via map, filter and so on. Maybe there's a more complete definition.

Gabriel Kovacs17:09:55

I remember seeing this one a long time ago 🙂. every pattern in OO is a function in FP . https://youtu.be/srQt1NAHYC0?t=228

👌 1
genRaiy11:09:24

Good wet weekend morning

❤️ 2
lread15:09:06

good morning, eh

borkdude15:09:13

eeeeeh, good morning @lee

👋 1