Fork me on GitHub
#beginners
<
2024-07-05
>
jimmysit004:07:10

Hey, I have a structure that looks like this (["3" "blue," "4" "red"] ["" "1" "red," "2" "green," "6" "blue"] ["" "2" "green"]) and my goal is to build a nested structure that's something like

[[{:blue 3 :red 4}
  {:red 1 :green 2 :blue 6}
  {:green 2}]]
How can I achieve that?

seancorfield04:07:31

Think about the steps to get from what you have to what you want. In each group, you want to remove the empty items I think? Then you want to break each group into pairs (`partition`), and then for each sequence of pairs, you want a sequence of transformed pairs, and then you want a sequence of hash maps from that.

seancorfield04:07:54

So you're going to map (or mapv) some process over the groups... ...that process is to... ...`filter seq` to keep only the non-empty items... ...then partition 2 to break the sequences into pairs... ...then take each (so map) [x y] pair and turn it into [(keyword y) (parse-long x)]... ...then pour those pairs into {}...

jimmysit004:07:58

The thing with partition is that it kind of messes up the fact that they're already split, won't it? This is what I tried:

(defn foo
  [bar]
  (let [split-input (str/split bar #";")]
    (->> split-input
         (map #(str/split % #" "))
         (map #(remove empty? %)))))

(partition 2 (foo "3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green"))
; ((("3" "blue," "4" "red") ("1" "red," "2" "green," "6" "blue")))

seancorfield04:07:55

Right, but that isn't what I'm suggesting.

seancorfield04:07:31

The "process" is a whole bunch of thing applied to each group.

seancorfield04:07:52

I'm not suggesting applying partition 2 to the whole sequence -- but to each group in that sequence.

šŸ‘ 2
jimmysit004:07:49

Oh sorry, I misunderstood

jimmysit004:07:18

you mean something like this:

(map #(partition 2 %) (foo "3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green"))
; ((("3" "blue,") ("4" "red")) (("1" "red,") ("2" "green,") ("6" "blue")) (("2" "green")))
right?

Bob B04:07:18

I believe that's what was intended. Another thing you might want to consider is splitting with the trailing space in both cases (after the semicolon and after a comma), which handles some of the text clean up (i.e. the commas), and could also replace the partitioning

jimmysit004:07:11

Alright, I got to this point

(("3" "blue" "4" "red") ("1" "red" "2" "green" "6" "blue") ("2" "green"))
which is pretty close to what I want. I'm not 100% sure how to destructure it so I can get, say blue to become a keyword instead

seancorfield04:07:12

Oh, I hadn't even noticed the , -- good call Bob!

jimmysit004:07:42

this is the code I have so far

(defn foo
  [bar]
  (let [input (-> bar
                  (str/replace #"," "")
                  (str/split #";"))]
    (->> input
         (map #(str/split % #" "))
         (map #(remove empty? %)))))

Bob B04:07:42

you can call keyword on a string like "blue" to make :blue

seancorfield04:07:45

@U01TN77V2HJ So (map #(partition 2 %) coll) will get you the pairs I was talking about.

šŸ’œ 2
seancorfield04:07:43

And once you have pairs, you can (map (fn [[v k]] [(keyword k) (parse-long v)]) pairs)

jimmysit004:07:42

The thing is, that partition creates another lazy seq, so now it's a list of a list of a list, basically:

(map #(partition 2 %) (foo "3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green"))
((("3" "blue") ("4" "red")) (("1" "red") ("2" "green") ("6" "blue")) (("2" "green")))

jimmysit004:07:13

so I don't really know how to destructure that

seancorfield04:07:45

Like I showed above.

seancorfield04:07:51

(fn [[v k]] ..) will match ("3" "blue") so that v is "3" and k is "blue"

jimmysit004:07:43

won't it match (("3" "blue")... ?

(map (fn [[v k]] [(keyword k) (parse-long v)]) (foo "3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green"))
; Error printing return value (IllegalArgumentException) at clojure.core/parse-long (core.clj:8052).
; Expected string, got clojure.lang.LazySeq

(foo "3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green")
((("3" "blue") ("4" "red")) (("1" "red") ("2" "green") ("6" "blue")) (("2" "green")))

seancorfield04:07:28

You need map over map here.

seancorfield04:07:57

(map #(map (fn [[v k]] ..) %) (foo ..,))

šŸ’œ 2
jimmysit005:07:09

and, how do I build the nested map? I thought of

(map #(map #(into {} %) %) (foo "3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green"))
but got
; Syntax error reading source at (REPL:264:14).
; Nested #()s are not allowed
(foo "3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green")
(([:blue 3] [:red 4]) ([:red 1] [:green 2] [:blue 6]) ([:green 2]))

jimmysit005:07:24

oh wait, I can just (map #(into {} %) coll)

didibus07:07:29

Thought I'd share this here. I know sometimes beginners struggle to understand how to do certain things. Say you wanted to implement a Stack data-structure. In an OOP language, you'd have encapsulated the state (data) along with the behavior (methods) all behind an object. In Clojure, you separate the state (data) and behavior (functions). But it ends up being very similar still. The only difference is the state (data) is returned directly to the caller, instead of being wrapped (encapsulated) inside an object and having the object returned. It's a subtle difference, that takes a while to wrap your head around. The data is not encapsulated in Clojure, but that's ok, because it is immutable, and so it is safe to expose it directly and it does not need to be encapsulated because of that. Here's a simple stack implemented in Clojure:

(ns stack)

(defn make-stack []
;; This is where you initialize the state of what in OO would be initializing the fields, here you just create state in a data-structure of your choice and return that. This state is the "object" in a sense that all stack functions operate over. But it is not encapsulated, so this returns the state directly, not an object wrapping it.
  '())

(defn push [stack element]
  (cons element stack))

(defn pop [stack]
  (if (empty? stack)
    [nil stack]
    [(first stack) (rest stack)]))

(defn top [stack]
  (first stack))

(defn is-empty? [stack]
  (empty? stack))

(ns user
  (:require [stack :as s]))

(def stack (-> (s/make-stack)
               (s/push 1)
               (s/push 2)
               (s/push 3)))

(println stack)  ;; Output: (3 2 1)

(let [[popped new-stack] (s/pop stack)]
  (println popped)  ;; Output: 3
  (println new-stack)  ;; Output: (2 1)
  (println (s/is-empty? new-stack))  ;; Output: false
  (println (s/top new-stack)))       ;; Output: 2
It's almost like defining a class Stack , you have a namespace Stack with your functions. And you have a "constructor", but it doesn't construct an object of type Stack, it just initializes a data-structure that will contain the state of the Stack and returns that. All other functions operate over this data-structure. The data-structure is immutable, so that's all safe. You would do the same if you wanted to have a User, a Car, a Player, a Board, etc.

šŸ‘ 2
2
adi08:07:55

Related: The Expression Problem. The grid of Type x Method is fully extensible in Clojure. We can add new functionality to existing types. And we can extend new types to existing functionality. chouser's talk walks through it in depth. Clojure's solution to the Expression Problem https://www.youtube.com/watch?v=t6ktSfInNhU

adi09:07:15

On the same lines, Sean Devlin's talk is fantastic too. Protocol XIII: Clojure Protocols Explained https://www.youtube.com/watch?v=kQhOlWXXl2I

adi09:07:44

IRL use case, looping back to didibus's original post. I used these ideas to implement the so-called "Page Object" system in an integration testing framework (using clj-webdriver) while I was QA at a former employer. Each "page object" is basically a Record + protocols. And there is a constructor to create page objects, so they can be handled specially (distinct from other records and maps). That allowed us to model a fully object oriented system with immutable data and functions. And write browser automation tests with it. slides: https://github.com/adityaathalye/slideware/blob/master/designing_object_functional_system_IN-Clojure_2016.pdf talk: https://www.youtube.com/watch?v=hwoLON80ZzA

didibus21:07:33

Right, the expression problem normally is addressed in Clojure using Protocols or Multimethods. That said, you really don't often have that problem I feel. Like inside an application, it's not a big deal to add more functions to the namespace, or alter the data. The expression problem comes up more with libraries, frameworks, and such. Where you're trying to provide more generic facilities. I'd say for testing sometimes it comes up in an application as well. But even then only around specific components, like maybe your DB accessor you want to be able to change the DB it's using to a in-memory/mocked one vs the real one, etc.

šŸ‘ 1
didibus21:07:21

At least for beginners I'd start with just what I showed, and that can take you pretty far already.

šŸ‘ 1
adi08:07:55
replied to a thread:Thought I'd share this here. I know sometimes beginners struggle to understand how to do certain things. Say you wanted to implement a Stack data-structure. In an OOP language, you'd have encapsulated the state (data) along with the behavior (methods) all behind an object. In Clojure, you separate the state (data) and behavior (functions). But it ends up being very similar still. The only difference is the state (data) is returned directly to the caller, instead of being wrapped (encapsulated) inside an object and having the object returned. It's a subtle difference, that takes a while to wrap your head around. The data is not encapsulated in Clojure, but that's ok, because it is immutable, and so it is safe to expose it directly and it does not need to be encapsulated because of that. Here's a simple stack implemented in Clojure: (ns stack) (defn make-stack [] ;; This is where you initialize the state of what in OO would be initializing the fields, here you just create state in a data-structure of your choice and return that. This state is the "object" in a sense that all stack functions operate over. But it is not encapsulated, so this returns the state directly, not an object wrapping it. '()) (defn push [stack element] (cons element stack)) (defn pop [stack] (if (empty? stack) [nil stack] [(first stack) (rest stack)])) (defn top [stack] (first stack)) (defn is-empty? [stack] (empty? stack)) (ns user (:require [stack :as s])) (def stack (-> (s/make-stack) (s/push 1) (s/push 2) (s/push 3))) (println stack) ;; Output: (3 2 1) (let [[popped new-stack] (s/pop stack)] (println popped) ;; Output: 3 (println new-stack) ;; Output: (2 1) (println (s/is-empty? new-stack)) ;; Output: false (println (s/top new-stack))) ;; Output: 2 It's almost like defining a `class Stack` , you have a namespace Stack with your functions. And you have a "constructor", but it doesn't construct an object of type Stack, it just initializes a data-structure that will contain the state of the Stack and returns that. All other functions operate over this data-structure. The data-structure is immutable, so that's all safe. You would do the same if you wanted to have a User, a Car, a Player, a Board, etc.

Related: The Expression Problem. The grid of Type x Method is fully extensible in Clojure. We can add new functionality to existing types. And we can extend new types to existing functionality. chouser's talk walks through it in depth. Clojure's solution to the Expression Problem https://www.youtube.com/watch?v=t6ktSfInNhU

growthesque09:07:30

I've heard Rich Hickey sometimes say that Haskell is tough and people stick to Haskell because by the time they figure it out they have such sunk cost that they can't afford to leave. Obviously it's kind of a joke, but I am curious why Hickey thinks Haskell is like that, aren't Clojure and Haskell somewhat similar?

šŸ‘ 2
daveliepmann09:07:47

is this in Maybe Not or somewhere else?

growthesque09:07:20

Unfortunately I no longer remember. I saw it a while ago but somehow popped into mind right now that I am looking more closely into Clojure.

daveliepmann09:07:28

here's a direct explanation from Effective Programs: > C++ is a very complex language, and so is Haskell, and so is Java, and so are most of them. Clojure is very small. It is not quite Scheme small, but it is small compared to the others. And it is just the basic lambda calculus kind of thing with immutable functional core. There are functions. There are values. You can call functions on values and get other values. That is it. There is no hierarchy. There is no parameterization. There is no existential types, blah blah blah blah blah.

šŸ‘ 3
daveliepmann09:07:09

a classic instance of incidental versus essential complexity, with the claim being Haskell's type hierarchies are incidental

growthesque09:07:16

I think I read somewhere @U04V70XH6 saying that Typed Clojure is super hard, is typing what makes Haskell hard?

daveliepmann09:07:42

(or perhaps not fully incidental but not the trade-off he wants)

phill10:07:41

You might say "have sunk so much into Java...". I remember having that exact thought when Ruby came out. As Robert C Martin wrote in the recent book "Functional Design", Clojure is super easy to learn. The reason you stick to Clojure is, it's a lot less trouble than the others!

teodorlu10:07:33

Clojure and Haskell both emphasize the value of pure functions - but a big difference is how IO is treated. In clojure, any function can do IO - read/write files, send network requests, etc. In Haskell, the type system needs to know exactly where in your application effects are going on. I've sometimes struggled with getting progress early on in Haskell - before I know exactly what I'm doing, how should I treat the IO? In clojure, I can skip both the io modeling and the type modeling. I write out some data, and can start working on it. Or load it from a file (slurp and edn/read-string). Or load it from the network. --- Does that make Haskell harder? I guess it depends on who you ask. Experience with Haskell will teach you to "think in types" - a bit similar to how some clojure people "think in data". Though you're going to have to connect your types to the real world, http, databases and the network, and Haskell's type system won't get you all the way there.

šŸ§  2
seancorfield16:07:37

I said Typed Clojure is trying to solve a hard problem. But, yeah, in general trying to apply the correct types to functions in Clojure is inherently hard because you're often in a situation where the arguments can be a much broader range of values than the obvious, deduced type signature allow. This is the whole issue that a strong type system often prevents you from writing certain valid programs. And the flip side is that a program that satisfies the type system and is therefore "type correct" isn't necessary "logically correct". My PhD work back in the '80s included a bunch type inference research work on dynamic languages: it's hard stuff.

didibus17:07:54

I think Clojure has grown a lot, and I wouldn't really consider it a small language. There's a lot to learn and know nowadays, not sure it's any smaller than Java for example, especially since you kind of also need to learn a lot of the underlying Java.

seancorfield17:07:58

The language is still very small. The set of "core" libraries has grown -- and the ecosystem overall has definitely expanded.

didibus17:07:00

If you start learning about gen-class, protocols, records, structs, transducers, metadata, multi methods, derivation, seqs, chunking, metadata extension, macros, tagged literals, core.async, clojure.test, deps.edn, tools.build, clojure CLI, type hinting, numeric tower, and all the little gotchas in a lot of places. And I'm forgetting stuff for sure, like iteration, recur, aliases, Vars, watchers, exceptions, atoms, refs, agents, spec, destructuring, tap, datafy, and it keeps going

āž• 1
seancorfield17:07:39

Well, gen-class is its own special hell -- but that's really Java's fault šŸ™‚ Struct maps fell out of use a long time ago so those can be ignored. And you can get a long way in Clojure (the language) without a lot of those things. But I take your point. The language syntax is still very small but the semantics have expanded over the years. Of that list, things I've either never used or only very rarely used (in 13+ years of Clojure): gen-class, structs, derivation, tagged literals (mostly), numeric tower (my work doesn't involve math very often), watches, refs... (and I avoided core.async for years).

didibus18:07:31

I don't mean this as a negative. Because it mostly remains simple, all these things are decomplected, often a-la-carte, offer good composability, and don't firm a house of card. Like you said, you can ignore what you don't need, and it's neat that when you do need something you've got all these tools at your disposal. But saying it's small I think is very misleading at this point haha. Even the basic functions I would say have a more than small surface. Take just conditionals. A small version of Clojure would have cond and case, nothing else. But we've got if, when, when-some, when-not, if-let, condp, etc. That's a larger than necessary surface. Don't get me wrong, I like having those extra forms available, but it's more to learn. Once you know them it's great. Let's not forget cl-format haha, you can spend over a week just learning everything that can do on its own, it has so many features. I'd say Clojure is a simple language with a large surface. But in your learning journey, you can start with only what you need and expand as needed. But if encountering an existing code base, if it uses a lot of what's on offer, it'll require learning more than if Clojure was truly a small language.

didibus18:07:00

C++ and Haskell also have large surfaces. C++ also suffers from a lot of things being complected, and it's more likely you fall into a house of card. And Haskell needs you to understand things that are just hard to grasp, even after extensive use, because the level of abstraction of even the basic constructs is really high.

seancorfield18:07:59

> Let's not forget cl-format haha Another "special hell" šŸ™‚ > I'd say Clojure is a simple language with a large surface. But in your learning journey, you can start with only what you need and expand as needed. I like that characterization. A lot. ā¤ļø

šŸ‘ 2
lepistane13:07:01

What do you use for hosting your clojure apps and websites? Heroku used to have free tier where instance would sleep when not in use. Was super easy to deploy as well. As far as i can see free heroku option is gone. What do you use now?

valerauko13:07:03

The cheapest linode VPS

āž• 2
lread13:07:21

cljdoc uses a DigitalOcean droplet

šŸ‘ 4
danieroux13:07:44

https://application.garden/signup is where I would go to play

āž• 2
danieroux13:07:27

#C06TTMERLEP

lepistane14:07:06

is app.garden even usable? @U9E8C7QRJ is it wip only?

daveliepmann14:07:07

you need an invite but it's usable

āœ… 1
didibus17:07:32

Just GitHub pages

Nim Sadeh18:07:15

I use DigitalOcean droplets which gets me predictable pricing. I have also used Google Cloud Run and currently Google Cloud VMs

phronmophobic19:07:31

For static sites, I use s3 or github pages. For dynamic sites, I use digital ocean.

šŸ‘ 1
Nim Sadeh18:07:10

I want to ask about a general pattern I see in libraries for external data systems (e.g., carmine for Redis, next.jdbc for SQL, Cognitect for AWS). These libraries use macros to validate configuration at compile time. That seems like an antipattern to me. Configuration should be validated at runtime. The for doing it at runtime is typically low - if you forget to set an environment variable your application will crash and you have to set it. The cost for doing doing it at compile time is that I have to litter my code with pointless defenses against empty configurations, e.g.

{:hostname (or (env :hostname) "")
Because otherwise lein uberjar crashes. Why is this a common pattern in Clojure?

hiredman18:07:39

You likely are mistaken

hiredman18:07:05

They do not use macros to validate their configuration and compile time

Nim Sadeh18:07:41

All I know is that when I pass in a nil hostname at build time I get a macroexpanding error, and when I pass in "" to some libraries it is silenced.

hiredman18:07:44

Clojure's compiler runs code as it loads and compiles it, and you have top-level forms that connect to external datasoury

hiredman18:07:53

When (def x (launch-missles)) is compiled launch-missles is called

Nim Sadeh18:07:35

So if I made my db specs a defn form instead of a def the compile errors should disappear?

hiredman18:07:36

Aot compiling in clojure is basically exactly the same as typing the code in repl, it just saves the resulting bytecode to disk too

hiredman18:07:49

Very likely

Nim Sadeh18:07:10

Isn't there a downside of evaluating my db connection every time I want to make a db call?

hiredman18:07:59

Usually you have a -main function, and in the main function you'll setup your connection pool and pass it down to anything that needs it

Nim Sadeh18:07:37

If I have a web server, is the typical pattern to use an atom to hold something like a db connection (to avoid having to thread it through every function) or to assoc it with the request using the middleware?

hiredman18:07:39

There are lifecycle management libraries that help with this kind of construction. My preference is very much for https://github.com/stuartsierra/component, but other people like others

hiredman18:07:18

Def atoms is bad

seancorfield18:07:47

> assoc it with the request using the middleware This is the preferred way.

šŸ‘ 1
hiredman18:07:22

There are other state management systems like mount which are basically the def'ed atom approach with extra steps, and I hate them

seancorfield18:07:08

I'm also a big fan of Component over other options: it's very small/lightweight and it's simple. I probably ought to update that example to use a connection pool instead of a single, long-lived Connection šŸ˜

Nim Sadeh18:07:49

I have to RTFM but may give it a try, thanks for sharing

Ben Sless19:07:11

Why do you prefer assoc-ing the connection to the request instead of closing over it in a stateful handler?

seancorfield19:07:49

@UK0810AQ2 I don't know what that would even look like in an app with a lot of handlers šŸ™‚

Ben Sless19:07:23

Guess - not that bad with extend-via-metadata, but your system map is going to be yuuuuuge

seancorfield19:07:47

We have one app where every handler is a Component and has just the dependencies it needs -- and that is a pretty huge, complex system map -- but our core Application Component only has about a dozen dependencies and the apps that assoc it into the request stay fairly simple because of that.

Ben Sless19:07:28

My feeling with all life cycle management libraries in Clojure is that Component is Good Enough while everything else is crazy, but missing some conveniences I need to get back to fleshing this library out, but I think with a big of programmability / composability you could go for the former option without having to manually type out a huge, complex system map https://github.com/bsless/companion

Ben Sless20:07:39

Hey, that little example inspired me to write companion

1
seancorfield20:07:27

Oh, I remember looking at companion and thinking "Ah, it still builds records of things under the hood..." and then puzzling over what all the qualify stuff was for. So I wasn't quite sure how to apply it to my use cases. If you do finish/document it, I'd take another look at it to see if it would simplify any of our code at work...

didibus21:07:06

I still advocate that you just use delay and if you need reloaded workflow, you can move to https://github.com/aroemers/redelay . At least when you are starting out. Component/Integrant are good as well, maybe once your app grows really large.

Nim Sadeh23:07:41

For component, how do I do hot reloading?

seancorfield00:07:40

You're asking about ClojureScript?

Nim Sadeh00:07:06

No, Clojure - if I make a change to parts of the app I have to stop and start

Nim Sadeh00:07:22

I refactored my app following the usermanager example (except I use Reitit)

seancorfield02:07:58

I run my app all day in my REPL and eval code as I change it. No need to stop/start anything.

seancorfield02:07:43

Can you tell why just eval'ing changes isn't enough for you?

Henning Jansen19:07:40

Hi Clojurians, I wonder how I can get the value from a sequence, returned from a function and append it to a :key as a value without the parentheses? Something in my fundamental understanding of Clojure is missing. I am trying to get a vaule from a XML element with zip-xml/text, and the result is :key ("value") instead of :key "value" I can get the :key "value" using (first (xxx)) , which is incredibly clumsy. My eyes are crossed after a looong week,.. what am I missing?

phronmophobic19:07:54

It depends on what you're trying to do. It looks like zip-xml/text returns a sequence because an xml node may have multiple text nodes as children. If you're interested in the first text node, then first seems like the right option. If you're interested in all the text nodes, you can combine them with clojure.string/join.

phronmophobic19:07:30

I'm not sure why first feels clumsy here.

Henning Jansen19:07:31

Hmm, maybe its not clumsy? I was initially surprised by this behavior. and I am trying to get towards an idiomatic solution.

phronmophobic19:07:41

It's possible I've just gotten used to the oddities. What do you think is the weird behavior? Is it that zip-xml/text returns a sequence rather than a string? or something else?

Henning Jansen19:07:32

I didn't consider the case where an XML element would return more than one single value, hence the no-need-for-a-sequence consideration.

Henning Jansen19:07:36

I would expect one string from an element. But then, I have not worked with this library before, and am trying to get used to it.

phronmophobic19:07:07

Actually, zip-xml/text seems to return a single string for me. Do you have some example code? The problem is probably somewhere else.

Henning Jansen19:07:37

:some-key (first (zip-xml/xml-> my-element :XmlElement zip-xml/text))

phronmophobic19:07:37

that's because of zip-xml/xml->, https://clojure.github.io/data.zip/#clojure.data.zip.xml/xml-%3E >

xml-> returns the final sequence.

phronmophobic19:07:11

maybe use xml1-> instead?

Henning Jansen19:07:43

Yes! šŸ™‚ That did it! Thank you.

Giu19:07:17

I want to start some little personal projects to continue my learning path, and some of them involves connecting to postgres. Where do you suggest to start diving to learn about how to interact with postgres, data modeling and so on?

Ben Sless19:07:58

Use: ā€¢ next.jdbc to connect to the DB ā€¢ honeysql to build queries Start a local container with postgres and start playing with it You'll see the "Just Use Maps" philosophy play nice with getting records in and out of a DB No ORM

Vincent21:07:06

aero/read-config giving me nil even though file in root directory

R.A. Porter21:07:17

When you say, "root directory", do you mean the root of your project or the root of one of your source/resource directories? Your config file needs to be in your classpath; if you're using tools.deps, for example, it should be located at the root of one of the directories listed in the :paths parameter.

Vincent23:07:31

I appreciate everyone's patient help, I was frustrated, and it turns out my error was unrelated to aero

Vincent21:07:05

Should config files need a lot of massaging to be effective additions to our apps?

Sam Ferrell22:07:37

I'm not sure what you mean by this

āž• 1
Mitch00:07:17

aero is like it's own language implemented with reader macros. imo it makes things way more complicated than they need be unless you're just using it to read environment variables

didibus04:07:12

Need some context? What is massaging a config file?

Vincent21:07:14

My vote is absolutely no and apparently I'm in the minority