Skip to content

Instantly share code, notes, and snippets.

@sritchie
Last active December 22, 2021 23:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sritchie/810b898891aa6b9d0810ee0700645cf0 to your computer and use it in GitHub Desktop.
Save sritchie/810b898891aa6b9d0810ee0700645cf0 to your computer and use it in GitHub Desktop.
(ns tutorial.petageconvert)
;; woohoo, some comments!
;;
;; A note about `pet-multiplier`... for your keys, symbols are great, but
;; Clojure provides an additional idea of keywords over Scheme. Keywords like
;; `:dog` evaluate to themselves, so you don't have to quote them. Symbols are fine, but you have one less thing to remember (the quote).
(def pet-multipler
{'dog 7 'cat 5 'fish 10})
;; with keywords:
(def pet-multipler-keywords
{:dog 7
:cat 5
:fish 10})
(comment
;; Just for fun, notice that maps can sit in the function position. They take
;; a key and return either the value if the key exists, or nil.
(pet-multipler 'dog)
;; => 7
;; Another fun thing is that keywords and symbols both can act as functions.
;; If you pass them a map, they will try and look themselves up in the map:
('dog pet-multipler)
;; => 7
;; If you pass a second argument, it's returned if the key is not in the map.
('dogecoin pet-multipler "not-found")
;;=> "not-found"
(pet-multipler 'dogecoin "not-found")
;;=> "not-found"
)
;; This is good:
(defn human-age [pet age]
(* (get pet-multipler pet) age))
(comment
;; Notice that if `pet` is not in the map, this will fail, since `(* nil
;; <age>)` will not work. BUT you could decide that a good default age is `0`,
;; and write the function to returna default multiplier of 0:
(defn human-age [pet age]
(* (pet-multipler pet 0) age))
(human-age :cactus 12)
;; => 0
;; If multiplying by 0 was expensive, you could write the function differently
;; to skip the final multiplication in the case of the key not being found:
(defn human-age [pet age]
(if-let [mult (pet-multipler pet)]
(* mult age)
0))
;; or you could error in the <else> position, or take an `<on-failure>`
;; function argument if this was important enough that you wanted to let users
;; control the default.
;; now you are into good design areas.
)
;; @bfeld here is a challenge -
;; take two pairs of animal symbol and age,
;; and return the pair that is oldest
;; (after the conversion for the species)"
;; This is great, just one modification to suggest. Note that this is taking
;; FOUR arguments, basically the pet and age flattened down.
(defn compare-human-age
[pet1 age1 pet2 age2]
(if (> (human-age pet1 age1) (human-age pet2 age2))
[pet1 age1]
[pet2 age2]
)
)
(comment
;; You can use Clojure's "destructuring" to write basically the same function,
;; but take TWO vectors instead of four arguments, and rip them open right in
;; the function's argument vector (I'll say why in a moment):
(defn compare-human-age
;; spot the difference?
[[pet1 age1] [pet2 age2]]
(if (> (human-age pet1 age1)
(human-age pet2 age2))
[pet1 age1]
[pet2 age2]))
;; The advantage of this is that if you want to use `reduce`, you'll need a
;; function that can take a list of items, and pairwise combine them down
;; until there is just one left. So `compare-human-age` works naturally now to
;; find the max age of any number of animals. I will use the `& pairs` syntax
;; to allow any number of inputs. They all get stuffed into a list and bound
;; to `pairs`.
(defn max-human-age [& pairs]
(reduce compare-human-age pairs))
(max-human-age
['dog 12]
['fish 10]
['cat 3])
;; => ['fish 10]
;; this is a nice pattern. If you can compare 2 things, you can compare any
;; number.
)
@bradfeld
Copy link

@sritchie

When I change pet-multiplier to:

(defn human-age [pet age]
(* (pet-multiplier pet 0) age))

and then do:

(pet-multiplier :cactus 12)
=> 12

I expected 0.

Any idea what I'm doing wrong?

@bradfeld
Copy link

@sritchie Never mind. I figured it out. I was called pet-multiplier when I should have been calling human-age. Oops.

@bradfeld
Copy link

@sritchie Helpful learning on another front.

(pet-multiplier :dog 10)
=> 7

So, if pet-multiplier finds the "keyword" (e.g. dog), it returns 7, otherwise, it returns the extra param that I passed to pet-multiplier.

@sritchie Can you give me the lingo for this stuff?

Function: pet-multiplier
Keyword: :dog

?1: 10 (I know 10 isn't a param. What is it called?)

?2: (can you give me a definition of a "map") as you are using it. I know map is a function that does stuff across "a collection." And the definition of a collection is There are four key Clojure collection types: vectors, lists, sets, and maps. Of those four collection types, vectors and lists are ordered. So, map shows up in a thing that is a thing and can do a thing.

@bradfeld
Copy link

@sritchie Is there a way to reference in-line a block of code?

For this question, I want to ask you something about lines 53-58. The question is, "How would I determine if this was expensive?" Instinctively, it doesn't feel expensive and seems much more efficient than the if-let.

Ideally, I can just ask it by referring to the code somehow, but it's not obvious.

Alternatively, I would do the following:

Re:
;; If multiplying by 0 was expensive, you could write the function differently
;; to skip the final multiplication in the case of the key not being found:
(defn human-age [pet age]
(if-let [mult (pet-multipler pet)]
(* mult age)
0))

Question: How would I determine if multiplying by 0 is expensive? Intuitively, it doesn't feel like it should be.

@bradfeld
Copy link

@sritchie On max-human-age and compare-human-age, can I modify compare-human-age so it can take an infinite number of pairs without having to do the reduce thing? It seems like I could / should do that in the compare-human-age function. I don't really know about pairs yet so I may be missing something obvious.

@sritchie
Copy link
Author

On max-human-age and compare-human-age, can I modify compare-human-age so it can take an infinite number of pairs without having to do the reduce thing?

Yes, you can do it internally. Here is the pattern for this:

(defn compare-human-age
  ;; this function provides explicit versions for the 1-arity, 2-arity, and
  ;; 3-or-more-arity cases. You could provide a no-arity if there was a good
  ;; default here, but I think there is not!
  ([]
   ;;
   )
  ([pair] pair)
  ([[pet1 age1] [pet2 age2]]
   (if (> (human-age pet1 age1)
          (human-age pet2 age2))
     [pet1 age1]
     [pet2 age2]))
  ([x y & more]
   (let [init (compare-human-age x y)]
     (reduce compare-human-age init more))))

Then you can just call this with any number of pairs.

@sritchie
Copy link
Author

@sritchie Can you give me the lingo for this stuff?

?1: 10 (I know 10 isn't a param. What is it called?)

I would refer to :dog and 10 here as "arguments". params is fine too!

?2: (can you give me a definition of a "map") as you are using it. I know map is a function that does stuff across "a collection." And the definition of a collection is There are four key Clojure collection types: vectors, lists, sets, and maps. Of those four collection types, vectors and lists are ordered. So, map shows up in a thing that is a thing and can do a thing.

Good question. map as I was using it, as a noun, is the associative data structure. other languages say "hashmap" to distinguish these two cases you've pointed out, for good reason. (Java also calls it a HashMap or ArrayMap... but "map" comes from the math word for a "function", where you are creating a "mapping" between two sets of things. (Math people also will complain that what we have in programming are not "functions", but "procedures".

I got caught up in a memorable mess with a library that I called "Bijection", leading to this obnoxious issue: twitter/bijection#41

when you use it as a verb, to "map" a function across a sequence is to "push items from the sequence across the mathematical map that the function represents".

sicmutils.env> (map (fn [x] (* x x)) (range 10))
(0 1 4 9 16 25 36 49 64 81)

So it's an annoying terminology clash. I would say "hashmap" mentally for the data structure to keep them from clashing. Once it's all infused into your soul you might find yourself shortening hashmap to map.

@sritchie
Copy link
Author

To reference a block of code inline... ugh, it is not working here, BUT usually you can click the line number of the first line, then shift-click the NEXT line number to get a highlighted block. When you paste this link into Discord it should render the full snippet. Apparently not here though...

https://gist.github.com/sritchie/810b898891aa6b9d0810ee0700645cf0#file-notes-clj-L53-L58

Let me clarify what I meant by this comment, since I wasn't clear and it was, as a result, CONFUSING!

  ;; If multiplying by 0 was expensive, you could write the function differently
  ;; to skip the final multiplication in the case of the key not being found:
  (defn human-age [pet age]
    (if-let [mult (pet-multipler pet)]
      (* mult age)
      0))

I should have been clear that I was saying that this pattern of "maybe I want to skip some work if I can't lookup a value" is not really worth using here, since (* mult (pet-multipler pet 0) age) is pretty clear.

By "if...expensive" I meant, "if you see this pattern crop up when you are doing something where you might want to avoid work", like

  (defn human-age [pet age]
    (if-let [mult (pet-multipler pet)]
      (do-expensive-database-transaction-that-I-know-is-super-slow mult)
      0))

it can be better than (do-expensive-database-transaction-that-I-know-is-super-slow (pet-multipler pet 0)). "expensive" means "I KNOW that is takes a couple of seconds because it's some over-the-internet thing"... and if you don't know that something's expensive, just pretend it's not!

@sritchie
Copy link
Author

sritchie commented Dec 22, 2021

Notes from Discord:

There is a Cons cell! https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Cons.java#L22

@bfeld (cons a b) here is a function that will build SOMETHING that, when you call first on it, will return a, and when you call rest on it will return b.

so, just like Scheme. Clojure is built on a small number of data structure abstractions, as you noted in the gist. You can of course add more but if you add custom types, you often try hard to make them ACT like set, map, vector or a generic seq (sequence)
and sets, maps and vectors are sequences too (in the map case, a sequence of [k v] pairs), so sequence really is the ur-abstraction for sequential things.

sequences act like your mental model of lists, always, no matter what the data structure backing the sequence. So if you go make a new type and implement the "sequence" interface, you had better follow that convention. That is why (cons a vector) adds to the left.

OH, and it does not ONLY add to the left - it does not return a vector anymore! it returns a sequence. you no longer get to do random-access lookups, because you are now in sequence land.

The latter note is the problem. If you are using a vector explicitly, you probably want to STAY in vector land.

So conj is a more fine-grained "add to the collection in the most efficient way, but KEEP it the same type (don't convert to sequence).

vectors conj at the end because if they didn't, then the random-access lookup indices would all have to increment by one. (nth v 2) would change what it returns, and if you are adding things in by index and looking them up that would be a no-no. "sequences" like linked lists conj at the front because that is the most efficient thing for them.

cons adds to the beginning of the collection, as noted, but returns a seq, which I think can be whatever clojure deems the most efficient type for the action. (In my experience this is always a list)

This would be a LazySeq , or Cons, if you had an infinite sequence you were consing onto like:

sicmutils.env> (take 10 (cons 10 (range)))
(10 0 1 2 3 4 5 6 7 8)
sicmutils.env> (type (cons 10 (range)))
clojure.lang.Cons

@bfeld here is a design rule of thumb:

  • functions that act on generic sequences take the sequence LAST. map, reduce, mapcat, filter, etc all follow this. The bonus is you can use the ->> threading macro nicely here, like
(->> (range 10)
     (map square)
     (filter odd?)
     (reduce +))
;;=> 165
  • functions that take an explicit data structure take it FIRST, like assoc, conj, nth, get etc:
(-> {:k "vee"}
    (assoc :new-key "new-value")
    (dissoc :k))
;;=> {:new-key "new-value"}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment