Fork me on GitHub
#beginners
<
2020-05-08
>
Chris K00:05:03

How hard do you think is the shift from FP to OOP? I've heard many people say that OOP to FP is hard because to "unlearn" the things you already know, but I wasn't sure for the opposite

noisesmith00:05:52

that's kind of a subjective question, I think sometimes I have thought that OO is more intuitive than FP, even if it's less resilient

noisesmith00:05:54

there's also OCaml and Erlang which seem like good models of FP OO (and Clojure shares some design features with both)

potetm01:05:31

idk, but I do think I’m much better at OO after having done clojure for years

phronmophobic01:05:28

i also feel like i learned a lot about OO after learning clojure. for example, i had never heard the phrase “single dispatch” before.

Scott Starkey02:05:26

OK - I’m thinking game ideas and need random numbers. I tried this and didn’t get what I was expecting:

(rand-int 5)
=> 1
(rand-int 5)
=> 0
(repeat 10 (rand-int 5))
=> (4 4 4 4 4 4 4 4 4 4)
Do I need to seed the randomization somehow? Why am I always getting the same number from a rand-int?

potetm02:05:42

(repeat 10 4) is what was run

potetm02:05:12

I think repeatedly might be what you want.

Cameron02:05:20

(and to pull the first example off google:)

(take 10 (repeatedly #(rand-int 5)))

potetm02:05:21

(repeatedly 10 #(rand-int 5))

Cameron02:05:08

(where the former will give you an infinite lazy list with (repeatedly #(rant-int 5), useful if you're generalizing that and reusing it, such as with

(let [ powerball-numbers       (repeatedly #(rand-int 100))
       my-powerball-numbers    (take 10 powerball-numbers) ;; Clearly I don't play powerball
       their-powerball-numbers (take 10 powerball-numbers)]
  {:mine my-powerball-numbers 
   :theirs their-powerball-numbers})
and the latter will more directly, and concisely, give you the 10 numbers you need right away)

👍 1
noisesmith02:05:45

my-powerball-numbers and their-powerball-numbers are the same

Cameron02:05:47

They probably cheated

😄 1
Cameron02:05:06

That being said, I did not know it would not produce fresh values each time

noisesmith02:05:39

lazy-seqs are immutable and cached, reading the nth item of a given lazy-seq always returns the same value

Cameron02:05:27

I'm playing with it now, I see they work / produce fresh values when produced from separate function calls, but not when created in the same let

noisesmith02:05:43

if you call repeatedly again, you get a different lazy-seq, but if you reuse the same lazy-seq (the return of one call to repeatedly) you'll get the same values in the same order

Cameron03:05:25

ah yep I see it now

noisesmith03:05:31

so clearly what you need is (repeatedly (fn [] (repeatedly #(rand-int 5))))

noisesmith03:05:41

or maybe not :D

noisesmith03:05:17

a fun way to do your pass/fail above: #(rand-nth (into [] cat [(repeat 7 "Passed!") (repeat 3 "Failed!")])

noisesmith03:05:26

better to construct that vector outside the function or hard code it actually

Cameron03:05:45

personally, as an optimist, I'd like to go with (repeat 10 "Passed!")

noisesmith03:05:11

in that case you can just use (constantly "Passed!")

noisesmith03:05:40

oh never mind, if you use repeat you don't need constantly

Cameron02:05:04

I supposed also useful if you have more transformations to add there, to eventually create a stream generating something else entirely

(let [ ;; This stream produces random 'pass' 'fail' scenarios  
        grade-simulator (->> (repeatedly #(rand-int 100)) ;; Stream of random numbers 
                                          (map (fn [grade] 
                                            (if (> grade 70) :pass :fail))  ;; Oop, now a stream of 'pass' / 'fail' symbols
        ;; This stream produces random messages reflecting that scenario you might print out, like 'Passed!' 
        grade-message   (map (fn [pass-or-fail] 
                               (if (= pass-or-fail :pass) "Passed!" "Failed!"))
                                   grade-simulator)]
   (take 34 grade-message)) ;; "Passed!" "Failed!" "Passed!" "Passed"... 
but let me stop ahahah

David Pham06:05:54

I really like protocols, but the community seems to advise against using defrecord. Is there a way to conciliate the two tools?

didibus06:05:52

Well, it's not that you shouldn't use defrecord or protocols. Its more that you should only use them when they are really needed

didibus06:05:20

What people try to advise against is using defrecord and protocol as a means to replicate classes

didibus06:05:30

It's too easy with them to think of your protocol as methods that operate over the records, which is basically what a class/object is.

didibus06:05:31

So what people say is, try not to use them for that. Only use protocols when you need polymorphism, that means that you have two records that both share functionality and you want the type of the record to be used to choose which implementation of the protocol to use.

didibus06:05:56

Then you can use protocols and records.

didibus07:05:33

I'd suggest for now if you want to model some entity like say a person. Just do this:

(defn make-person
  [{:keys [name age height]}]
  {:name name
   :age age
   :height height})
 
(defn legal-age?
  [person]
  (> (:age person) 21))

hindol11:05:23

Protocols are also useful when you want users of your library to plug into a feature you expose. For example, a WebSocket library may expose a Sendable protocol and you can implement that for your own types to be able to send your custom type across.

David Pham17:05:19

Thanks a lot!

David Pham17:05:36

For me it was more that protocol allowed me to define operations on some kind collection that I know will be generic. For example: how can you implement the datafy protocol without record?

hindol17:05:57

Protocols need not be mixed with records though. You can even extend Java types with protocols using proxy, reify etc.

didibus18:05:34

Datafy should be implemented by things that are not already data, that means Java objects mostly. Maps are already data, and don't need to implement datafy

didibus18:05:13

But if you just used datafy as an example. When you need type polymorphism, you can use either multi-methods or protocols.

didibus18:05:28

Maps can now implement protocols using metadata. But it is also trivial to upgrade a map to a record when you need too, since they share the map interface, it won't break anything to convert your map to a record later when needed

didibus18:05:13

Most of your generic operations over domain entities can often be generic over maps. And if they can't, you can put metadata on the maps and have multi-methods or protocols over that. Or you can have a :type key on the map itself, and a multi-method over that. The advantage of the latter is that they are then trivial to serialize. But multi-method dispatch is a bit slower, so when you need generic dispatch inside a hot loop, you can switch to a record, and like I said, upgrading a map to a record is easy, so there's an easy route from simple map to more complex records.

didibus18:05:28

Using metadata protocol dispatch:

(defprotocol Walker
  :extend-via-metadata true
  (walk [this]))

(defn make-person
  [{:keys [name age height]}]
  (letfn [(walk [person]
            (update person :distance-walked inc))]
    ^{`walk walk}
    {:name name
     :age age
     :height height
     :distance-walked 0}))

(defn make-fast-person
  [{:keys [name age height]}]
  (letfn [(walk [person]
            (update person :distance-walked #(+ 10 %)))]
    ^{`walk walk}
    {:name name
     :age age
     :height height
     :distance-walked 0}))

(walk (make-person {:name "Bob" :age 23 :height "6.4"}))
;;=> {:name "Bob", :age 23, :height "6.4", :distance-walked 1}

(walk (make-fast-person {:name "Bob" :age 23 :height "6.4"}))
;;=> {:name "Bob", :age 23, :height "6.4", :distance-walked 10}

didibus18:05:27

Using multi-method:

(defmulti walk :type)

(defmethod walk :person
  [person]
  (update person :distance-walked inc))

(defmethod walk :fast-person
  [person]
  (update person :distance-walked #(+ 10 %)))

(defn make-person
  [{:keys [name age height]}]
  {:type :person
   :name name
   :age age
   :height height
   :distance-walked 0})

(defn make-fast-person
  [{:keys [name age height]}]
  {:type :fast-person
   :name name
   :age age
   :height height
   :distance-walked 0})

(walk (make-person {:name "Bob" :age 23 :height "6.4"}))
;;=> {:type :person, :name "Bob", :age 23, :height "6.4", :distance-walked 1}

(walk (make-fast-person {:name "Bob" :age 23 :height "6.4"}))
;;=> {:type :fast-person, :name "Bob", :age 23, :height "6.4", :distance-walked 10}

didibus18:05:59

Upgrading to a record for faster dispatch:

(defprotocol Walker
  (walk [this]))

(defrecord Person [name age height]
  Walker
  (walk [person]
    (update person :distance-walked inc)))

(defrecord FastPerson [name age height]
  Walker
  (walk [person]
    (update person :distance-walked #(+ 10 %))))

(defn make-person
  [{:keys [name age height]}]
  (map->Person
   {:name name
    :age age
    :height height
    :distance-walked 0}))

(defn make-fast-person
  [{:keys [name age height]}]
  (map->FastPerson
   {:name name
    :age age
    :height height
    :distance-walked 0}))

(walk (make-person {:name "Bob" :age 23 :height "6.4"}))
;;=> #user.Person{:name "Bob", :age 23, :height "6.4", :distance-walked 1}

(walk (make-fast-person {:name "Bob" :age 23 :height "6.4"}))
;;=> #user.FastPerson{:name "Bob", :age 23, :height "6.4", :distance-walked 10}

Amir Eldor14:05:37

Hello all, I have strange problem with records and tests (I can be considered a beginner). I have a namespace that defines a record (ags4.game.ship) and I have the test ns (ags4.game.ship-test). When I first run my tests in the test ns, stuff pass nicely. As soon as I added another test which failed, I suddenly get on subsequent test runs a:

Amir Eldor14:05:55

|| :cause "class ags4.game.ship.Ship cannot be cast to class clojure.lang.IFn (ags4.game.ship.Ship is in unnamed module of loader clojure.lang.DynamicClassLoader @54464330; clojure.lang.IFn is in unnamed module of loader 'app')"

Amir Eldor14:05:07

Does it ring any bells? Do you want more code?

Amir Eldor14:05:27

If I reload the regular ship namespace then I can run tests again without this weird error, but it gets back as soon as the new test code is run and fails.

noisesmith14:05:21

did you create a test, then delete the test and reload the file?

Amir Eldor14:05:28

As in deleted the deftest? I might have, let me restart the repl and try again

Amir Eldor14:05:42

If I delete a problematic test I might get this issue?

noisesmith14:05:04

the issue is that deleting the code from the ns doesn't delete the test

noisesmith14:05:37

if it was (deftest bad-test ...) that you needed to delete, you can fix it by running (def bad-test nil) in the same ns - that ensures the test is unregistered

noisesmith14:05:15

the error itself is saying that you had some instance of ship in parens, but it isn't a function

Amir Eldor14:05:56

ohhhhhhhhhhhh

Amir Eldor14:05:01

I know, Silly me.

Amir Eldor14:05:31

I had a (def ship ship) for debugging, probably inside the namespce that defined the (ship) creation function

noisesmith14:05:53

loosely translated "foo cannot be cast to IFn" is "why did you put a foo in parens, I dont' know how to call it?"

Amir Eldor14:05:56

thanks, and also thanks for the bad-test tip

noisesmith14:05:05

that wasn't talking about a function called ship, but rather an instance of a class called Ship

Amir Eldor15:05:00

In a test,

(:require [clojure.test :refer :all]
With ☝️ I get an "unable to resolve thrown?" at:
(is (not (thrown? java.lang.IllegalArgumentException
Ideas?

Amir Eldor15:05:17

I think it doesn't like the 'not'

noisesmith15:05:55

thrown? is not a function or macro, it's a special syntax of is

noisesmith15:05:54

all calls to is are implicitly assertions that no error is thrown - the is will fail and tell you about it if any throw reaches it

noisesmith15:05:10

thrown? is needed for the case where you need to ensure a specific throw happens

Amir Eldor15:05:09

hmmm ok, thanks again

alidlorenzo16:05:41

Do you all use tools.deps for a monorepo? If so, how? I tried setting it up using :local/root dep field, so I could require nested modules within my repo, but I think there’s a limitation (or bug) with tools.deps that doesn’t allow local deps to be within same git repo. (ref: https://github.com/bhauman/rebel-readline/issues/176#issuecomment-421860184)

alexmiller16:05:52

that bug was fixed I believe

alidlorenzo16:05:10

oh maybe it’s a shadow-cljs issue then? I was using it with tools.deps for front-end code

seancorfield16:05:02

@alidcastano We have a monorepo at work with about 30 subprojects. The key, for us, was having all the subprojects on the same "level" in the repo:

<repo>
  |--- build
  |       <various build scripts and utilities, configurations, etc>
  |--- clojure
  |       subproject-1
  |       subproject-2
  |       subproject-4
  +--- documentation
          <yes, we have a whole tree of documentation!>
We run clojure in one of the subprojects and each has a deps.edn with :local/root "../subproject-N" for dependencies on other subprojects.

👍 2
seancorfield16:05:32

(and we have a small wrapper script in build that automatically cd's to a subproject folder and then runs CLJ_CONFIG=../versions clojure -A:whatever ... -- where our versions subproject has a "master" deps.edn containing repo-wide aliases and pinned versions of dependencies etc, via :override-deps)

alidlorenzo16:05:52

@seancorfield thanks, that helps. Do you keep all deps in master deps.edn, or selectively put ones that are used across modules there?

seancorfield16:05:33

Each subproject lists the deps it actually needs, but mostly as group-id/artifact-id {} since the version comes from the master deps via :override-deps { ... group-id/artifact-id {:mvn/version "1.2.3"} ...}

seancorfield16:05:12

If a dep is only used by one subproject (rare but does happen) we will put the actual dep in the subproject deps.edn file instead of in the master overrides.

alidlorenzo16:05:12

ah interesting. so group-id/artifact-id {} is to explicitly state this dep is necessary for local module. I was wondering how to declare that, thanks

seancorfield16:05:43

We have an alias :defaults in the master deps file that brings in all the :override-deps -- and our wrapper script turns build alias subproject into cd /path/to/repo/clojure/subproject; CLJ_CONFIG=../versions clojure -A:defaults:alias

seancorfield16:05:18

Yes, each subproject still declares its own explicit dependencies, just with that empty version map.

seancorfield16:05:03

An example from our api subproject

{:deps
 {worldsingles {:local/root "../worldsingles"}
  worldsingles/datamapper {:local/root "../datamapper"}
  worldsingles/environment {:local/root "../environment"}
  worldsingles/lowlevel {:local/root "../lowlevel"}
  worldsingles/newrelic {:local/root "../newrelic"}
  worldsingles/redis {:local/root "../redis"}
  worldsingles/web {:local/root "../web"}
  worldsingles/worldsingles-web {:local/root "../worldsingles-web"}
  worldsingles/wsbilling-sdk {:local/root "../wsbilling-sdk"}
  worldsingles/presence {:local/root "../presence"}
  compojure {}
  date-clj {}}
 :paths ["src" "resources"]}

alidlorenzo16:05:45

this is really helpful, thank you

seancorfield16:05:30

And here's the relevant alias etc from our master deps file:

;; "pinned" versions for all cross-project dependencies
  :defaults
  {:override-deps
   {bidi {:mvn/version "2.1.6"}
    camel-snake-kebab {:mvn/version "0.4.1"}
    cfml-interop {:mvn/version "0.2.9"}
    cheshire {:mvn/version "5.8.1"}
    ...
    compojure {:mvn/version "1.6.1"}
    date-clj {:mvn/version "1.0.1"}
    hiccup {:mvn/version "1.0.5"}
    ...}

👍 2
Kamuela16:05:39

With CLJS, how can I map over the keys of an #object?

David Pham17:05:03

Can you transform the object into a Clojure hash map?

practicalli_john17:05:37

https://github.com/mfikes/cljs-bean is very efficient for converting object to Clojure

Kamuela11:05:14

Thank you!

Patrick Truong17:05:04

Hello all, I’m new to Clojure/ClojureScript and trying to render a list in Reagent. All the examples I’ve seen use (for [item items]) syntax. I was wondering if there is an idiomatic way to extract indices in a for loop since I want to conditionally render a CSS class after the first item. Thanks for all the help 🙂

noisesmith17:05:18

as an aside - for is a comprehension that makes a list out of a list, it isn't a loop per se

👍 1
Patrick Truong17:05:56

@U051SS2EU thanks for the clarification!

alexmiller17:05:13

you might want to look at map-indexed

👍 1
hindol17:05:31

for and map-indexed together worked really well for me in the past.

(for [[i v] (map-indexed vector [:0 :1 :2 :3])]
  [i v])

noisesmith17:05:23

this is identical to (map-indexed vector ...)

noisesmith17:05:55

but if you needed nested clauses, this form can help

hindol17:05:12

Yeah, that is for the example's sake, 🙂

noisesmith17:05:27

and I think in reagent code especially, for is idiomatic so having it there can help communicate intent

noisesmith17:05:41

so I don't need to read closely looking for forms that might result in lists inside the dom

hindol17:05:43

for is useful if OP wants a condition on i , like keep only the odd-indexed elements. I love the flexibility of for.

Jim Newton18:05:00

what is the correct way inside a function to assert that a given argument is of a certain type? I've changed the sematics of one of my functions and I want to run the tests assuring that the write values are being passed to certain functions. I think I've seen this done using meta data on the function declaration, but I didn't understand it exactly.

Babis Zinoviadis18:05:55

Would this work? Is there a more idiomatic way of writing this?

andy.fingerhut18:05:56

There are multiple ways. You could simply insert a call like (assert (map? arg1)) into the body of such a function.

andy.fingerhut18:05:50

You could use preconditions. Look for :pre on this page for some documentation on that approach: https://clojure.org/reference/special_forms#_fn_name_param_condition_map_expr_2

👍 1
Jim Newton18:05:57

Yes, but I've seen this done something like (defn [f^String y^Long] ....) .... That's not right, but it's vaguely what I remember.

andy.fingerhut18:05:59

You could use spec and instrument the function during testing.

andy.fingerhut18:05:37

That is a type hint, and does not check that the value is of that type. It gives a hint to the compiler that if there is a Java interop call involving one or more of those values, when the Clojure compiler is looking up what Java method to use, it can assume that those values are of those types, to narrow down the possibilities. Without that narrowing down, sometimes multiple Java methods are contenders, and run-time searching for the correct method is what the Clojure compiler produces (i.e. run-time reflection), which is hideously slow compared to not using run-time. reflection

✔️ 1
noisesmith18:05:32

there's also the potentially confusingly named cast, which takes an object and a class, and throws an error if the object isn't an instance of that class

noisesmith18:05:40

(ins)scratch=> (cast java.util.Collection [1 2 3])
[1 2 3]
(ins)scratch=> (cast java.util.Map {:a 0 :b 1})
{:a 0, :b 1}
(ins)scratch=> (cast java.util.Map [])
Execution error (ClassCastException) at java.lang.Class/cast (Class.java:3605).
Cannot cast clojure.lang.PersistentVector to java.util.Map

noisesmith18:05:11

it returns the object itself if the cast succeeds

ssushant20:05:35

what are some stand-out usecases of multimethods?

noisesmith20:05:53

they work quite well for pr (used for printing data in a way that's usually readable) - there's just one thing to implement, and it makes sense to extend it to things outside clojure core (new libraries or java builtins)

phronmophobic20:05:10

i’ve seen them used a couple times for event processing when your events are represented with maps: eg.

(def my-event {:type :foo :a 10})
(defmulti process-event :type)

ssushant20:05:27

That’s an interesting use case, any examples from open source projects?

phronmophobic20:05:55

not off the top of my head ¯\(ツ)

phronmophobic20:05:09

basically, in other languages, you create a bunch of new types and use the normal OO single dispatch. in clojure, it’s possible to use plain ol’ data and dispatch based on the data itself

Matthew22:05:07

@ssushant multimethods are good for any type of polymorphism. For example, write a compiler or interpreter in Java and your best shot at walking the AST is with the visitor pattern. In CLJ you can just dispatch with something like eval for every node type. It’s just eval A, eval B, eval C, all the way down. More commonly I use multimethods for extensible switch statement like logic. Think something like a turn based game where at every turn the outcome is dependent on a few different aspects of state. Instead of a giant switch statement with cases and nested if statements you can just dispatch based on state and extend the multimethod indefinitely without ever needing to touch the older code. Pretty sure multimethods are also runtime extensible but I’ve never tried it needed it.

Charles H22:05:12

so is emacs still the best way to get your cloj on? Anyone satisfied with vim plugins like vim-fireplace? Just wondering if I should really put in the time to get re-acquainted with Emacs.

dpsutton22:05:14

3/5 coworkers are vimmers at my work. emacs is great but an investment and not necessary

noisesmith22:05:45

vim fireplace works great, I switched from emacs+cider

noisesmith22:05:25

for a long time I just used a standalone repl, I've experimented lately with fireplace (plus a neovim "terminal" buffer with a real repl client in it)

Charles H22:05:59

ok cool I was enjoying fireplace for koans last night so I guess I'll stick with that. Thanks!

noisesmith22:05:22

I also recommend vim-clojure-static and vim-sexp

noisesmith22:05:50

but really, vim is already so good at structure based editing you don't need as much extensions for lisp as you do in emacs imho

Charles H22:05:26

but can you slurp and barf? 😆

noisesmith22:05:46

you get objects for ( and [ and { that all work as expected, plus " and all work with c/y/d and a/i

dpsutton22:05:11

> (plus a neovim "terminal" buffer with a real repl client in it) is this libterm? (libvterm?)

noisesmith22:05:25

it's a built in neovim buffer type

noisesmith22:05:59

oh look at that, yeah it uses libvterm

dpsutton22:05:24

speedy? i was using an emacs module built on that but it couldn't keep up with output of sql loggings

noisesmith22:05:41

@charleshu yeah vim-clojure-sexp introduces slurp / barf / convolute

😎 1
noisesmith22:05:33

@dpsutton I have only noticed it lagging once, and unlike in emacs control-c has never taken longer than 20 seconds at worst :P

dpsutton22:05:45

i feel attacked 🙂

dpsutton22:05:50

i am jealous of that

noisesmith22:05:53

but I haven't tried connected to a true firehose haha

noisesmith22:05:17

whether syntax highlighting is parsing each line makes a big difference

noisesmith22:05:26

being able to connect a new view to an existing editor process is awesome and I miss it

dpsutton22:05:09

i'm not a remote machine user really and the few times i have tramp was buggy on bad remote machine prompts it couldn't parse or gets terrible if you disconnect an ssh tunnel to another machine while there are buffers open in emacs

noisesmith22:05:33

sure, everything awesome about emacs has sharp edges at the corners