Core.match with guards on a map key / value - clojure

Is it possile to use core.match to do the following (silly example):
(defn summaries-film [film]
(match film
{:genre "Horror" :budget :guard #(< % 1000000) :star _} "Low budget horror"
{:genre "Comedy" :budget _ :star "Adam Sandler"} "Trash"
{:genre _ :budget _ :star "Emily Blunt"} "5 Stars"
:else "Some other film"))
??
I'd like to be able to pattern match over a map, but then have the :guard #(< % 10000) bit? i.e. have a function in the pattern based on the value of the key in the map?
Is this possible, I know I can do this over a vector, but can't work out the syntax or if its possible with maps.
I know I can use destructuring, but I'd like to know if it's possible with pattern matching.
Thanks

You can use guards with maps, though the syntax is different. Wrap the pattern in a list and append :guard your-guard-fn. The guard function will be invoked with the entire map, assuming the pattern matches otherwise:
({:foo 1} :guard #(= 1 (:foo %)))
Here's what it looks like with your example:
(defn summaries-film [film]
(match film
({:genre "Horror" :budget _ :star _} :guard #(< (:budget %) 100)) "Low budget horror"
{:genre "Comedy" :budget _ :star "Adam Sandler"} "Trash"
{:genre _ :budget _ :star "Emily Blunt"} "5 Stars"
:else "Some other film"))
(summaries-film {:genre "Horror" :budget 1 :star "Kelsey Grammer"})
;=> "Low budget horror"
(summaries-film {:genre "Horror" :budget 101 :star "Robert Forster"})
;=> "Some other film"

Related

Get list of values from vector of hashmap using filter or for

I'm quite new to clojure and have been struggling to understand how things work exactly. I have a vector of hashmaps as such, titled authors:
------ Authors -----------
[{:id 100, :name "Albert Einstein", :interest "Physics"}
{:id 200, :name "Alan Turing", :interest "Computer Science"}
{:id 300, :name "Jeff Dean", :interest "Programming"}]
I want to write a function that takes the id, and returns a list of the corresponding author names. I have two options for doing so: using filter or using for loop.
When using filter, I have a predicate function already that returns true if the author has matching id:
(defn check-by-id [author id]
(if (= id (:id author)) true false))
But I'm not sure how to use this in order to get the list of author names when passing the id.
Three other ways via keep, for and reduce:
(keep (fn [{:keys [id name]}] (when (= id 100) name)) authors)
;; => ("Albert Einstein")
(for [{:keys [id name]} authors
:when (= id 100)]
name)
;; => ("Albert Einstein")
(reduce (fn [v {:keys [id name]}]
(if (= id 100) (conj v name) v))
[]
authors)
;; => ["Albert Einstein"]
I prefer for (with :when) since it's shortest and in my eyes most clear. reduce I find best when you want to build a specific type of collection, this case a vector.
Filter will filter the list of maps. But the result is still a sequence of maps. You can map or reduce the result to get the list of authors.
(def authors [{:id 100, :name "Albert Einstein", :interest "Physics"}
{:id 100, :name "Richard Fynmann", :interest "Physics"}
{:id 200, :name "Alan Turing", :interest "Computer Science"}
{:id 300, :name "Jeff Dean", :interest "Programming"}])
(defn check-by-id [author id] (= id (:id author)))
(defn filter-ids [id col] (filter #(check-by-id % id) col))
(filter-ids 100 authors)
;; ↪ ({:id 100, :name "Albert Einstein", :interest "Physics"}
;; {:id 100, :name "Richard Fynmann", :interest "Physics"})
(map :name (filter-ids 100 authors))
;; ↪ ("Albert Einstein" "Richard Fynmann")
You can also use group-by for this task:
(def list-of-maps
[{:id 100, :name "Albert Einstein", :interest "Physics"}
{:id 200, :name "Alan Turing", :interest "Computer Science"}
{:id 300, :name "Jeff Dean", :interest "Programming"}])
(map :name (get (group-by :id list-of-maps) 100))
;; => ("Albert Einstein")

Using clojure.spec for a map

I have the following map:
(def gigs {:gig-01 {:id :gig-01
:title "Macaron"
:artist "Baher Khairy"
:desc "Sweet meringue-based rhythms with smooth and sweet injections of soul"
:img "https://res.cloudinary.com/schae/image/upload/f_auto,q_auto/v1519552695/giggin/baher-khairy-97645.jpg"
:price 1000
:sold-out false}
:gig-02 {:id :gig-02
:title "Stairs"
:artist "Brentr De Ranter"
:desc "Stairs to the highets peaks of music."
:img "https://res.cloudinary.com/schae/image/upload/f_auto,q_auto/v1519552695/giggin/brent-de-ranter-426248.jpg"
:price 2000
:sold-out false}})
I'd like to create a spec for it, but I'm not sure how to define the key e.g. ":gig-01" any pointers?
You could try:
(s/def ::gig-id
(s/and keyword?
(fn [x] (->> x name (re-matches #"gig-\d+")))))

manipulating an atom containing a ref collection in clojure

I have an application that is supposed to book flights for customers within their specified budget. As such I have customer data and available flights data. I then develop the solutions in Clojure as follows.
First, I create a flights atom:
(def flights
(atom []))
I then create a function to initialize flights into an atom containing a collection of refs. Here I pass flights data which is included further down this post.
(defn initialize-flights [initial-flights]
(reset! flights (map ref initial-flights)))
I then process customers through the process-customers function as follows. And this is where it gets really confusing.
(defn process-customers [customers]
(doseq [customer1 (partitionCustomerInput N-THREADS customers)]
(doseq [customer2 customer1]
(swap! flights
(fn [flights_collection]
(if-let [updated-flight (process-customer flights_collection customer2)]
(assoc flights (:id updated-flight) updated-flight)
flights_collection)))))
(reset! finished-processing? true))
Inside process-customers I pass flights-collection to process-customer (notice process-customer is a helper function for process-customers and they are not the same function). Flights-collection at this point is a collection of flight refs process-customer is supposed to search through the list and in case a customer qualifies for a flight therein, it uses the book function to edit the flight. How should I pass flights-collection to process-customer? As it is, process-customer does not search through the flight refs and it does not alter the flight refs either?
Below is the process-customer function followed by its helper functions.
(defn- process-customer [flights customer]
"Try to book a flight from `flights` for `customer`, returning the updated
flight if found, or nil if no suitable flight was found."
(if-let [{:keys [flight price]} (find-flight flights customer)]
(let [updated-flight (book flight price (:seats customer))]
(log "Customer" (:id customer) "booked" (:seats customer)
"seats on flight" (:id updated-flight) "at $" price " (< budget of $"
(:budget customer) ").")
updated-flight)
(do
(log "Customer" (:id customer) "did not find a flight.")
nil)))
(defn filter-pricing-with-n-seats [pricing seats]
"Get `pricing` for which there are at least `seats` empty seats available."
(filter #(>= (second %) seats) pricing))
(defn lowest-available-price [flight seats]
"Returns the lowest price in `flight` for which at least `seats` empty seats
are available, or nil if none found."
(-> (:pricing flight) ; [[price available taken]]
(filter-pricing-with-n-seats seats)
(sort-pricing)
(first) ; [price available taken]
(first))) ; price
(defn- find-flight [flights customer]
"Find a flight in `flights` that is on the route and within the budget of
`customer`. If a flight was found, returns {:flight flight :price price},
else returns nil."
(let [{:keys [_id from to seats budget]}
customer
flights-and-prices
; flights that are on the route and within budget, and their price
(for [f flights
:when (and (= (:from f) from) (= (:to f) to))
:let [lowest-price (lowest-available-price f seats)]
:when (and (some? lowest-price) (<= lowest-price budget))]
{:flight f :price lowest-price})
cheapest-flight-and-price
(first (sort-by :price flights-and-prices))]
cheapest-flight-and-price))
(defn- book [flight price seats]
"Updates `flight` to book `seats` at `price`."
(update flight :pricing
(fn [pricing]
(for [[p a t] pricing]
(if (= p price)
[p (- a seats) (+ t seats)]
[p a t])))))
(def finished-processing?
"Set to true once all customers have been processed, so that sales process
can end."
(atom false))
(defn partitionCustomerInput
[threads customers]
(let [partitions (partition-all
(Math/ceil (/ (count customers) threads)) customers)]
partitions))
Below is the main function. It initializes flights and kickstarts customer procecessing
(defn main []
(initialize-flights input/flights)
(let [f1 (future (time (process-customers input/customers)))
#f1
)
(println "Flights:")
(print-flights (map deref #flights)))
(main)
(shutdown-agents)
Below are the customers and flights collection.
(def flights
[{:id 0
:from "BRU" :to "ATL"
:carrier "Delta"
:pricing [[600 150 0] ; price; # seats available at that price; # seats taken at that price
[650 50 0]
[700 50 0]
[800 50 0]]}
{:id 1
:from "BRU" :to "LON"
:carrier "Brussels Airlines"
:pricing [[300 150 0]
[350 50 0]
[370 20 0]
[380 30 0]]}
{:id 2
:from "BRU" :to "LON"
:carrier "Brussels Airlines"
:pricing [[250 100 0]
[300 50 0]]}
{:id 3
:from "BRU" :to "MAD"
:carrier "Brussels Airlines"
:pricing [[200 150 0]
[250 50 0]
[300 100 0]]}
{:id 4
:from "BRU" :to "MAD"
:carrier "Iberia"
:pricing [[250 150 0]
[300 50 0]]}])
(def customers
[{:id 0 :from "BRU" :to "ATL" :seats 5 :budget 700}
{:id 1 :from "BRU" :to "ATL" :seats 5 :budget 550}
{:id 2 :from "BRU" :to "LON" :seats 6 :budget 270}
{:id 3 :from "BRU" :to "ATL" :seats 4 :budget 600}
{:id 4 :from "BRU" :to "LON" :seats 3 :budget 270}
{:id 5 :from "BRU" :to "LON" :seats 9 :budget 250}
{:id 6 :from "BRU" :to "MAD" :seats 5 :budget 200}
{:id 7 :from "BRU" :to "MAD" :seats 9 :budget 150}
{:id 8 :from "BRU" :to "LON" :seats 5 :budget 250}
{:id 9 :from "BRU" :to "ATL" :seats 4 :budget 500}
{:id 10 :from "BRU" :to "MAD" :seats 1 :budget 180}
{:id 11 :from "BRU" :to "LON" :seats 2 :budget 320}
{:id 12 :from "BRU" :to "ATL" :seats 3 :budget 850}
{:id 13 :from "BRU" :to "ATL" :seats 4 :budget 200}])
Also, note that I want to use refs for this implementation to alter the flights as ref offers support for coordinated read and writes to change the flights atomically. I aim to formulate a highly parallelized solution for this application and conflicts cannot be tolerated.
I think you need a ref instead of an atom at the top level. It seems that you will need to coordinate change to individual flight and change to the list of flight. What if one thread is modifying a flight while another thread removes it from the list? Your process-customer side effects must all be done within a (dosync).
Performance wise, it should be ok, because if you don't modify your list-of-flights ref in transaction, it will not cause other transaction that alters it to retry.
Another reason is because you are breaking a very important rule for swap!. The function passed to swap! must be free of side effects as it can be retried by the STM. Altering a ref is side effect, and may cause difficult to understand bugs.
So I would do something like
(def flights
(ref [(ref {:id "flight-1"})
(ref {:id "flight-2"})]))
;; Run a bunch of these in their own threads...
(doseq [customer partitioned-customers]
(dosync (process-customer customer flights)))
Then you can fine tune process-customer with alter, commute and ensure to maximize concurrency and minimize retries.
Hope this helps and good luck!

Clojure Macro using filter returns an object reference. Do not know how to interpret this reference

I am defining this macro
seminar.core=> (defmacro select
#_=> [vara _ coll _ wherearg _ orderarg]
#_=> `(filter ~wherearg))
#'seminar.core/select
And then defining a table
(def persons '({:id 1 :name "olle"} {:id 2 :name "anna"} {:id 3 :name
"isak"} {:id 4 :name "beatrice"}))
When I try to run my macro, so that I get the columns from the table where the id is greater than 2 (i.e {:id 3 :name "isak"} {:id 4 :name "beatrice"})
seminar.core=> (select [:id :name] from persons where [> :id 2] orderby :name)
I receive the following message and do not know quite how to interpret it
#object[clojure.core$filter$fn__4808 0x18e53c53 "clojure.core$filter$fn__4808#18e53c53"]
Update
I added a second argument to filter
seminar.core=> (defmacro select
#_=> [vara _ coll _ wherearg _ orderarg]
#_=> `(filter ~wherearg ~coll))
and receive IllegalArgumentException Key must be integer clojure.lang.APersistentVector.invoke (APersistentVector.java:292) as my return value now. I do not know how to interpret this error
When you use macroexpand-1 function to see the expanded form of macro it may give you a clue:
(macroexpand-1 '(select [:id :name] from persons where (> :id 2) orderby :name))
;;=> (clojure.core/filter [> :id 2] persons)
The form [> :id 2] isn't a valid function definition in Clojure. You have to pass proper function to filter, e.g. using anonymous function:
(select [:id :name] from persons where #(> (:id %) 2) orderby :name)
;;=> ({:id 3, :name "isak"} {:id 4, :name "beatrice"})

NOT - EXISTS / NOT - IN type query in Clojure

I have 2 data structures like the ones below
(ns test)
(def l
[{:name "Sean" :age 27}
{:name "Ross" :age 27}
{:name "Brian" :age 22}])
(def r
[{:owner "Sean" :item "Beer" }
{:owner "Sean" :item "Pizza"}
{:owner "Ross" :item "Computer"}
{:owner "Matt" :item "Bike"}])
I want to have get persons who dont own any item . (Brian in this case so [ {:name "Brian" :age 22}]
If this was SQL I would do left outer join or not exists but I not sure how to do this in clojure in more performant way.
While Chuck's solution is certainly the most sensible one, I find it interesting that it is possible to write a solution in terms of relational algebraic operators using clojure.set:
(require '[clojure.set :as set])
(set/difference (set l)
(set/project (set/join r l {:owner :name})
#{:name :age}))
; => #{{:name "Brian", :age 22}}
You basically want to do a filter on l, but negative. We could just not the condition, but the remove function already does this for us. So something like:
(let [owner-names (set (map :owner r))]
(remove #(owner-names (% :name)) l))
(I think it reads more nicely with the set, but if you want to avoid allocating the set, you can just do (remove (fn [person] (some #(= (% :owner) (person :name)) r)) l).)