Fork me on GitHub
#beginners
<
2023-02-02
>
Guild Navigator03:02:45

Getting a compile error on line 15 (the map call) No such var: data-xml/xml1->data:

(ns rss-parser.core
  (:require [clojure.xml :as xml]
            [clojure.data.xml :as data-xml]
            [ :as io])
  (:gen-class))

(def rss-url "")

(defn parse-rss []
  (with-open [rdr (io/reader rss-url)]
    (->> rdr
         xml/parse
         data-xml/xml-> data-xml/xml1
         (data-xml/xml1->data :channel :item)
         (map (fn [item] (data-xml/xml1->data item :title))))))

(defn -main
  []
  (println (parse-rss)))
What am I doing wrong?

hiredman03:02:52

I don't think any of the functions in clojure.data.xml you are trying to call actually exist

Alejandro08:02:43

Hello. Is there a naming convention for impure functions? Would be great to distinguish them when separating functional core from imperative shell when doing DDD. Or maybe it's easier just to put them in two adjacent namespaces? Which is not ideal tbh

Ben Sless08:02:31

A name which ends in ! is usually a good indicator

plus_one 4
Ferdinand Beyer09:02:30

If you are using DDD and want to keep your domain code pure, using different namespaces sounds like a great solution in my opinion!

Alejandro09:02:22

@UK0810AQ2, hm, I looked it up, and it's not entirely clear if exlamation mark is reserved for functions that mutate specific entities of STM, or it's ok to use them for all impure functions. https://github.com/bbatsov/clojure-style-guide#naming-unsafe-functions https://stackoverflow.com/questions/20606249/when-to-use-exclamation-mark-in-clojure-or-lisp

Alejandro09:02:28

@U922FGW59, the only thing that bothers me is that the closely related parts of the code get separated. Not a huge issue, since editors help with jumping back and forth, but still.

Ferdinand Beyer09:02:20

Concerning !, there is no standard. It is definitely not reserved for STM. People use it for different purposes. At a minimum, I would definitely expect a ! function to be impure, though.

Ferdinand Beyer09:02:25

I think it is difficult to achieve in practice, but writing the majority of your code in pure functions and to “glue it together” with impure ones sounds like a very nice architectural goal. When you can manifest that in namespaces, this might be a good idea, as it is easy to communicate that for example app.domain.* must only contain pure code. Then there should not be any “closely related parts” in different namespaces, as they are separated by an architectural boundary. Not saying you have to do it like this, but it does sound appealing to me. I used a similar approach by letting my domain code describe effects instead of performing them, remaining pure. “Application service” code in DDD parlour could then be impure, call the domain code and apply the effects, possibly in a transaction.

Ferdinand Beyer09:02:20

I used both, actually, so that a project.service.user namespace would have an add-user! impure function that writes to a DB, but delegates most of the logic to a pure function project.domain.user/create-user that checks invariants and describes what should be done (errors, DB writes, events).

pavlosmelissinos10:02:49

I prefer to use nouns for functions that just do pure data transformation and verbs to denote some interaction with a stateful thing (e.g. write to a database or a file). My inspirations are https://stuartsierra.com/2016/01/09/how-to-name-clojure-functions and https://leanpub.com/elementsofclojure/read_sample. ! was https://clojure.org/guides/weird_characters#_symbol_unsafe_operations but nowadays it's used a lot to say "hey, I do side-effects"

Alejandro10:02:23

@UEQPKG7HQ, thanks for mentioning nouns and verbs, I found this Stuart Sierra's blogpost on naming conventions: https://stuartsierra.com/2016/01/09/how-to-name-clojure-functions

👍 2
pavlosmelissinos10:02:30

I think I linked to that above, didn't I? 🙂 (but yeah, seems really good advice to me)

Alejandro10:02:47

@UEQPKG7HQ, oh, I missed that link haha. Yeah, it's probably a good practice. Somehow this blogpost didn't show up when I looked for clojure style guide.

🙃 2
pavlosmelissinos10:02:53

Check out the second link too, it's a chapter from Elements of Clojure which suggests a slightly different naming convention.

pavlosmelissinos10:02:54

but still talks about verbs vs nouns, which I like as an approach.

Alejandro10:02:46

@UEQPKG7HQ, ok, thanks, opened it up.

Alejandro10:02:58

@U922FGW59, yeah, structuring code is always hard, so why not make effort in this direction anyway.

pavlosmelissinos10:02:31

For what it's worth, it took a while until it clicked with me and it takes some discipline to be consistent (I'm not where I'd like to be). If you find it interesting, consider keeping it around to check it out again in a couple of days/weeks.

pavlosmelissinos10:02:48

I wouldn't adopt those any time soon to be honest 🙂 I might use metadata though for some of them (e.g. expensive computation) if I had to

Alejandro11:02:12

@UEQPKG7HQ, metadata? Interesting. How would it show up when those functions are used in code? Or it's purely for documentation?

pavlosmelissinos11:02:17

Mostly for documentation but some editors can be configured to show metadata when you inspect functions

pavlosmelissinos11:02:47

You can always do neat stuff from a REPL, like list all the known symbols, get their metadata as, well, data with (meta #'symbol) and filter by a particular entry

Alejandro11:02:03

@UEQPKG7HQ, that's a cool idea, it has to be documented and communicated though

👍 2
anovick09:02:47

Hello 🙂 I am trying to use Cursive IDE for the first time. I made a new project from a Leiningen template and following the instructions on the Cursive website, I looked for the context menu option to open the REPL but I can't find it. When I right click on project.clj file in the files menu I don't see an option for 'Run REPL for new-project'. is it a feature I'm missing with the community edition? thanks :hugging_face:

kennytilton10:02:32

It's weird. You have to start a "Run configuration". Is that in the doc? Then you get the Cursive tooling in menus. In the pic below, you will not have "local" as the current menu item since you have not created any. I forget what it says on a new project.

❤️ 2
kennytilton10:02:59

Then follow here. https://cursive-ide.com/userguide/repl.html Also, consider the #cursive channel for folks more fluent than I in Cursive.

anovick10:02:19

Hi @U0PUGPSFR thanks for the advice with "Edit Configurations" ! I think I was able to run the REPL with this menu

anovick10:02:49

I will consult with the Cursive channel now that I know about it, thanks

kennytilton11:02:37

Awesome. Now if you check the IntelliJ Tools menu on the app's main toolbar, you will see a "REPL" sub-menu with the clj logo, and I think other Cursive/CLJ-specific items sprinkled throughout that massive IDE. Happy hunting!

anovick11:02:18

Hey guys is there some function that prints all built-in functions in Clojure? I'm trying to learn about what's available so I can make use of it maybe something like

(list-fns)
or something similar 🙃

Martin Půda11:02:58

(vals (ns-publics 'clojure.core))

delaguardo11:02:59

(ns-map 'clojure-core) and (clojure.repl/source symbol)

delaguardo11:02:31

combine those functions and you can print almost entire namespace

anovick11:02:11

@U01RL1YV4P7 that's 671 results 😲

anovick11:02:12

oh nice cheatsheet 😁

delaguardo11:02:54

Btw, instead of printing, you can clone clojure source code and read it with help from your editor 🙂

2
anovick11:02:27

What do you mean @U04V4KLKC

anovick11:02:13

I already have clojure.core file available through Cursive

practicalli-johnny11:02:15

Many editors support function name completions. Rebel terminal UI also supports function completion, signatures and docs. I als wrote a little project to generate a random function to help me lear https://practical.li/clojure/simple-projects/random-clojure-function/

😮 2
anovick11:02:18

I think that's what you mean?

practicalli-johnny11:02:36

Or an internet search with Clojure topic can help find functions, usually including https://clojuredocs.org/ which also has a link to the source code of functions

delaguardo11:02:37

I mean clone this https://github.com/clojure/clojure together with java code and read source code with good code navigation. If you visiting clojure.core via cursive it will require you to type function name first and then go to its definition.

anovick11:02:06

@U04V4KLKC I'm not sure what that enables me to do?

anovick11:02:25

@U01RL1YV4P7 perhaps you know of a way to filter the sequence for only defn and not def or defmacro?

anovick11:02:45

oh I discovered there's a built-in function function?

anovick11:02:12

tried to do this:

(filter function? (vals (ns-publics 'clojure.core)))

anovick11:02:29

didn't really work

anovick11:02:31

Syntax error

delaguardo11:02:05

it allows you to avoid problems like that ^

anovick11:02:51

hmm I think the problem is I don't have function values but a data about the functions

anovick11:02:35

@U04V4KLKC what do you mean?

Martin Půda12:02:56

If you're starting with Clojure, I suggest you https://4clojure.oxal.org- it's better than aimless examining (ns-publics 'clojure.core) result.

anovick12:02:58

but I wanna know how to do this

anovick12:02:06

so it's not aimless

delaguardo12:02:42

You are trying to print and facing problems in getting what you want. And that is ok. You are learning. But instead of bashing the wall trying to achieve something you don't know yet, you could learn differently. The best advice I could give myself when I started with Clojure was to get Clojure source code and spend a week reading it through.

delaguardo12:02:48

sorry in advance for probably unwanted advice 🙏

anovick12:02:51

thanks @U04V4KLKC but I'm not asking for more advice... I'm trying to do this specific thing and you're being unhelpful towards that

kennytilton12:02:13

Foe my money, part of expert help is using experience and expertise to get past a noob question to a different question. I love the hardware store tradition, "What do you want that for?" Usually they have a better idea what we should have. Anyway, this is neat, The question about a list of functions reminded me of Common Lisp apropos. I wondered if anyone had ported that to Clojure...doh! https://clojuredocs.org/clojure.repl/apropos So add [clojure.repl :refer all] to your requires, reload, and:

(apropos "redu")
=>
(cljs.core/areduce
 clojure.core/areduce
 clojure.core/ensure-reduced
 clojure.core/reduce
 clojure.core/reduce-kv
 clojure.core/reduced
 clojure.core/reduced?
 clojure.core/reductions
 clojure.core/unreduced
 clojure.core.protocols/coll-reduce
 clojure.core.protocols/internal-reduce
 clojure.core.protocols/kv-reduce
 taoensso.encore/convey-reduced
 taoensso.encore/preserve-reduced
 taoensso.encore/reduce-indexed
 taoensso.encore/reduce-kvs
 taoensso.encore/reduce-n
 taoensso.encore/reduce-top)
hth.

2
Martin Půda12:02:21

@UDHL22ZFE You wanted to filter only functions from ns-public, that is something like this:

(filter #(and  (:arglists (meta %))
               (not (:macro (meta %))))
        (vals (ns-publics 'clojure.core)))

2
delaguardo12:02:57

here is more constructive suggestion:

(filter (comp (some-fn ifn? fn?) deref) (vals (ns-publics 'clojure.core)))
values in the map are vars instead of real objects so to get them you can call deref.

👍 2
2
anovick12:02:20

Cool suggestions... I will take a look 🙂

phill10:02:26

When getting started, I used to like to read the API docs page, A-Z, in sections, and follow the "Source" links when I got curious. https://clojure.github.io/clojure/clojure.core-api.html That page, for all its glories, did rather gave me the impression of a chaotic grab-bag of inane functions. I was very impressed to find the book by Emerick, Carper, & Grand, "Clojure Programming", which discerns, within that mass of functions, distinct libraries that cohere by their own patterns. The most glaring being the functions that thread by -> and those that thread by ->>, which all makes absolutely perfect sense when the book points the prism the right way; another example being the collections vs the lazy sequence functions.

Alejandro12:02:02

Can this code be rewritten in a way that there's no duplication of everything in arguments?

(defn check-login-ok
  ([form] (check-login-ok
           form                     ; argument
           get-managed-users        ; dependency
           get-current-manager))    ; dependenty
  ([form get-managed-users-fn get-current-manager-fn] ...))   ; full signature, can be used for testing
I could write a macro, but is there a better way? This is a DDD-style function definition with an explicit list of dependencies. I could also use partial for this, but this doesn't help with duplication.

pavlosmelissinos13:02:19

Where's the duplication? I don't see it

Alejandro13:02:05

@UEQPKG7HQ, for every dependency I have to write it's text twice: first, within the first signature, and then with -fn suffix in the full signature.

2
delaguardo13:02:11

(defn check-login-ok [form & {:or {get-managed-users get-managed-user
                                   get-current-manager get-current-manager}}]
  )

delaguardo13:02:47

when called like (check-login-ok form) will use defaults and for tests you can provide positional arguments to be used instead of defaults: (check-login-ok form :get-managed-user some-fn)

delaguardo13:02:28

you might also want to look at one of dependency injection library: integrant, component, mount.

Alejandro14:02:15

@U04V4KLKC, oh, this is much better, even though duplication is still there, but it's less of an issue now.

Alejandro14:02:54

@U04V4KLKC, yeah, those libraries are great, but how to do this kind of injection with them? Should I declare every function as a component?

delaguardo14:02:11

Depends on the library. Most of them emphasise the idea of having top-level components always dependent of something. For example your check-login-ok in integrant might look like this:

(defmethod ig/init-key ::check-login [_ {:keys [get-managed-users get-current-manager]}]
  (fn [form]
    ,,,))

(def config
  {::check-login {:get-managed-users (ig/ref ::users)
                  :get-current-manager (ig/ref ::manager)}
   ::manager {:db (ig/ref ::db)}
   ::users {:db (ig/ref ::db)}})

(def test-config
  {::check-login {:get-managed-users (fn [,,,] ,,,)
                  :get-current-manager (fn [,,,] ,,,)}})
there are two system configurations, one for production and one for testing. The former declares all parts of the system, and the later uses the stub function.

Alejandro15:02:52

@U04V4KLKC, cool, thanks for writing a concrete example. Do I get it right, that with integrant all the impure functions in this DDD-style are listed in the config along with the resources like db pool, etc?

delaguardo15:02:00

depends, a function that accepts db connection as an argument is impure but you can define it safely in some namespace and just use it from the component definition which takes care only of how resources are connected together.

Alejandro15:02:47

@U04V4KLKC, yeah, that's my concern. I'd like to have explicit dependencies of functions in the layer of domain logic. Integrant is great, but it's not for DDD. It's debatable if it's actually worth it, but I'd like to try.

Alejandro16:02:45

@U04V4KLKC, would you mind if I take your code snippet and ask a question in the #C52HVRVE1 chat?

didibus16:02:07

If you're trying to do DDD in Clojure, have a look at: https://github.com/didibus/clj-ddd-example

Alejandro17:02:35

@U0K064KQV, cool, thanks, it's a good intro. My concern is testing though, at the moment. I'm exploring options.

Ferdinand Beyer18:02:11

Do you know about with-redefs for testing?

Ferdinand Beyer18:02:29

> you might also want to look at one of dependency injection library: integrant, component, mount. There’s also https://github.com/ferdinand-beyer/init. Need to do more advertisement 🙂

Alejandro18:02:15

@U922FGW59, yeah, with-redefs is fine most of the time, just looking around. I'll try init, thanks.

Alejandro19:02:51

@U0K064KQV, I mean, I'm trying to figure out how to do what the author of the "Domain Modeling Made Functional" book does. Modeling with spec is the next thing I'm going to look into. Thanks for sharing.

Alejandro19:02:42

I'm not even sure now if explicitness is even worth it, to be honest.

didibus20:02:26

I don't know what that author does in his book, but you don't need to mock anything to test DDD, the domain service and domain model are already pure. Nothing needs to be mocked to test them. All you have to mock is when testing the application service, you can substitute the repository for a mocked repository. Or just have integ tests for them instead.

didibus21:02:02

It seems the author of the book you're reading seems to advocate that: "A function is still pure if it uses an impure function or object that is injected into it" And my guess is it's having you write a domain service that takes an impure function/component as argument and uses that. In my opinion, you shouldn't do that. Keep the function truly pure by writing it as:

(defn check-login-ok
  [form managed-users current-manager]
  ...)
Now in the application service, before calling the domain service function check-login-ok it has to use the repository to get the managed users and current manager:
(ns application-service
  (:require [repository :as repo]))

(def repository (delay (repo/get-repo (System/getEnv "ENVIRONMENT")))

(defn do-x
  [form username]
  (let [managed-users (repo/get-managed-user repository username)
        current-manager (repo/get-current-manager username)]
  (check-login-ok form managed-users current-manager)
  ...)

Alejandro07:02:14

Yeah, this is what I'm asking about. Great, thanks. So, if I use integrant or component and follow this style, I probably should have a protocol and a record that gets into the config, and I receive those functions from it like in your example above, do I get it right?

Alejandro07:02:26

@U0K064KQV, and one more question: if I want to have an domain-layer abstraction over a database, e.g., I have a document store, should I just rename the db component to document-store, or should I add another component that is called document-store and uses the db component?

didibus18:02:40

Hum, in my example I'm not using higher order functions. I'm passing the data the function needs to the function directly.

didibus18:02:46

For the repository, ya, you would use a Protocol, so you can have a Repository implementation that uses PostgressSQL, and another one that uses an in-memory ATOM.

didibus18:02:22

In my example I'm not using any component or integrant like, the "get-repo" function would maybe just be a switch/case that says if environment is test return the in-memory repository, else return the PostgressSQL one.

didibus18:02:41

You could use Component or Integrant instead for that part.

didibus18:02:52

Basically, you'd have a component for the database connection, and a component for the repository, the repository would need the database connection injected, Component/Integrant would manage that.

didibus18:02:22

But, the first idea of it all is that your domain services and domain models are all pure. So if you have something like: login-user Okay, you need to check if the username exists as a registered user, validate the password matches, generate a session id, track the session, and return success/failure. Now, you might think, this can't be pure, it's all side effects, but that's wrong, there's a ton of pure logic in there. Take a list of registered users and compare against the username given. Compare a password matches a salted hash. Generate a UUID. Etc. So you can make this a domain service:

(defn login-user
 [username password
  password-hash registered-users]
 (when
  (and
   (contains? registered-users username)
   (= (hash password) password-hash))
  (random-uuid)))
This is now a pure login-user function. You can choose to optimize it a bit by breaking purity a little and passing in higher order functions.
(defn login-user
  [username password
   password-hash user-exists-fn]
  (when
   (and
    (user-exists-fn username)
    (= (hash password) password-hash))
   (random-uuid)))

didibus18:02:46

But you can also decide to move out that behavior into the application service, and inject the result instead (which I prefer), like:

(defn login-user
   [username password
    password-hash user-exists?]
   (when
    (and
     user-exists?
     (= (hash password) password-hash))
    (random-uuid)))

didibus18:02:14

Now the app service would do:

(defn login-user
 [repository username password]
 (let
  [user (repo/get-user username)
   password-hash (:hash user)
   user-exists? (boolean user)]
  (dom-service/login-user username password password-hash user-exists?)))
To test the domain service login-user you don't need to mock anything. And to test the app-service you would just use a mocked repository. You could also use a "nil" as the repository and with-redef the repo/get-user function.

Alejandro18:02:14

@U0K064KQV, thanks for a detailed answer. It clicked for me after your code samples and this: > Basically, you'd have a component for the database connection, and a component for the repository, the repository would need the database connection injected I think I can start coding now without the feeling that I'm missing something.

2
didibus19:02:28

If you're used to other OOP languages, basically, everything that you'd make a Singleton, in Clojure that would be a Component, if using Component/Integrant.

didibus19:02:55

The difference is going to be more that, most of the processing after that uses immutable data-structures and variables. And in general, people will try way harder in Clojure, then in OOP, to separate all pure from impure behavior. So in Java you might also inject into User instances the DataAccess Singleton for example. Every-time you create a User, you give it the DataAccess component. Now you mutate the user instance, and finally do user.save() . This won't fly in Clojure. You'll need to introduce a third Component that takes a user and the DataAccess component, and saves the user using the DataAccess component. Something more like: (let [changed-user (assoc user :email new-email)] (save user-repo changed-user)) Here user-repo would be another Component where you injected the DataAccess into it for example.

Alejandro19:02:07

@U0K064KQV, well, I have some experience in clojure, I understand immutability and other values it brings, I'd just like to try this functional DDD-style of structuring code after reading Grokking Simplicity, Data-Oriented Programming, and Domain Modeling Made Functional. It's takes time to figure out details and to build discipline to write this way. I work with python and js at my day job, and the code is written chaotically at my company, as you can imagine haha

Alejandro19:02:44

@U0K064KQV, by the way, would you choose integrant or component to try first?

Alejandro19:02:14

It seems like it's an important decision as migrating between them is hard

Alejandro19:02:43

But I'm going to use web-kit, it uses integrant, so my choice is limited.

Alejandro19:02:27

An easy decision this time.

didibus19:02:03

Ya, they're both pretty good. Integrant is more opinionated, Component is kind of barebones, but more flexible.

upvote 2
metapredicate17:02:30

Hey, complete beginner. I want to try to achieve something the following Clojure psuedocode:

(defn start []
  (let [my-server (try (jetty/run-jetty #'app {:port 3000 :join? false})
                       (catch Exception e (prn "got an error when trying to start server:" e)))]
    (pprint/pprint my-server) ;; this is to check what the hell is returned!
    (if (:port my-server) ;; again this might not be called :port
      (format "server running in port %s" (:port my-server))
      (prn "couldn't start server. all the deets I have are: " my-server))))
Instead of (println "server running on port 3000"), I wanted to print the port that the server is using. I got a little bit more familiar with how to work with keywords, i.e. via the (:key map) or (get :key map) notation. I could do etc. (def conf {:port 3000 :join? false}) but the downside is that this would get taking the port number from the config, rather than what the server is actually using if that makes sense. Does anyone have any pointers?

dpsutton17:02:39

(let [server (jetty/run-jetty routes {:port 0 :join? false})
      port (.. server getURI getPort)]
  ...)

dpsutton17:02:24

passing a port of 0 means grab an available one so I have to check which one it bound to. If you pass in a port, it will use that one or it will throw an error. So you can trust the config in that sense. if it doesn’t blow up, it has used that port

👀 2
dpsutton17:02:19

internal-test=> (jetty/run-jetty (constantly {:status 200 :body "hi"}) {:port 3000 :join? false})
Execution error (BindException) at  (Net.java:-2).
Address already in use

dpsutton17:02:40

already have a webserver running on port 3000 so this fails with a helpful “Address already in use” error

dpsutton17:02:51

internal-test=> (let [server (jetty/run-jetty (constantly {:status 200 :body "hi"})
                                              {:port 0 :join? false})]
                  (println "server running on port" (.. server getURI getPort))
                  server)
server running on port 56660
#object[org.eclipse.jetty.server.Server
        "0x4cb299fd"
        "Server@4cb299fd{STARTED}[9.4.50.v20221201]"]
here start a server on a random port, print it out, and then in a terminal
❯ http get localhost:56660
HTTP/1.1 200 OK
Date: Thu, 02 Feb 2023 17:49:11 GMT
Server: Jetty(9.4.50.v20221201)
Transfer-Encoding: chunked

hi

dpsutton17:02:55

yeah. it’s really about the (.. server getURI getPort). I showed my use of 0 as a port number because that’s exactly why i had to get the port. But you can trust the config if you pass in a dedicated port because it will error out

🙌 2
anovick19:02:45

Does anyone know why this gives an error?

(ns playgrounds.core
  (:require [clojure.repl :refer all]))
Syntax error macroexpanding clojure.core/ns at (src/playgrounds/core.clj:1:1).
((:require [clojure.repl :refer all])) - failed: Extra input spec: :clojure.core.specs.alpha/ns-form

anovick19:02:49

omg it's Alex Miller :hugging_face:

❤️ 2
Alex Miller (Clojure team)19:02:51

omg, it's Amit Novick !

anovick19:02:38

😁 not the same scale of a celeb, but yea finally getting some recognition. will start working on my autographs

anovick19:02:20

loved reading your blog!

Stuart Nath19:02:15

Hello All, I have data that looks like this:

| :FieldName | :DimensionScheme | :SortRank |
|------------|------------------|----------:|
|     FAMILY |       FAMILY-SKU |         1 |
|     SKU_ID |       FAMILY-SKU |         2 |
This is in a http://tech.ml dataset format. I also can convert it to a map, which would look like this:
[{:FieldName "FAMILY", :DimensionScheme "FAMILY-SKU", :SortRank 1} 
 {:FieldName "SKU_ID", :DimensionScheme "FAMILY-SKU", :SortRank 2}]
Either way, I am trying to write a function that gives me the following output: {:DimensionScheme "FAMILY-SKU", :group-by-params ["FAMILY" "SKU_ID"]} With the following constraints: 1. If there are multiple DimensionSchemes in the map, I would like to have a row/map entry for each DimensionScheme. 2. The position of the :FieldName string within the group-by-params vector has to match the :SortRank in the input data. I've been trying things like apply str , into , reduce , but I can't seem to figure this out. Is there an idiomatic way to concatenate strings and put them into vectors by group, while maintaining sort order within the vector?

Martin Půda19:02:43

(def data [{:FieldName "FAMILY", :DimensionScheme "FAMILY-SKU", :SortRank 1}
           {:FieldName "SKU_ID", :DimensionScheme "FAMILY-SKU", :SortRank 2}])

(->> data
     (group-by :DimensionScheme)
     (mapv (fn [[k v]] {:DimensionScheme k
                        :group-by-params (->> v (sort-by :SortRank) (mapv :FieldName))})))

;=> [{:DimensionScheme "FAMILY-SKU", :group-by-params ["FAMILY" "SKU_ID"]}]

genmeblog07:02:27

tablecloth has a function for that: fold-by.

Stuart Nath18:02:14

@U1EP3BZ3Q thanks for the callout, I'll check that function out

genmeblog07:02:11

@U0458EQECB1

(def ds (tc/dataset [{:FieldName "FAMILY", :DimensionScheme "FAMILY-SKU", :SortRank 1} 
                   {:FieldName "SKU_ID", :DimensionScheme "FAMILY-SKU", :SortRank 2}]))

(tc/fold-by ds :DimensionScheme)
;; => _unnamed [1 3]:
;;    | :DimensionScheme |          :FieldName | :SortRank |
;;    |------------------|---------------------|-----------|
;;    |       FAMILY-SKU | ["FAMILY" "SKU_ID"] |     [1 2] |

2
👍 2