Fork me on GitHub
#beginners
<
2022-06-12
>
Godwin Nicholas02:06:55

Please is Om or Om next still use?.

seancorfield03:06:43

Just in case others go to respond, this was cross-posted into #clojurescript and answered there.

Godwin Nicholas09:06:55

Okay thank you sir

Eric14:06:41

Unfortunately, I’m still struggling with program structure in one particular sense. From imperative programming, (even if this is bad style for imperative as well) I am used to writing a main function that serially executes a whole bunch of functions, and gradually builds up state variables that can be used as I go along. So in Clojure, I’m not sure how to handle variables that need to be 1) calculated at runtime (so they can’t be in a global def) and 2) used by many functions in main. Do I need to have highly nested functions so that everything that uses a runtime value is inside a function that encapsulates it (with a let )? One helpful person suggested a map of global state that I can assoc new things into. That sounded good, but again it means I have to nest every function that uses those variables, since I have to return a new copy of the hash-map. Is that a “good thing” that I should learn to accept or am I doing it wrong?

Eric15:06:44

As an example, I have in -main a function that reads a CSV, cleans the data, transforms it a bit, then extracts some values (like min, max, and other stats) that need to be used by many more functions that will further reshape the data and generate graphics and reports about the data.

aratare15:06:27

If I understand correctly, you can put the hashmap into an atom. This way you can access and update it anywhere as you go along.

🙏 2
jumar15:06:18

It could be helpful to publish a code sample on GitHub so we can see what exactly you mean and if there's a better way

👆 1
Eric15:06:14

Ok, it’s not pretty (which is why I’m trying to refactor it into -main instead of doing everything at the top level), but here you go. https://github.com/ericdlaspe/elevation-plot/blob/main/src/elevation_plot/core.clj

Eric15:06:30

I appreciate you taking a look!

Eric15:06:23

The “main” part of the code starts down in the bottom 1/4 where most things are “def” instead of “defn”

Eric15:06:11

At about “;;; Import CSV data”

Eric15:06:17

@U013F1Q1R7G The description of atoms does sound like what I’m after. I was trying not to go that direction because I thought in Clojure mutating state was something to try to avoid. Is it perfectly OK as long as I’m only adding to it and not changing existing values?

sansarip15:06:30

Hmm, I think you have the right idea with the main function, but is there a reason you can’t put the values of those defs in a let binding in your main function? I’m not sure that I quite see the issue with that :thinking_face: e.g.

(defn main []
  (let [imported-meters-data (->>  (load-csv "/Users/easy/Downloads/20220519082008-04213-data.csv")
                   data
                   clean-csv-data
                   drop-header-row
                   csv-strings->numbersproject-WGS84->meters)  
        proportion (get-data-proportion imported-meters-data)
        sheight (m/round (* swidth proportion))
        ...etc]
(q/sketch
  :size [swidth sheight]
  ;; :renderer :p3d
  :settings settings
  :setup setup
  :draw draw
  :features [:keep-on-top])))
I tend to not need atoms unless I’m doing something asynchronous that needs to coordinate state, but by all means use them if they answer the mail!

Eric15:06:06

@U022T96EFV3 I guess I imagined with that approach that if I ended up adding much more code, my main would be one giant let vector that would get hard to read… or at least not look like “idiomatic” Clojure. I will try it and fall back to atoms if things get too hairy. Thank for taking the time to review my code and advise!

👍 1
sansarip16:06:27

Understandable! You can always hide hard-to-read parts of the let into nicely named functions as well! In fact, you’ve already done that with get-data-proportion. As for it not being idiomatic, I think it actually is quite idiomatic to write clojure code this way! 😄

🙏 1
pithyless16:06:18

@U032LAD66SF I suggest you avoid the atom in this scenario - it will get you into more trouble and lead to less idiomatic code. You want to reach for atom and other concurrency tools when you are dealing with concurrent operations (e.g. if the parsing, interpolation, and sketching could all be happening concurrently on different threads and they need to share data). In the posted example, you're just trying to wrangle a function that has a lot of steps. Don't be afraid to just write more functions that "assemble" and hide some of those steps from other parts of the system that don't care about the details (e.g. if you need to load, clean the CSV, fix empty fields, etc. this can all be hidden in a function that just is called load-data from a caller's perspective).

🙏 2
pithyless16:06:56

It seems your -main docstring is already a good template for what your let would look like, eg.:

(defn main
  "Read a CSV, massage the data, interpolate some points, and plot the data"
  []
  (let [path                "/Users/easy/Downloads/20220519082008-04213-data.csv"
        ;; ^ In the future, you may want to pass the path as CLI option;
        ;;   just need to change string to a function call that parses ARGS
        data                (load-data path)
        metered-data        (import-meters-data data)  
        interpolated-points (interpolate-dasta ,,,)
        plot-data           (prepare-plot-data ,,,)
        sketch-data         (prepare-sketch plot-data)]
    (q/sketch sketch-data)))

Eric16:06:55

Thank you, guys! It’s so great to get some guidance and reassurance to get me heading in the right direction. As you’ve suggested, I will build my functions to hide the complexity from main, and I will definitely add CLI parsing to generalize the program.

🙌 1
kennytilton17:06:06

@U032LAD66SF wrote: "I guess I imagined with that approach that if I ended up adding much more code, my main would be one giant let vector that would get hard to read… or at least not look like “idiomatic” Clojure. Your passing observation that the code is not ideal even for the imperative paradigm, @U032LAD66SF, may be a key here. We see a concern for "hard to read", but that originates in the imperative approach whomping together intermediate computations in one stack, with some values being consumed further down, but with no easy way to see where those occur. So one neat trick would be, without yet worrying about idiomatic Clojure, would be to first reorganize as great imperative code. Then we can worry about atoms and FP and dynamic vars and all that. btw, I have developed much functional code in steps as your example does, and once I understood what I was doing and had it working, went back and rolled things up into functional code that exposed the state flow. A trivial example:

;;; (def data (load-csv "/Users/easy/Downloads/20220519082008-04213-data.csv"))

(def imported-meters-data
    (->> (load-csv "/Users/easy/Downloads/20220519082008-04213-data.csv")
      clean-csv-data
      drop-header-row
      csv-strings->numbers
      project-WGS84->meters))
What I do not know without searching the rest of the code is whether data gets used again later on, so maybe mine is a bad edit, but it gives you the idea. The neat thing is that not pausing to establish the data binding tells the reader it will be used only here, if that is the case. Next, the code spacing suggests lines 328-347 https://github.com/ericdlaspe/elevation-plot/blob/1fd9b209996783eaee457ad7f231b431aec8978a/src/elevation_plot/core.clj#L328-L347 , commented "Import CSV data", are a standalone computation yielding imported-meters-data and a couple of values derived off that. This suggests a carve-out into a standalone function is possible, which would divided the one wodge of lets in two. 🙂 If you want this carve-out to return those extra values, have it return a map:
(defn imported-meters-digest [csv-file-name]
   ...processing...
  {:data imported-meters-data
   :proportion (get-data-proportion imported-meters-data)})
...and now we have this concept of a digest if that is useful. Next passing thought. Things like (def swidth 700) appear here and there. Can those be rolled up into a sketch-config map, if that is what they are? Readability would improve with all those gathered in one bunch. And then we would see code like:
(def sheight (m/round (* (:swidth sketch-config) (:proportion digest)))
Now we know at a glance the origin of sheight. As I said, I always start as you have when sorting out what I am up to, and then I clean it up so people think I am this amazing programmer who can write code that way the first time. 🙂 And as you have anticipated, as the app and required computations grow, they will stay organized. hth.

👍 1
🙏 1
💯 1
Eric17:06:03

@U0PUGPSFR Wow, there is so much helpful stuff to unpack. Above all, I appreciate the relatable narrative parts describing how your initial approach to problems is not much different. > What I do not know without searching the rest of the code is whether data gets used again later on, so maybe mine is a bad edit, but it gives you the idea. No, that is a good edit! That observation—along with everything you said building to it—is a key concept I need to remember. Extracting these parts of my code into functions is going to help readability by making it clear what data continues to be relevant further along in the execution of the program. I would guess that will also help memory performance because the intermediate data can be garbage-collected. > ...and now we have this concept of a digest if that is useful. Am I understanding correctly that a digest is a data structure you just generally use for returning multiple values from a function?

Eric17:06:06

Also, where would I end up putting this piece of code since it would be an “inline def” if it were inside any function, yet it depends on other functions?

(def sheight (m/round (* (:swidth sketch-config) (:proportion digest)))

kennytilton21:06:19

"Am I understanding correctly that a digest is a data structure you just generally use for returning multiple values from a function? (edited) " Pretty much! I was just making a (perhaps) minor point that we can keep things self-documenting, when pre-processing a wodge of raw data, by naming it carefully. Like, what is this? The raw data? The raw data transformed? The raw data with some helpful derivations off that? The name "digest" reminds me, "Oh, OK. This is the wodge that has been preprocessed with a few handy aggregates off that". As I code further I will always know if I want the raw data, the filtered data, or aggregates off that, so I know right where to go. As Confucius said, "First we correct the names." https://en.wikipedia.org/wiki/Rectification_of_names

kennytilton22:06:51

@U032LAD66SF asked: "Also, where would I end up putting this piece of code since it would be an “inline def” if it were inside any function, yet it depends on other functions?

(def sheight (m/round (* (:swidth sketch-config) (:proportion digest)))
This is just the same code packaging task you handle now in any other language. Anticipating growth, I can envision a config.clj and digest.clj each with their own little APIs. Perhaps you want a normalied-data.clj to bundle up all that, and that offers am sheight entry. btw, during this decomposition process you get into two cljs needing the other, break the shared code out into utils.clj or some such, a glib answer, but if glib does not work we need to think deeply about the cyclic dependency. How did the cycle arise? It is worth understanding this -- our code is talking to us, and it is not happy. Sorting this out will enhance the authoring of every line of code to come. But again, this is universal best practices, nothing Clojure specific, other than Clojure letting us work quickly and dynamically to sort out the ideal code organization.

🙏 1
gratitude-merci 1
Eric02:06:40

Ah, I see. Have some init code in each source file or library that prepares constants related to its purpose. This is very helpful. Coming to Clojure, maybe I took the task of unlearning old OOP patterns to an extreme. … Not that I was any good at organizing code in other languages. Its probably a great thing that Clojure is forcing me to address that weakness at this early stage of my project.

🙌 1
kennytilton03:06:41

@U032LAD66SF wrote: "maybe I took the task of unlearning old OOP patterns to an extreme." "OOP Bad!" was as big a mistake as NoSQL. Discuss. 🙂 The CLJ OOP phobia goes too far with its course correction away from the productivity crushing bloat of Java/C++/TypeScript. Clojure devs must deal with objects because the world has objects. CLJ has acknowledged this implicitly with clojure.spec. As a curious newcomer, you may enjoy these ruminations from CLJ's creator: https://clojure.org/about/spec. But I am not a spec fan; it feels like Java. "Its probably a great thing that Clojure is forcing me to address that weakness " You seem quite willing! But yes, I am reminded of when I introduced structured programming at a company forty years ago. My manager got into it big time, and when I said I might have a GOTO here and there. He challenged me not to have any. To show him how silly the code would turn out, I indeed wrote completely GOTO-free code. It was a real "wow" experience; something greater than the absence of GOTOs emerged, a certain transparency. The neat thing is that the next time you work in an imperative language, you will find a way to continue writing in a more functional style. ps. Make sure you share the rewrite!

clojure 1
clojure-spin 1
didibus04:06:18

> As an example, I have in -main a function that reads a CSV, cleans the data, transforms it a bit, then extracts some values (like min, max, and other stats) that need to be used by many more functions that will further reshape the data and generate graphics and reports about the data. What doesn't work with just this:

(defn -main [& args]
  (let [raw-csv (slurp "./myfile.csv")
        clean-data (clean raw-csv)
        [min max avg p99] (extract-stats clean-data)]
    (generate-graphics min max avg p99)
    (send-reports min max avg p99))

☝️ 1
didibus05:06:01

Having a big -main I would say is actually quite idiomatic, in Clojure, you want to have things as flat as possible, with shallow stack, and your main should look like a big data-flow definition, something that's just the step of the recipe with their data connections

1
didibus05:06:44

If you have 100 steps in your recipe, and you find its too many details, and you want to start at a higher level view, you'd just move a chunk of them in a sub-function that's like a sub-workflow.

didibus05:06:28

Imagine a recipe: 1. Get coffee bag from counter 2. Open coffee bag 3. Get grind container from counter 4. Pour 10gram coffee ground from open bag into grind container 5. Locate coffee machine on counter 6. Pour content of grind container into coffee machine 7. Grab water cup from counter 8. Locate water faucet 9. Pour water from faucet into water cup till full 10. Pour water cup content into coffee machine 11. Press coffee machine start button You basically want your top functions (such as main, or commands for a CLI, or API handlers for a service) in Clojure to look and read just like this. But just like in natural language, you might think, that's too much details, can't we abstract some things a bit behind some higher level concepts so that I can read this more quickly with less detail? So then you'd just factor out some of the steps into a function that abstract the series of step into a new name for it. 1. Get coffee bag, coffee grinder, water cup, water faucet and coffee machine. 2. Pour 10gram coffee ground from coffee bag into coffee grinder. 3. Pour content of grind container into coffee machine 4. Fill up coffee machine with water using cup from water faucet. 5. Press coffee machine start button How do you "2. Pour 10gram coffee ground from coffee bag into coffee grinder." ? 1. Open coffee bag 2. Pour 10gram coffee ground from open bag into grind container How do you "4. Fill up coffee machine with water using cup from water faucet." ? 1. Pour water from faucet into water cup till full 2. Pour water cup content into coffee machine And so on. You'd do the same, just with functions.

pavlosmelissinos05:06:40

> Having a big -main I would say is actually quite idiomatic, in Clojure, you want to have things as flat as possible, with shallow stack, and your main should look like a big data-flow definition, something that's just the step of the recipe with their data connections > I can understand the argument for a shallow stack but I don't consider it a strong one and I dislike long functions so, when I have to choose one or the other, I almost always go for shorter functions. If your names are good the person that reads your code can skim it much more easily if it's broken down. How do you decide when it's time to break some logic down into functions? Some obvious reasons are reusability and isolating side effects but what other factors do you consider important?

didibus06:06:04

One thing that is often underappreciated is what is meant by "readable" code, and counter-intuitively, code that is easy to read at a glance is not really the point, you want code that is easy to make the change you want to make to it. A function used by two parent callers is harder to change, because if you need to change it only for one caller, you might break the other for example. A function that calls another that calls another is harder to change, because introducing new things in between, or figuring out the actual flow involves jumping to more places to see what happens next. What that means is that if you have smaller more granular steps, they're easier to compose and recombine. But if you reuse too much of your composition code, code becomes harder to change and more fragile. For me, if you have granular steps, and a big composition of them it's a pretty good place to be in. But ya, generally if I find myself reusing a "series of step" in many places, that's when I might extract it out, and even then, I often consider just copy/pasting it, based on if I believe that in the future I'll want to change it for all parents using it or not, if the former I'll break it out into a function, if the latter I'll copy/paste. Most logic will be in functions, just not the orchestration. You won't see a loop, or data processing in the main or top functions for example, those will be behind pure or impure only functions. (unless your orchestration needs a loop, like say a game loop for example, or CLI command loop) That's why I showed the recipe, a recipe is just a description of the steps, doing them is another thing, the doing will be in reusable functions, but the steps will be at the top defining the workflow.

didibus06:06:16

One way to make it more readable is to add comment sections, you can break it out into a function + name and it's a similar result, but it makes it more risky that other code takes a dependency on it and its suddenly being reused, it also means if you want to see the details, you got to jump in/out of the factored functions, and it means if you need to interleave something now, you have to move it back into the function or do more refactoring.

didibus06:06:33

Have you ever heard of "Ravioli Code", it's an Anti Pattern like Spaghetti Code. You have a lot of little functions, they are easy to understand individually, but taken as a group they're not, and they add to the app callstack and complexity.

didibus06:06:27

Anyways, it's not to be taken too religiously. The important part is to have a separation between orchestration logic / effect logic / transform logic. Orchestration in the series of effect / transform logic functions in some order with some connection between their ins and outs. And you want it so that orchestration fns calls effect fns and transform fns, effect fns only calls other effect fns, transform fns only calls other transform fns. If orchestration fns get too big for your liking, factor some chunk out into smaller orchesration fns, and then some orchestration fns can call others, but be careful thought if you reuse these from more than one parent orchestration fn, because it prevents the behavior to diverge in the future without refactor.

🙏 1
pavlosmelissinos07:06:09

Your orchestration functions should resemble your business logic. If your business logic has nested orchestration, you can't avoid having that in the code as well. But I agree with the separation of orchestrating/transform/effect logic and that you could take it too far and break down your logic too much if you're not careful. I like to think of transformation functions as definitions. As in you have this complex concept that is derived from this value and that. It makes sense to have interim definitions, so composing them is mostly harmless and I use nouns to name these functions, so it's clear what they represent.

didibus07:06:10

I don't think I understand what you mean about nested orchestration business logic?

Eric15:06:17

@U0PUGPSFR I will have to look into spec more. I love the concept, but using it looks a little bit painful and I’d rather not endure the pain yet for a project that’s still in alpha. I just finished 9 years at a company where I was “living on” alpha software constantly and I’m burned out on it, tbh. I really appreciate reading about your experiences. Your advice is relatable, which is really important in helping me feel less unique and alone in having the kinds of problems I’m having. Thank you for putting in the work to teach me effectively. 🙂 And I certainly will share the rewrite!

Eric15:06:13

@U0PUGPSFR Oh, I forgot to mention that, yes, I am learning many valuable lessons about programming in general in the process of learning Clojure. For example, though I learned about recursion in college, I never bothered to use it outside of solving certain coding interview questions. In my day jobs, it was discouraged because unless it was applied very carefully, it could cause stack overflows that were easily avoidable with iterative algorithms, and the iterative approaches were usually easier to read. Now I’m having fun revisiting recursion and I think it feels more natural in Clojure. Other benefits I’m getting include: Being influenced/forced to make smaller functions, and leaning toward trying a TDD approach.

👍 1
Eric15:06:32

@U0K064KQV Thank you for the recipe analogy for organizing functions and deciding on smaller units of computation. > … counter-intuitively, code that is easy to read at a glance is not really the point, you want code that is easy to make the change you want to make to it. This is the part of your explanation that resonated most with me. It definitely gets difficult deciding, for each function, on a “boundary of concerns” and then the reasonable interface that should stem from that. In my elevation-plot code, for example, I wanted to play with different methods of interpolation (which I was just making up for experimenting with the performance of various Clojure features). Writing those functions to be modular quickly became tricky because after I wrote two that took the same parameters and so could be interchanged, my third interpolation method needed extra information. :face_vomiting: Just like that, the fun of interactively experimenting and quickly iterating became a slog through the hell of deciding how to fit a square peg into a round hole. All the options for solving that are bad. Anyway, I do enjoy breaking problems down into bite-sized, composable pieces so the necessary changes in a case like that can be isolated.

pithyless19:06:41

> Writing those functions to be modular quickly became tricky because after I wrote two that took the same parameters and so could be interchanged, my third interpolation method needed extra information. Clojure has two killer features here that are often under-appreciated: instead of variable arguments pass an open map (ie. a map that may have more keys than the specific function may need) and use fully-qualified keywords (reduces the chance of named collisions and gives you more confidence to pass around data with named-context). Neither of these are limited to Clojure, per se, but they are idiomatic and prevalent in Clojure codebases (so you will find they have more utility).

🙏 1
Jim Strieter00:06:28

TLDR: Every function requires 1 map which defines all program state, and returns a new variant of the state map. State map is defined in a file called loop_state.clj, which documents every key that all functions in your program might require. That allows your functions to assume the existence of certain keys, and gives you tremendous flexibility over what you do and how to do it. I'm writing my first multi-1k line Clojure program. I saw others wrote some really good answers using atoms. I'm writing mine without atoms so I thought I'd throw my approach in the mix. The program I'm writing is a platform for a stock trading robot. Note that I did not say that the program is a stock trading robot. My project is a platform for a stock trading robot. The goals for this project are to 1) allow specific trading rules (the business logic, the actual trading robot) to be hot swapped at runtime; 2) to make it easy to run large optimjzation problems; and 3) to run the same code in real time as I did for simulation. Here is what I did: I wrote a file called loop_state.clj which defines a map like this:

(def init-loop-state {
  :epoch 0 ;; Unix timestamp for simulation time
  :ticker-list '(:ABC :DEF :GHI ... :XYZ) ;; List of tickers we are simulating
  :spot-prices {:ABC 42 :DEF 99 :XYZ 404} ;; Every ticker, a key, leads to some number representing stock price at time epoch
  ;; Everything that can be considered state is represented in this map.
  })
An event loop runs business logic until some termination condition is reached:
(defn event-loop [state]
  (loop [state' state]
    (if (:keep-going state') 
      (recur (f1 (f2 (f3 (f4 ... (fn state) ... )))))
      state')))
Some useful properties of this approach: 1. Every function called by event loop requires exactly 1 argument - a map - and returns 1 thing - a map. We defined the keys that map has in init-loop-state above. (Strictly speaking you don't need to define init-loop-state, but the project is far more manageable if you define all your fields in one place. That way you have documented what keys your project requires. You can also assume the existence of all these keys. Additionally, for unit testing, you can easily send a known state into any function by doing stuff like this: (function-being-tested (conj init-loop-state {:field1 x :field2 y ...})) which makes unit tests more manageable than their OOP equivalent.) This convention of always taking state and returning state is motivated by a quote sometimes attributed to Donald Knuth: "It is better to have 100 functions operate on 1 data structure than to have 1 function operate on 100 data structures." My experience has been that that statement leads to maintainable code. 2. event-loop returns a state. That means that after 1 event loop does whatever it does, you can take that output and pass it into something else - dump results into a file, run some kind of data analysis, or pass that state into another event loop. Anything you want. Do you want to do something C-like and return 0 if the program ran successful and nonzero for an error code? That's easy! Just call something like this when you're done:
(assoc state' :termination-result (if (:no-errors state') 0 (:error-code state')))
and then in your -main function return that result. 3. All data structures in Clojure are immutable by default. Therefore if you want to pass the same state into several different variants of the same program, each one will automatically see the correct input. Say, for example, I want to test N variants of a trading strategy to see how to best survive the crash of 2008. I can run event-loop from Jan 1, 2001 to Dec 31, 2007, which returns a state map like the one we defined in init-loop-state above. After that, I can feed the state from Dec 31, 2007 into an optimization loop, like this:
(defn optimize-loop
  [init-condition]
  (loop [trading-system-list '(variant-1 variant-2 ... variant-n)
         results '()]
   (if (empty? trading-system-list)
       results
       (recur
         (rest trading-system-list)
         (conj results (simulate-one-variant init-condition))))))
This approach is scalable because you can call an algorithm at the start of your program to decide what the N variants of your trading system are, and assign a different subset of those variants to every core on your laptop or every computer in your cluster. At the conclusion of each optimization run, each core/thread/computer/etc sends results to a database which compiles the results. This design has allowed a team of 1 (me) to build a decently complex platform doing scientific computing on a database > 10 GB presently and which may grow > 100 GB. I don't have to worry about the number of computers or amount of data eventually placing a limit on the algorithms I run, which is nice. Open to feedback and I hope this helps.

Eric13:06:11

@U014Z9N3UTS Sorry for my delay in responding—I didn’t mean to leave you hanging. Now that I’ve read it, my impressions are: 1. Wow! That is an ambitious project and I love the idea. It sounds like you are essentially building your own version of Interactive Brokers. It sounds really powerful to have complete control of the data and the ability to run all your backtesting locally. Best of luck with it! 2. That method of passing everything around as a map within an event loop sounds awesome! Thank you so much for sharing how you implement it. Now I finally understand what it means to have a global map that can be used by all parts of the program without breaking the rules of immutable state. I really did not want to have to resort to mutating state, so this is perfect. I started using loop…recur to update vectors within a few functions, but never thought to expand that concept to the whole program state. It’s a little mind-bending for me (even within a single function), but I’m gradually grokking it.

Eric13:06:06

@U014Z9N3UTS Now, I have a question or two about this approach, if you don’t mind. I’m very curious and a little afraid. > … a quote sometimes attributed to Donald Knuth: “It is better to have 100 functions operate on 1 data structure than to have 1 function operate on 100 data structures.” My experience has been that that statement leads to maintainable code. When I read a quote like this, it feels a bit like I’m interpreting the words of a prophet. “Did he really mean for us to take it this far? 😆” But your approach really does seem do exactly that, so it sounds great. I’m curious though… Is it an unqualified advantage to have every single function take that one map as its sole argument? Does it ever become difficult to understand which variables within that state map are manipulated by particular functions? I can imagine a let at the top of each function acting as a sort of function signature, telling you up front which keys are used… How do you handle it? What would you say are the drawbacks (if any) to this approach?

didibus17:06:02

I've never personally tried this approach, but I've heard in functional circles sometimes it's referred to as an anti-pattern, because it is very similar to using global variables everywhere. Technically you pass a local variable, but the input contains everything the app uses from everywhere. So people consider it a form of hidden global context. Again, I've never personally tried it, those people might have their reasons for saying this and maybe it doesn't always apply depending what you want to do or given you're the only developer on the project. Or I'm guessing if you need more often to read data then to write to it, etc. Seems an interesting design in any case. I'd be curious to try it for myself one day.

pithyless19:06:59

Small correction: the original quote "It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures." comes from Alan Perlis (also known for other epigrams such as "A LISP programmer knows the value of everything, but the cost of nothing."). Here's a web cache of the source article (originally published in 1982): https://web.archive.org/web/19990117034445/http://www-pu.informatik.uni-tuebingen.de/users/klaeren/epigrams.html

pithyless19:06:26

PS. I would think Donald Knuth would be far more inclined to say each new program needs a custom data structure to solve the very precise problem described in the most efficient manner possible, without any unnecessary frills and cruft. And if it's well thought out, you can prove it to yourself before even compiling it. The actual process of typing out, compiling, and running the program is almost an after thought.

pithyless19:06:51

> I've never personally tried this approach, but I've heard in functional circles sometimes it's referred to as an anti-pattern, because it is very similar to using global variables everywhere. Technically you pass a local variable, but the input contains everything the app uses from everywhere. So people consider it a form of hidden global context. I found this a common approach and complaint in Haskell circles, where inexperienced Haskellers would by iterative modification and expansion of a codebase end up needing more and more context in a very deeply nested function, leading sooner or later to this explosion where you may end up passing around the entire world AppState as an argument to all functions. Another way the problem sometimes showed up was wanting to do some IO in a deeply nested layer and ending up "poisoning" and wrapping most code in IO side-effects by accident. Perhaps there is something we can learn from that community on how to get some of the benefits without repeating all the mistakes? I find a lot of positive inspiration in things like: • Fulcro's global-state-db; • Datomic/XTDB/Asami/et.al. separation of read/write operations; • Pathom passing around global context but building an implicit DAG (directed-acyclic-graph) of resolvers/mutations so it's easy to compartmentalize the amount of context you need at any moment; • Interceptors (pedestal/sieppari/et al) and systems like Onyx or previously plumatic/graph declaratively describing computation and data nodes

pithyless19:06:42

^ wow, sorry - started going off on quite a tangent there. Going to cut this short, because we've strayed really far from the original thread post. :)

didibus22:06:43

The 100 functions over one data-structure versus 10 over 10 refers to ADTs and Classes mostly, where you model things at the app level. It means a Car can only: honk, accelerate and break. If you just model a Car as a Map, than honk is just (assoc car :honk true), accelerate is just (assoc car :acceleration 2.3), etc. It means be it a Car, a Truck, a Player, a Star, a LampPost, you still have access to all hundreds of Map functions. You can remediate this a bit by introducing Traits or Mixins that kind of let you reuse functions from one type to another, but it still involves a lot more planning.

Jim Strieter10:06:54

@U032LAD66SF I left off qualification to make a point. In practice it would be stupid to say "all functions must have the same 1 signature throughout your entire program." Functions that are math heavy I usually like to give descriptive parameter names and then call them from a wrapper. The "everything gets state and returns state" pattern makes the most sense for things that need to be composable, and it's not 100% clear in advance what the composition will be.

Eric12:06:37

@U014Z9N3UTS Makes perfect sense. Thank you for the clarification. 🙂

Eric16:06:53

@U0K064KQV > I’ve heard in functional circles sometimes it’s referred to as an anti-pattern, because it is very similar to using global variables everywhere. This pattern struck me that way too. I can imagine how it might get out of hand, and it would take a strong sense of organization—or deliberate and rigorous planning as you suggested, @U05476190—to ensure that it doesn’t. Reading @U014Z9N3UTS’s technique also gave me (brief) pause because it seems like a clever way of sneaking “mutability” into functional programming. When I thought about it more for another second, I changed my mind. I think it’s not really equivalent, because we retain the benefits of immutability with respect to each function and even the event loop. Also, I’m very new to FP, so I can’t really make such a judgement from any place of experience. I would be interested to know how common Jim’s approach is in FP though.

Eric16:06:29

@U014Z9N3UTS Btw, have you written a blog post or something on Clojureverse about your technique? It seems like something that would be really helpful to other beginners.

Jim Strieter18:06:56

If you use immutable data structures you're not sneaking anything. I use the word state because I find it useful to think “feed state in, get state out,” but technically it’s not state you're feeding at all, just a data structure. The reason I find this helpful is I like to think about the event loop as having internal state, even though it truly doesn't. State in this case is a useful fiction, nothing more.

👍 1
Jim Strieter18:06:04

One caveat of this approach is that it requires a fair amount of referring to loop_stare.clj to check the structure of everything. There is a lot of fixing null pointer exceptions when you make a mistake. I'm happy to write an article about the pros and cons if others in the community think it's a good practice.

mhuebert17:06:09

asking for a friend: > Do you know if there is a clojurebridge or camp for teenagers? > > My 15-year-old grandson wants to learn programming. I thought of working through maria.cloud with him. > > But if there’s a specific online school or camp aimed at teenagers that might be even better

mhuebert17:06:57

> I like Maria.cloud because there’s no need to set up an IDE or anything else. And definitely a 15-year-old could understand all the assignments there. I just don’t know where I would go after I walked him through the first few lessons

Bob B18:06:28

I'm hoping someone else knows more about classes/camps, but in terms of places to go after maria.cloud, 4clojure forever (<https://4clojure.oxal.org/>) has more exercises with difficulty ratings, and is totally web-based. There's probably a point at which a student would want something local for saving files, maybe testing sub-forms in a REPL, etc, and at that point there's also exercism's clojure track (<https://exercism.org/tracks/clojure>), which I believe also offers mentoring/personal feedback - not a camp per se`, but at least a source of feedback.

🙂 1
devn22:06:12

I tend to view those as a bit more complicated than a beginner programmer might require. Making me think about writing up a short summer course option. I've never taught but have mentored professionally. Could be fun to run a course

devn22:06:29

Then again, maybe at 15 they're perfectly reasonable.

devn22:06:47

Some guidance required I imagine, depending on background.