Fork me on GitHub
#beginners
<
2020-05-12
>
clintascencio09:05:51

sorry if this is a very cursed question, but I want to learn clojure (and functional programming in general) and wanted to do it by implementing algorithms. Is it possible to implement basic sorts (selection sort, etc,) in clojure in a functional way, instead of imperative? Or is that just very cursed way of learning/doing clojure/functional programming

andy.fingerhut09:05:29

Merge sort is very amenable to functional style implementation. Quicksort as well. What both have in common is that they can be thought of as dealing with manipulating sequences, and sub-sequences of the input sequence, versus mutating an array. Quicksort is also typically presented as how to implement it while mutating the elements of an array that holds the input sequence to be sorted. That can be done in Clojure in mutating style, too, if you wish, but it would be the traditional imperative "style" of implementation.

andy.fingerhut09:05:33

If you are hyper-concerned about performance, then I doubt you will get the best performance from most functional style implementations that avoid mutation. If you are interested because of learning functional style programming, and not mainly concerned with performance, then go for it.

timofey.sitnikov12:05:23

Clojure seems to encourage very loooong lines. I do like to keep my lines down to 80 columns, but when using if only a single expression per branch is allowed. How would you break up the last line of the block below? I am not smart enough to keep track of all that goes on in the last line

(if (empty? todays_migrations)
  (string/join "-" [tstr "001"])
  (string/join "-" [tstr (format "%03d" (+ 1 (apply max (map #(Integer/parseInt (subs % 11 14)) todays_migrations))))]))

vale12:05:01

you could indent it wildly

(if (empty? todays_migrations)
  (string/join "-" [tstr "001"])
  (string/join "-" [tstr
                    (format
                      "%03d"
                      (+ 1
                         (apply max
                           (map
                             #(Integer/parseInt (subs % 11 14))
                             todays_migrations))))]))

sogaiu12:05:20

or name the format portion

vale12:05:21

or just cut it up and make it pretty!

(defn max-migration
  [migrations]
  (->> migrations
       (map #(Integer/parseInt (subs % 11 14)))
       (apply max)))

(if (empty? todays_migrations)
  (string/join "-" [tstr "001"])
  (string/join "-" [tstr
                    (format "%03d" (inc (max-migration todays_migrations)))]))

sogaiu12:05:40

or start the whole thing with (string/join "-" [tstr and move/adjust the if portion appropriately to be right after tstr

sogaiu12:05:12

fwiw, i also try to stay within 80 columns -- one nice side-effect is that side-by-side windows comparisons are way easier

sogaiu12:05:35

...and i find it easier to "take in the view" at once

manutter5112:05:58

(defn extract-integer
  [s]
  (-> (subs s 11 14)
      Integer/parseInt))

(defn get-max-migration-number
  [coll]
  (max (map extract-integer) coll))

(defn format-migration-number
  [n]
  (format "%03d" n))

(if (empty? todays_migrations)
  (string/join "-" [tstr "001"])
  (string/join "-" [tstr (format-migration-number 
                          (inc (get-max-migration-number todays_migrations)))]))

manutter5112:05:20

This is my preferred style--break out the expressions into their own functions and give each function a descriptive name. This has the added advantage of making your code essentially self-documenting, without the use of comments.

sogaiu12:05:25

i like to go more in that direction once i'm happier that my code is likely to stay around.

noisesmith13:05:31

I never nest things as deeply as the original example, my first version of it would use let instead of nesting

(if (empty? todays_migrations)
  (string/join "-" [tstr "001"])
  (let [max-val (apply max
                       (map #(Integer/parseInt (subs % 11 14))
                            todays_migrations))]
    (string/join "-" [tstr (format "%03d" (inc max-val))])))

krzyzowiec17:05:56

I would say you really want to use let in order to establish some intermediary bindings, the way noisesmith shows with max-val. What this does is allow you to make sense of what you are doing by giving a descriptive identifier to it. You don't want to have to mentally parse that long line, so pull the transformation out and either name it as a function that you call, or put it in a let that appears earlier.

potetm13:05:24

(let [nxt-id (inc (apply max
                         (map (fn [s]
                                (Long/parseLong (subs s 11 14)))
                              todays_migrations)))]
  (if (empty? todays_migrations)
    (str/join "-"
              [tstr "001"])
    (str/join "-"
              [tstr
               (format "%03d" nxt-id)])))

potetm13:05:30

is what I’d recommend offhand

noisesmith13:05:44

I think calculating nxt-id might error if todays_migrations is empty (also see the thread above)

potetm13:05:55

tl;dr - pls don’t make long lines. In the worst case, it devolves to manual paren counting. Leverage indentation.

potetm13:05:16

@noisesmith yeah your solution is better

noisesmith13:05:41

we posted our let blocks at almost exactly the same time :D

potetm13:05:53

something something great minds 😉

noisesmith13:05:18

@potetm the error I would foresee:

scratch=> (apply max [])
Execution error (ArityException) at scratch/eval28493 (REPL:1).
Wrong number of args (0) passed to: clojure.core/max

potetm13:05:37

(let [nxt-id (inc (reduce max
                          0
                          (map (fn [s]
                                 (Long/parseLong (subs s 11 14)))
                               todays_migrations)))]
  (str/join "-"
            [tstr
             (format "%03d" nxt-id)]))

potetm13:05:15

@  My official recommendation 😄

timofey.sitnikov13:05:46

This is perfect, thank you

potetm13:05:35

FYI - If you get used to formatting things like this, you start to be able to glance at code w/o having to read every word. It’s really nice.

noisesmith13:05:40

most clojure functions behave well when used with apply and empty colls, max isn't one of them

potetm13:05:01

It looks like the min is actually zero (given the default is 001)

noisesmith13:05:58

there's also (apply max 0 coll)

potetm13:05:29

’tis but a word difference

potetm13:05:58

(and I think reduce is probably faster?)

potetm13:05:07

just random guessing, but it doesn’t have to re-allocate a seq

noisesmith13:05:41

yeah, max is implemented using reduce1 which is less optimized

noisesmith13:05:07

sometimes I think that people use some instinctual "line count = complexity" metric to end up with 20 forms nested on one line

noisesmith13:05:50

I think it's actually more like complexity = line-count^expressions-per-line

potetm13:05:23

I want to +1million that^

potetm13:05:49

You actually remove information when you pack everything on one line.

potetm13:05:23

When you properly lisp-indent things, your ability to scan increases at least 10-fold.

potetm13:05:50

*10-fold of what you might ask? idk, I just made that up 😛

ben.sless13:05:14

Enforce a strict 59 char line width, see all the hidden complexities pop up

noisesmith13:05:29

I think operations-per-line is a better metric than character count, but it's harder for an editor to show as a vertical bar

alexmiller13:05:17

just count the left parens :)

doby16213:05:30

Yeah, I prefer >80 chars per line to allow for really verbose var names

ben.sless13:05:00

I think Kevlin Henny put it well, "this line is so long it has a different weather system on the other end"

ben.sless13:05:31

And then it's compounded by arbitrary indentation, with all the arguments stacked in a column on the far right hand of the screen

potetm14:05:39

I know it’s not your point, but I wanna point out that this formatting isn’t arbitrary – it’s by design. Stacking arguments tells you something about what’s going on!

potetm14:05:33

Think bullet-points in a memo.

ben.sless14:05:27

What I meant by arbitrary about the indentation is the difference between these two:

(foo-bar-bazz-quux-controler-proxy-factory arg1
                                           arg2
                                           arg3
                                           arg4)


(foo-bar-bazz-quux-controler-proxy-factory
 arg1
 arg2
 arg3
 arg4)

ben.sless14:05:28

People align their code (information) in a terrible manner because it's facilitated by their tools (screen, IDEs), not because it's human readable or conveys information better. The second form is way more readable in my opinion

potetm14:05:58

uuuh, yeah in this case, I would seriously consider the second

potetm14:05:15

but the first is the default – most things should fall into that when the line isn’t too long

ben.sless14:05:41

Exactly. If I have to scroll right on my laptop screen in a maximized buffer only to see the arguments there has been a serious failure in the physical design of the code's layout, which makes me wonder what other weird ideas the programmer had. Which would you say you prefer, one of the first two or this third option?

(foo-bar-bazz-quux-controler-proxy-factory
 arg1 arg2 arg3 arg4)

potetm14:05:34

never the third

potetm14:05:48

either 1 or 2 – lean hard towards 2

potetm14:05:11

my least favorite clojure/lisp formatting is the “word cloud” formatting

ben.sless14:05:54

The biggest problem I find with the first, it being a default only making things sadder, is that variable names can also explode in size, making attention drift across the screen. Turns out we don't read in long lines, but in tight columns. Look at all the websites designed towards consuming text, they all lay it out in the center in a rather narrow column (facbook, twitter, blogging platforms, news sites, etc.)

ben.sless14:05:11

> word cloud At this point I'm afraid to ask 😅

potetm14:05:17

(defrecord TakeALotOfParams [arg1 arg2 arg3
                             other-arg1 other-arg2 arg5 arg6
                             my-arg my-other-arg1])

potetm14:05:53

Well, the upside of 1: it makes crazy things look crazy

potetm14:05:57

which, is good actualy

ben.sless14:05:08

I'm going to have nightmares about that word cloud

ben.sless14:05:56

"How to identify crazy things" sounds like an entire discipline

potetm14:05:31

it’s called “software engineering” 😛

potetm14:05:54

my whole life: “🤔 that looks crazy. Is it? …uh yeah.”

ben.sless13:05:55

"But it's okay on my 32'' IMac"

noisesmith14:05:20

I have a problem with crud GUIs where I frequently lose track of row identity in wide tables, my monitor is 4k but that's not going to help me read a line that's as wide as my screen

alidcastano19:05:28

Clojure converts collections to sequences but not vice-versa. To convert it back to desired collection (i.e. using into) does that mean you’d be iterating over the items twice? Not sure if there’s a special way conversions are handled under the hood, just wasn’t intuitive at first that map, for example, would return a sequence rather than original collection

smith.adriane19:05:36

for the special case of vectors, you can use mapv

smith.adriane19:05:23

there are several other options depending on what kind of tradeoffs you want for: • being concise • being fast • clarity • generality typically, if I want a vector result, I use mapv. if I want the result to be a map, I use something like (into {} (map f) my-map). edit: using @noisesmith ‘s suggestion

smith.adriane19:05:23

there are several other options depending on what kind of tradeoffs you want for: • being concise • being fast • clarity • generality typically, if I want a vector result, I use mapv. if I want the result to be a map, I use something like (into {} (map f) my-map). edit: using @noisesmith ‘s suggestion

noisesmith20:05:44

by moving the first ) over to the left, you can avoid creating a redundant lazy-seq

alidcastano19:05:29

that links is helpful, thanks! Reading Programming Clojure as we speak as well (which is where question came from) 🙂

watchtheblur20:05:38

I'm using a 3rd library function (feedparser_clj/parse-feed) to return (what looks like) a resulting hashmap. From reading the source, parse-feed returns a map from a defrecord map->SomeRecordObject. Doing (str resultingmap) gives me "[email protected]" but not the actual hashmap I want. If I evaluate resultingmap, I do get some output starting like this #feedparser_clj.core.feed{<hashmap of the content i want>} How do I just get the resulting hashmap?

noisesmith20:05:48

that object is a map, you can do most things to it that you can do to a map

noisesmith20:05:05

if you really need it to be a hash map, call (into {} resultingmap)

watchtheblur20:05:20

Is that object a record? I'm not familar with the #some.name syntax

noisesmith20:05:51

right, and records are a special category of maps - unlike maps they can have custom methods

noisesmith20:05:18

in general, in clojure #foo[...] #foo "..." #foo{... ...} etc. are custom readers, they consume the folowing thing (usually a hash-map, vector, or string) and produce a custom type

noisesmith20:05:56

@watchtheblur also if youcall (defrecord Foo [...] ...) clojure automatically creates the functions ->Foo and map->Foo as constructors for creating new instances

noisesmith20:05:18

(as well as defining a reader tag, and using that reader tag when displaying the data by default)

watchtheblur20:05:48

I was reading this page (https://clojure.org/reference/reader) to understand more about readers, but couldn't find the relevant section for custom readers. Not sure if I was on the right page?

noisesmith20:05:47

@watchtheblur see here https://clojure.org/reference/reader#tagged_literals - it doesn't mention the readers created when you define new records, but they work in a similar way

watchtheblur20:05:20

Thanks @noisesmith! I'll take a look. Much appreciated!

noisesmith20:05:32

@watchtheblur they are pretty simple once you learn a few rules

scratch=> (defrecord Foo [])
scratch.Foo
scratch=> (map->Foo {:a 0 :b 1})
#scratch.Foo{:a 0, :b 1}
scratch=> (:a (map->Foo {:a 0 :b 1}))
0
scratch=> #scratch.Foo{:x 2}
#scratch.Foo{:x 2}

watchtheblur20:05:41

It's just odd that (str (map->Foo {:a 0 :b 1})) would give the object reference rather than the map?

noisesmith20:05:56

str never returns a map

noisesmith20:05:00

str on a clojure hash-map happens to be the same as pr-str, for records that's not true, and if you don't implement toString you get clojure's default object->string printer

noisesmith20:05:23

pr-str is the method that tries to print a string that you could read and get something =

noisesmith20:05:55

scratch=> (str #scratch.Foo{:x 2})
"[email protected]"
scratch=> (pr-str #scratch.Foo{:x 2})
"#scratch.Foo{:x 2}"

watchtheblur20:05:10

I will keep that in mind going forward. Great tip!

noisesmith20:05:57

there are many cases where pr-str is much better than str - another classic example:

scratch=> (str (map inc (range 10)))
"[email protected]"
scratch=> (pr-str (map inc (range 10)))
"(1 2 3 4 5 6 7 8 9 10)"

watchtheblur20:05:08

Ah, I see that I should've gone further and searched for the default reader for the object I was using. pr and pr-str points out that they produce output that can be read by the reader

andy.fingerhut21:05:58

Is there any existing page that gives a quick comparison/description of the differences between Clojure's different printing/convert-to-a-string options?

andy.fingerhut21:05:29

e.g. a table that has a bunch of different types of values/objects in a column on the left, and separate columns showing what str , pr-str , etc. will do with them?

alexmiller22:05:59

no, and it's pretty complicated

alexmiller22:05:49

in general, "pr" things print as readable data, "print" things print for humans, "str" things make strings

alexmiller22:05:40

the reality (esp once you get into pprint) is way more subtle

alexmiller22:05:11

there's also not one answer for most of those due to the number of dynamic flags that can be in play

andy.fingerhut23:05:40

Understood. Having such a table, one that included some of the flags like *print-readably* would be nice as a quick reference. I may consider creating such a table and publish it some time. Those kinds of things I have never successfully committed to memory before.

alexmiller23:05:47

there are actually some docs written by Tom Faulhaber buried in the api docs

alexmiller23:05:46

these are very old, dating back to the original introduction of pprint and cl-format

alexmiller23:05:47

I'd love to have modern versions of such things to put on the site

seancorfield22:05:51

(probably best to direct any follow-up discussion to #jobs-discuss so it doesn't distract from beginners getting answers to their Clojure questions here)

jeroen.leenarts08:05:27

It’s not strictly a job discussion that’s underneath this link. What this image hides is the fact that the same survey discloses you need about 7+ years of experience to get any of these salaries. And also that females on average have fewer years of experience compared to males. So again, to me this confirms that there are too few women in technology work. I am hoping this gets better in the coming years.

jeroen.leenarts08:05:17

I specifically notices the gender related details due to the twitter account that mentioned this image. It is an account advocating women to work in tech.

dev.4openid08:05:10

As an advocate of more women being in Tech - esp. the UK this is interesting however agree with @ this for another forum please

orchidea.preziosa10:05:01

1st Learning a new language is gender free, so this post is for everyone. 2nd a beginner can be motivated, not to give up, also by the fact that this language open up higher paid jobs. So read it as a motivational post for beginners and not as job advert. This is why I have posted it here.