Fork me on GitHub
#clojure
<
2023-01-24
>
didibus06:01:26

My brain is choking on finding a way to do this:

input: (1 2 :nest 3 4 :nest 5 6 7 :nest 8)

output: (1 2 (3 4 (5 6 7 (8))))
Write a function that takes above input and returns above output?

Ben Sless06:01:02

Put everything in a stack, build the sequence up as you pop it Or use recursion and build it up on return Remember recursive functions can do interesting things post call

didibus06:01:34

Erm, I was thinking of a stack, haha, but it's not coming to me. I'll think a bit more.

phronmophobic06:01:49

seems like zippers would allow a pretty straightforward implementation. you can use zip/edit + zip/rights when you encounter a :nest.

didibus06:01:56

I've actually never used zippers yet 😄

didibus06:01:19

Feel free to try it, this isn't some exercise question I'm looking for help with, but code I need for a macro I'm trying to write 😛 So I don't mind spoilers on the solution.

hiredman06:01:48

(cons 1 (cons 2 (cons :nest ...))) -> (cons 1 (cons 2 (cons ... nil)))

👍 2
Martin Půda07:01:32

(defn nest-seq [coll]
  (when (seq coll)
    (let [[f & r] coll]
      (if (= f :nest)
        (list (nest-seq r))
        (cons f (nest-seq r))))))

(nest-seq '(1 2 :nest 3 4 :nest 5 6 7 :nest 8))
=> (1 2 (3 4 (5 6 7 (8))))

nice 4
didibus07:01:37

Ok, also got the "imperative" one working:

(let [stack (atom '())
       new-list (atom '())
       input '(1 2 :nest 3 4 :nest 5 6 7 :nest 8)]
   (doseq [e input] (swap! stack conj e))
   (while (seq @stack)
     (let [e (ffirst (swap-vals! stack pop))]
       (if (not= :nest e)
         (swap! new-list #(cons e %))
         (do (swap! stack conj @new-list)
             (reset! new-list '())))))
   @new-list)

didibus07:01:49

Didn't manage to do it with zip/rights

phronmophobic07:01:59

(defn remove-rights [zip]
  (assoc-in zip [1 :r] nil))

(defn nest [xs]
  (loop [zip (z/zipper seqable? seq
                       #(do %2)
                       xs)]
    (if (z/end? zip)
      (z/root zip)
      (let [zip (if (= :nest (z/node zip))
                  (let [rights (or (z/rights zip)
                                   '())]
                    (-> zip
                        remove-rights
                        (z/replace rights)
                        z/next))
                  zip)]
        (recur (z/next zip))))))

👏 2
phronmophobic07:01:01

in comparison with the seq version, this will also work with existing nesting and trailing :nests:

> (nest '(1 2 (:nest 3 4) :nest 5 6 7 :nest 8  (:nest)))
;; (1 2 ((3 4)) (5 6 7 (8 (()))))

Martin Půda08:01:51

For completeness, recursive version for nested seq:

(defn nest-seq [coll]
  (if (empty? coll) '()
    (let [[f & r] coll]
      (cond
        (sequential? f) (cons (nest-seq f)
                              (nest-seq r))
        (= f :nest) (list (nest-seq r))
        :else (cons f (nest-seq r))))))

👏 2
arthurulacerda12:01:50

You could do it with reduce by inputting the reversed collection

(defn nest-seq
  [coll]
  (reduce (fn [acc curr]
            (if (= :nest curr)
              (list acc)
              (cons curr acc)))
          '()
          (reverse coll)))

(nest-seq '(1 2 :nest 3 4 :nest 5 6 7 :nest 8)) ;; => (1 2 (3 4 (5 6 7 (8))))

👏 2
didibus02:01:39

Ok, I think I got the hang of the zippers, pretty neat:

(let [zip (zip/seq-zip '(1 2 :nest 3 4 :nest 5 6 7 :nest 8))]
   (loop [zip (zip/down zip)]
     (cond (= (zip/node zip) :nest)
           (recur
            (zip/down
             (assoc-in
              (zip/edit zip #(rest (cons % (zip/rights zip))))
              [1 :r] nil)))
           (zip/right zip)
           (recur (zip/right zip))
           :else
           (zip/root zip))))

phronmophobic03:01:37

The other benefit of zippers is that they don’t consume the stack, so you can nest large structures without stack overflows

simongray08:01:42

good morning

Ben Sless08:01:31

Wrong channel?

Ben Lieberman15:01:42

is it possible to def a multimethod that dispatches on the result of a function call? e.g. (defmethod a-multimethod #(dispatch-f %) [v] (do-stuff))

delaguardo15:01:31

when dispatch-f should be called?

Ben Lieberman15:01:36

I would like it to be called when I pass in an argument to a-multimethod which I will update my code to reflect

Ben Lieberman15:01:04

But it doesn't seem to work that way, leading me to think that it's not possible.

delaguardo15:01:59

it looks like an example of case

(case (dispatch-f arg)
  "val1" (do-stuff)
  "val2" (do-stuff))

Ben Lieberman15:01:08

so I could use that case expr in place of #(dispatch-f %) here?

Darin Douglass15:01:13

is there a reason you can't use dispatch-fn on your defmutli call? are you trying to dispatch your defmethods on dynamic values?

Darin Douglass15:01:49

typically, how you'd set up multi-methods like this:

(defn dispatch
  [v]
  :foo)

(defmulti foo #'dispatch)
(defmethod foo :foo
  [v)
  (do-stuff))

Alex Miller (Clojure team)15:01:06

defmethods match on values. defmulti can use any function to generate those values.

Ben Lieberman15:01:14

so in essence, I have one multi-method that is conforming a map (one of a set of possible valid maps) to a spec, and then another multi-method that is giving me back only valid maps. I am concerned that this is a totally harebrained way to do it after thinking about it for awhile.

delaguardo15:01:45

> so I could use that case expr in place of #(dispatch-f %) here? instead of defmulti for a-multimethod you can use normal function that internally uses (case (dispatch-f arg) ,, expression

2
Ben Lieberman15:01:46

The problem I am running into is that conform returns either :clojure.spec.alpha/invalid or the conformed data, and I can't see how to def a method that works on my conformed data

Ben Lieberman15:01:53

the :invalid part is easy

p-himik16:01:28

The :default value? And you can provide your own default to defmulti.

Ben Lieberman16:01:34

Thanks @U2FRKM4TW. I looked at :default but ended up taking one "layer" out of my multi-method hierarchy and got it to work as expected.

👍 2
Volkmar Carrillo15:01:34

Hi guys I have doubts on how to convert a json file to a Clojure map using spec. I already have my spec structure defined and I have managed to read the json I need to convert. The challenge is to do the following (s/valid? invoice invoice) ; => true I don't know how to start getting there, i.e. should I read every object in my json file and convert it to a map, how would I implement my spec structure for that? Thank you very much for your help

Volkmar Carrillo15:01:57

invoice-spec.clj

jjttjj15:01:47

You need a step to transform the parsed json into the expected shapes you have spec'd. You can use the data.json library's key-fn conversion options to help with this: https://github.com/clojure/data.json#converting-keyvalue-types It might be easier if you use :req-un on your s/keys specs instead of :req so that you can just translate the key to the correct non-qualified key, so that you don't have to have context-awareness when you're doing this initial transformation.

borkdude15:01:04

Are you the same person who asked this on Reddit? There I recommended looking at e.g. https://github.com/exoscale/coax which allows you to coerce string values to other types via clojure.spec.alpha. The steps to follow here are: 1. Transform the JSON string to a Clojure data structure (with keywordized keys) 2. Then run that value through coax cc @U050SC7SV

4
jjttjj15:01:23

[@U04V15CAJ side note but for some reason I don't see your response on the reddit post] edit: nevermind there's one from yesterday and today

delaguardo15:01:20

there is also metosin/spec-tools that leverages coersion from json via spec https://github.com/metosin/spec-tools/blob/master/docs/01_coercion.md

borkdude15:01:13

Here is a REPL session in babashka:

$ rlwrap bb -Sdeps '{:deps {exoscale/coax {:mvn/version "1.0.0"}}}'

user=> (require '[clojure.spec.alpha :as s])
nil
user=> (s/def ::my-map (s/keys :req-un [::foo]))
:user/my-map
user=> (s/def ::foo int?)
:user/foo
user=> (require '[exoscale.coax :as c])
nil
user=> (c/coerce ::my-map {:foo "1"})
{:foo 1}
user=> (require '[cheshire.core :as json])
nil
user=> (json/parse-string "{\"foo\": \"1\"}" true)
{:foo "1"}
user=> (->> (json/parse-string "{\"foo\": \"1\"}" true) (c/coerce ::my-map))
{:foo 1}

mpenet15:01:22

yeah coax works well for this

mpenet15:01:50

not sure how maintained spec-tools is nowadays (no clue really, but since it's from the authors of malli maybe they moved on)

delaguardo15:01:33

spec-tools is also a part of well maintained reitit library

Volkmar Carrillo15:01:13

all this should be done in the same invoice-spec.clj file where I have the whole structure ready? Or I could call it from another file to perform the function I need, since my goal is to create a function that receives the name of the json file and return the map and then do the validation (s/valid?).

mpenet15:01:52

it's just a fn, call it from where-ever

Volkmar Carrillo15:01:23

I understand, so in order to do this I should import the invoice-spec file and require spec to be able to do it from another file?

borkdude15:01:22

(require '[order-namespace-with-spec :as order])
(s/valid? ::order/order ...)

borkdude15:01:49

or fully write: (s/valid? :order-namespace-with-spec/order ....)

borkdude15:01:05

as long as you load the namespace so the spec is defined. specs are defined by keywords in a global registry

borkdude15:01:24

Writing ::foo becomes :full-name-of-the-current-namespace/foo

Volkmar Carrillo15:01:18

Just to validate ::order corresponds to the spec structure order corresponds to the function?

borkdude15:01:35

You don't have to use ::foo , you can also write:

(s/def :my-foo/bar int?)
:: is just a notation to get auto-resolved keywords which either refer to the current namespace or another one via an alias

borkdude15:01:00

And a spec can be either a predicate or a complex object defined by any of the spec ops. A keyword is used to identity the spec by name.

Volkmar Carrillo15:01:59

Woow This is impressive, many doubts have been cleared up for me. Thank you very much for your support, I am new to Clojure and I appreciate your time.

👍 2
Volkmar Carrillo18:01:44

(ns problem_02
  (:require
    [clojure.data.json :as json]
    [clojure.spec.alpha :as s]
    [invoice-spec :as invoice]))

(defn invoice
  [name_json]
  (let [ file_json (json/read ( name_json))
         write_json (json/write-str file_json)]
    (json/read-str write_json :key-fn keyword)
    ))
I have created the following code Basically what I do is 1. Call the .json with Reader 2. I convert the json to a string using write-str 3. I read that string to be able to convert it to map thanks to key-fn I have revised the invoice-spec.clj structure by adjusting the field names to match the json structure and the data types When performing the validation I get false, does anyone know why this happens? Should I take more things into account?

borkdude18:01:54

@U04L0R4MVCN You can do (s/explain ::invoice/invoice ...) to see what's wrong

borkdude18:01:17

If you use https://github.com/bhb/expound you will it in a more human-readable way

Volkmar Carrillo18:01:51

I don't understand this error, the json file has these keys as well as invoice-spec.

Volkmar Carrillo20:01:52

I was checking the keys and actually key-fn did its job well, it should recognize the keys it says spec not recognize.