Clojure: Extract field from defrecord - clojure

I would like to extract the city field from the person1 the following snippet of code:
(defrecord Address [city state])
(defrecord Person [firstname lastname ^Address address])
(defn make-person ([fname lname city state]
(->Person fname lname (->Address city state))))
(def person1 (make-person "Mark" "Smith" "LA" "CA"))
Thanks,
R.

(prn (str (:state (:address person1))))

You can use defrecord like a map. The person1 record has the following structure:
{:address #ns.Address {:city "LA", :state "CA"},
:firstname "Mark",
:lastname "Smith"}
that's why you can access the field city by:
(:city (:address person1))
==> "LA"

Related

Clojure: Update value of record field

I have defined record to store User details and Address Details.
(defrecord User [id name address])
(defrecord Address [id location street city state])
(def usr (User. 1 "Abc"
(Address. 1 "Location 1" "Street" "NY" "US")))
I have updated "name" to "BCD" using the below code
(assoc usr :name "BCD")
Output:
#async_tea_party.core.User{:id 1, :name "BCD", :address #async_tea_party.core.Address{:id 1, :location "Location 1", :street "Street", :city "NY", :state "US"}}
(usr)
OutPut:
#async_tea_party.core.User{:id 1, :name "Abc", :address #async_tea_party.core.Address{:id 1, :location "Location 1", :street "Street", :city "NY", :state "US"}}
New value of name field has not updated and It still shows old value.
How can I update "name" field permanently in "User" record?
(def usr (User...)) is kind of immutable. You cannot change it.
When you do (assoc usr :name "BCD") you are not changing it. You create a new one. In order to do what you want you need an atom.
(def usr (atom (User. 1 "Abc"
(Address. 1 "Location 1" "Street" "NY" "US"))))
(:name #usr) ;; "Abc"
(swap! usr assoc :name "BCD")
(:name #usr) ;; "BCD"
This is called immutability and is one of the main reasons for me to like clojure so much.
To understand the reasoning why this behaviour is so beneficial, reading values and state really helped me

How to construct a query that matches exactly a vector of refs in DataScript?

Setup Consider the following DataScript database of films and cast, with data stolen from learndatalogtoday.org: the following code can be executed in a JVM/Clojure REPL or a ClojureScript REPL, as long as project.clj contains [datascript "0.15.0"] as a dependency.
(ns user
(:require [datascript.core :as d]))
(def data
[["First Blood" ["Sylvester Stallone" "Brian Dennehy" "Richard Crenna"]]
["Terminator 2: Judgment Day" ["Linda Hamilton" "Arnold Schwarzenegger" "Edward Furlong" "Robert Patrick"]]
["The Terminator" ["Arnold Schwarzenegger" "Linda Hamilton" "Michael Biehn"]]
["Rambo III" ["Richard Crenna" "Sylvester Stallone" "Marc de Jonge"]]
["Predator 2" ["Gary Busey" "Danny Glover" "Ruben Blades"]]
["Lethal Weapon" ["Gary Busey" "Mel Gibson" "Danny Glover"]]
["Lethal Weapon 2" ["Mel Gibson" "Joe Pesci" "Danny Glover"]]
["Lethal Weapon 3" ["Joe Pesci" "Danny Glover" "Mel Gibson"]]
["Alien" ["Tom Skerritt" "Veronica Cartwright" "Sigourney Weaver"]]
["Aliens" ["Carrie Henn" "Sigourney Weaver" "Michael Biehn"]]
["Die Hard" ["Alan Rickman" "Bruce Willis" "Alexander Godunov"]]
["Rambo: First Blood Part II" ["Richard Crenna" "Sylvester Stallone" "Charles Napier"]]
["Commando" ["Arnold Schwarzenegger" "Alyssa Milano" "Rae Dawn Chong"]]
["Mad Max 2" ["Bruce Spence" "Mel Gibson" "Michael Preston"]]
["Mad Max" ["Joanne Samuel" "Steve Bisley" "Mel Gibson"]]
["RoboCop" ["Nancy Allen" "Peter Weller" "Ronny Cox"]]
["Braveheart" ["Sophie Marceau" "Mel Gibson"]]
["Mad Max Beyond Thunderdome" ["Mel Gibson" "Tina Turner"]]
["Predator" ["Carl Weathers" "Elpidia Carrillo" "Arnold Schwarzenegger"]]
["Terminator 3: Rise of the Machines" ["Nick Stahl" "Arnold Schwarzenegger" "Claire Danes"]]])
(def conn (d/create-conn {:film/cast {:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many}
:film/name {:db/unique :db.unique/identity
:db/cardinality :db.cardinality/one}
:actor/name {:db/unique :db.unique/identity
:db/cardinality :db.cardinality/one}}))
(def all-datoms (mapcat (fn [[film actors]]
(into [{:film/name film}]
(map #(hash-map :actor/name %) actors)))
data))
(def all-relations (mapv (fn [[film actors]]
{:db/id [:film/name film]
:film/cast (mapv #(vector :actor/name %) actors)}) data))
(d/transact! conn all-datoms)
(d/transact! conn all-relations)
Description In a nutshell, there are two kinds of entities in this database—films and actors (word intended to be ungendered)—and three kinds of datoms:
film entity: :film/name (a unique string)
film entity: :film/cast (multiple refs)
actor entity: :actor/name (unique string)
Question I would like to construct a query which asks: which films have these N actors, and these N actors alone, appeared as the sole stars, for N>=2?
E.g., RoboCop starred Nancy Allen, Peter Weller, Ronny Cox, but no film starred solely the first two of these, Allen and Weller. Therefore, I would expect the following query to produce the empty set:
(d/q '[:find ?film-name
:where
[?film :film/name ?film-name]
[?film :film/cast ?actor-1]
[?film :film/cast ?actor-2]
[?actor-1 :actor/name "Nancy Allen"]
[?actor-2 :actor/name "Peter Weller"]]
#conn)
; => #{["RoboCop"]}
However, the query is flawed because I don't know how to express that any matches should exclude any actors who are not Allen or Weller—again, I want to find the movies where only Allen and Weller have collaborated without any other actors, so I want to adapt the above query to produce the empty set. How can I adjust this query to enforce this requirement?
Because DataScript doesn't have negation (as of May 2016), I don't believe that's possible with one static query in 'pure' Datalog.
My way to go would be:
build the query programmatically to add the N clauses that state that the cast must contain the N actors
Add a predicate function which, given a movie, the database, and the set of actors ids, uses the EAVT index to find if each movie has an actor that is not in the set.
Here's a basic implementation
(defn only-those-actors? [db movie actors]
(->> (datoms db :eavt movie :film/cast) seq
(every? (fn [[_ _ actor]]
(contains? actors actor)))
))
(defn find-movies-with-exact-cast [db actors-names]
(let [actors (set (d/q '[:find [?actor ...] :in $ [?name ...] ?only-those-actors :where
[?actor :actor/name ?name]]
db actors-names))
query {:find '[[?movie ...]]
:in '[$ ?actors ?db]
:where
(concat
(for [actor actors]
['?movie :film/cast actor])
[['(only-those-actors? ?db ?movie ?actors)]])}]
(d/q query db actors db only-those-actors?)))
You can use predicate fun and d/entity together for filtering datoms by :film/cast field of an entity. This approach looks much more straightforward until Datascript doesn't support negation (not operator and so on).
Look at the row (= a (:age (d/entity db e)) in the test case of the Datascript here
[{:db/id 1 :name "Ivan" :age 10}
{:db/id 2 :name "Ivan" :age 20}
{:db/id 3 :name "Oleg" :age 10}
{:db/id 4 :name "Oleg" :age 20}]
...
(let [pred (fn [db e a]
(= a (:age (d/entity db e))))]
(is (= (q/q '[:find ?e
:in $ ?pred
:where [?e :age ?a]
[(?pred $ ?e 10)]]
db pred)
#{[1] [3]})))))
In your case, the predicate body could look something like this
(clojure.set/subset? actors (:film/cast (d/entity db e))
In regards to performance, the d/entity call is fast because it is a lookup by index.

simplest way to add a type to a map in clojure

I'm searching for the lightest way to add a type to a clojure map
I know i could use records for doing that but i'm wondering if i can do it with regular maps
i've got a function to create a map that represent a person:
(defn person [first-name last-name] {:first-name first-name :last-name last-name})
i would like to be able to do something like that:
(def wayne (person "Wayne" "Shorter"))
....
(type wayne)
=> person
You could attach some metadata:
(defn person [first-name last-name]
(with-meta {:first-name first-name :last-name last-name}
{:type 'Person}))
(def wayne (person "Wayne" "Shorter"))
(type wayne) ;; Person

Repetitive structure in Clojure

It is said in the book "Web Development with Clojure" that the code
(defn registration-page []
(layout/common
(form-to [:post "/register"]
(label "id" "screen name")
(text-field "id")
[:br]
(label "pass" "password")
(password-field "pass")
[:br]
(label "pass1" "retype password")
(password-field "pass1")
[:br]
(submit-button "create account"))))
can be rewritten using a helper function as following:
(defn control [field name text]
(list (on-error name format-error)
(label name text)
(field name)
[:br]))
(defn registration-page []
(layout/common
(form-to [:post "/register"]
(control text-field :id "screen name")
(control password-field :pass "Password")
(control password-field :pass1 "Retype Password")
(submit-button "Create Account"))))
My question is: In the alternative code, why the value of the parameter name is not a string? For example, why is it (control text-field :id "screen name"), not (control text-field "id" "screen name") ?
I'm not familiar with Hiccup and I don't have the book you mentioned. But by reading Hiccup source code, you can find:
Label is calling make-id function which it calls as-str. Have a look at that function and see what it is doing.
(defn ^String as-str
"Converts its arguments into a string using to-str."
[& xs]
(apply str (map to-str xs)))
That will leads you to ToString protocol.
Pass strings instead of keywords in the snippet you posted and see what is happening!
Source code is the best documentation we can have!

how to get this working in webnoir

I am trying to do this in webnoir.
This works:
(defpage [:post "/testurl] {:keys [name phone]}
(html5
(str "name: " name)
(str "phone: " phone)))
Now I want to generate defpages for many modules, each has a list of different fields. And I want to call the defpages from a function. The defpage must accept post for the fields.
Basically I have this: (def fields1 ["Name" "Phone" "Email" "xyz"])
And I would like to pass this to defpage, instead of having to specify the keys manually.
The fields might change in the future and that's why I want my code to pick up the fields and create the defpages dynamically on server startup.
Is it possible?
Thank you for all your help!
You can do this with a macro:
(defmacro defpages [pages]
`(do
~#(map (fn [page]
`(~'defpage [:post ~(str "/" (page :name))]
{:keys ~(into [] (map symbol (page :fields)))}
(~'html5
~#(map (fn [field]
`(str ~(str field ": ")
~(symbol field)))
(page :fields))))) pages)))
(defpages [{:name "testurl"
:fields ["name" "phone"]}
{:name "user"
:fields ["age" "address"]}])