How can I create a set from a defrecord? - clojure

I have:
(defrecord human-being [uuid first-name last-name genome-sequence])
(defrecord space-ship [uuid ship-type engines home-world captain])
I want to have a single 'construction' function that will take records of either human-being or space-ship and return a set with the keys as the actual record keys:
(def john (human-being. "ABC123" "John" "Smith" "QWERTY"))
(def enterprise (space-ship. "ZXC123" "Galactic" "Warp" "Earth" "Picard"))
(constructFunc john) --returns--> {:uuid "ABC123" :first-name "John" :last-name "Smith" :genome-sequence "QWERTY"}
(constructFunc enterprise) --returns--> {:uuid "ZXC123" :ship-type "Galactic" :engines "Warp" :home-world "Earth" :captain "Picard"}
I don't just want these two defrecords. I need to be able to drop any defrecords in and get similar output...
I have a feeling I should be using macros for this, but that scares me somewhat....

Seeing as the question is essantialy "how do I turn a Defrecord into a map" I can think of 4 more or less equivalent ways.
; direct - but this is stricly speaking not 'a function'
(into {} john)
; anonymous
( #(into {} %) john )
; named
(defn ->map [r]
(into {} r))
(->map john)
; via composition/partial
( (partial into {}) john)
; all of them return
{:uuid "ABC123", :first-name "John", :last-name "Smith", :genome-sequence "QWERTY"}

Related

Clojure: set value as a key

May be, it is a stupid question, but it may help many of newbies. How do I add a key-value pair to the map?
I mean something like:
(defn init-item [v item]
(let [{:keys [id value]} item]
(-> v
(assoc :{ID_AS_A_KEY} value))))
And I get:
(init-item {} {:id "123456789" :value [:name "King" :surname "Leonid"]})
user=> {:123456789 [:name "King" :surname "Leonid"]}
Just don't do it. Use the string itself as your map key. There's no reason to make it a keyword. It's much easier to work with if you leave it alone.
(defn init-item [v item]
(assoc v (:id item) (:value item)))
I think this is what you meant to do:
(defn init-item
[dest-map item]
(let [item-id-str (:id item)
item-val (:value item)
item-id-kw (keyword item-id-str)]
(assoc dest-map item-id-kw item-val)))
(let [all-items {:a 1 :b 2 :c 3}
item-1 {:id "123456789"
:value [:name "King" :surname "Leonid"]}]
(init-item all-items item-1)
;=> {:a 1, :b 2, :c 3, :123456789 [:name "King" :surname "Leonid"]}
Clojure has functions name, symbol, and keyword to convert between strings and symbols/keywords. Since you already have the ID as a string, you just need to call keyword to convert it.
Be sure to always keep a browser tab open to The Clojure CheatSheet.

Converting string to nested map in Clojure

I have a file containing some text like:
1|apple|sweet
2|coffee|bitter
3|gitpush|relief
I want to work with this input using a map. In Java or Python, I would have made a nested map like:
{1: {thing: apple, taste: sweet},
2: {thing: coffee, taste: bitter},
3: {thing: gitpush, taste: relief}}
Or even a list inside the map like:
{1: [apple, sweet],
2: [coffee, bitter],
3: [grape, sour]}
The end goal is to access the last two column's data efficiently using the first column as the key.
I want to do this in Clojure and I am new to it. So far, I have succeeded in creating a list of map using the following code:
(def cust_map (map (fn [[id name taste]]
(hash-map :id (Integer/parseInt id)
:name name
:taste taste ))
(map #(str/split % #"\|") (line-seq (clojure.java.io/reader path)))))
And I get this, but it's not what I want.
({1, apple, sweet},
{2, coffee, bitter},
{3, gitpush, relief})
It would be nice if you can show me how to do the most efficient of, or both nested map and list inside map in Clojure. Thanks!
When you build a map with hash-map, the arguments are alternative keys and values. For example:
(hash-map :a 0 :b 1)
=> {:b 1, :a 0}
From what I understand, you want to have a unique key, the integer, which maps to a compound object, a map:
(hash-map 0 {:thing "apple" :taste "sweet"})
Also, you do not want to call map, which would result in a sequence of maps. You want to have a single hash-map being built.
Try using reduce:
(reduce (fn [map [id name taste]]
(merge map
(hash-map (Integer/parseInt id)
{:name name :taste taste})))
{}
'(("1" "b" "c")
("2" "d" "e")))
--- edit
Here is the full test program:
(import '(java.io BufferedReader StringReader))
(def test-input (line-seq
(BufferedReader.
(StringReader.
"1|John Smith|123 Here Street|456-4567
2|Sue Jones|43 Rose Court Street|345-7867
3|Fan Yuhong|165 Happy Lane|345-4533"))))
(def a-map
(reduce
(fn [map [id name address phone]]
(merge map
(hash-map (Integer/parseInt id)
{:name name :address address :phone phone})))
{}
(map #(clojure.string/split % #"\|") test-input)))
a-map
=> {1 {:name "John Smith", :address "123 Here Street", :phone "456-4567"}, 2 {:name "Sue Jones", :address "43 Rose Court Street", :phone "345-7867"}, 3 {:name "Fan Yuhong", :address "165 Happy Lane", :phone "345-4533"}}
I agree with #coredump that this is not concise, yet a quick solution to your code is using a list (or any other collection) and a nested map:
(def cust_map (map (fn [[id name taste]]
(list (Integer/parseInt id)
(hash-map :name name
:taste taste)))
(map #(clojure.string/split % #"\|") (line-seq (clojure.java.io/reader path)))))
This may be a somewhat naive view on my part, as I'm not all that experienced with Clojure, but any time I want to make a map from a collection I immediately think of zipmap:
(require '[clojure.java.io :as io :refer [reader]])
(defn lines-from [fname]
(line-seq (io/reader fname)))
(defn nested-map [fname re keys]
"fname : full path and filename to the input file
re : regular expression used to split file lines into columns
keys : sequence of keys for the trailing columns in each line. The first column
of each line is assumed to be the line ID"
(let [lines (lines-from fname)
line-cols (map #(clojure.string/split % re) lines) ; (["1" "apple" "sweet"] ["2" "coffee" "bitter"] ["3" "gitpush" "relief"])
ids (map #(Integer/parseInt (first %)) line-cols) ; (1 2 3)
rest-cols (map rest line-cols) ; (("apple" "sweet") ("coffee" "bitter") ("gitpush" "relief"))
rest-maps (map #(zipmap keys %) rest-cols)] ; ({:thing "apple", :taste "sweet"} {:thing "coffee", :taste "bitter"} {:thing "gitpush", :taste "relief"})
(zipmap ids rest-maps)))
(nested-map "C:/Users/whatever/q50663848.txt" #"\|" [:thing :taste])
produces
{1 {:thing "apple", :taste "sweet"}, 2 {:thing "coffee", :taste "bitter"}, 3 {:thing "gitpush", :taste "relief"}}
I've shown the intermediate results of each step in the let block as a comment so you can see what's going on. I've also tossed in lines-from, which is just my thin wrapper around line-seq to keep myself from having to type in BufferedReader. and StringReader. all the time. :-)

Retreiving data in clojure

I have three text files as http://paste.debian.net/plain/1027720. As the third file is in the following format
Third File
salesID | custID | prodID | itemCount
1|1|1|3
2|2|2|3
I want to display the table such that custID should be replaced by the customer name and the prodID by the product description,
as follows:
1: ["John" "shoes" "3"]
What I did till now is :
(def data (slurp "cust.txt"))
(->> (for [line (clojure.string/split data #"[ ]*[\r\n]+[ ]*")]
(-> line (clojure.string/split #"\|") rest vec))
(map vector (rest (range))))
How I can retreive and map the values accordingly?
EDIT
"demo_1.txt"
content id|name|address|phone-number
1|John|123 Street|456-4567
2|Smith|123 Here Street|456-4567
"demo_2.txt"
prodID | item | Cost
1|shoes|14.96
2|milk|1.98
The processing of this data is similar to how I process CSV files. I like to split the problem into functions that do line to vector and vector to map, using the first row as the header for each.
(defn line->vec [s]
(s/split s #"\|"))
(defn vec->map [desc row]
(into {}
(map vector desc row))) ; Map accepts multiple collections
(defn file->maps [filename]
; Destructuring here, for easy capturing of header row
(let [[desc & lines] (->> (slurp filename)
(s/split-lines)
(map line->vec))
desc-keys (map keyword desc)]
(for [line lines]
(vec->map desc-keys line))))
For your demo files, you can use group-by to generate a map, sort of like an index (I manually fixed the header formatting, but you'd want to do it with a utility fn):
For (group-by :content-id (file->maps "demo_1.txt"))
{"1" [{:address "123 Street",
:phone-number "456-4567",
:name "John",
:content-id "1"}],
"2" [{:address "123 Here Street",
:phone-number "456-4567",
:name "Smith",
:content-id "2"}]}
For (group-by :prodID (file->maps "demo_2.txt"))
{"1" [{:item "shoes", :prodID "1", :cost "14.96"}],
"2" [{:item "milk", :prodID "2", :cost "1.98"}]}
And then replace each column with its index value:
(defn replace-value [index idx-key m k]
(update m k #(get-in index [% 0 idx-key])))
(defn -main [& args]
(let [customers (group-by :content-id (file->maps "demo1.txt"))
products (group-by :prodID (file->maps "demo2.txt"))]
; Use customers and products to replace some data
(->> (file->maps "demo_3.txt")
(map #(replace-value customers :name % :content-id))
(map #(replace-value products :item % :prodID)))))
And the result:
({:prodID "shoes", :content-id "John", :salesID "1", :itemCount "3"}
{:prodID "milk", :content-id "Smith", :salesID "2", :itemCount "3"})
Then it should be straightforward to convert those maps back into the format you want.

Clojure's defrecord - how to use it?

I'm attempting to create my own immutable datatype/methods with defrecord in Clojure. The goal is to have a datatype that I can create instances of, and then call its methods to return a new copy of itself with mutated variables. Say a and b are vectors. I'd like to update a value in both and return a new copy of the entire structure with those vectors updated. This obviously doesn't compile, I'm just trying to get my ideas across.
(defrecord MyType [a b]
(constructor [N]
; I'd like to build an initial instance, creating a and b as vectors of length N
)
(mutate-and-return []
; I'd like to mutate (assoc the vectors) and return the new structure, a and b modified
)
)
I'd like to call the constructor and then the mutator as many times as I'd like (there are other functions that don't mutate, but I don't want to make it more complex for the question).
Alternatively, if this is not idiomatic Clojure, how are you supposed to do something like this?
Here's how you define your record:
(defrecord MyType [a b])
Note that in Clojure you don't typically define "methods" within your record type itself (the exception is if you want to directly implement a Java interface or a protocol).
A basic constructor (prefixed with ->) gets generated automatically for free:
(def foo (->MyType [1 2 3] [4 5 6]))
foo
=> #user.MyType{:a [1 2 3], :b [4 5 6]}
You can then write more sophisticated constructor functions that use this, e.g.
(defn mytype-with-length [n]
(let [a (vec (range n))
b (vec (range n))]
(->MyType a b)))
(mytype-with-length 3)
=> #user.MyType{:a [0 1 2], :b [0 1 2]}
And "mutate-and-return" also comes for free - you can just use assoc:
(assoc foo :b [7 8 9])
=> user.MyType{:a [1 2 3], :b [7 8 9]}
Clojure defrecord example:
;;define Address record
(defrecord Address [city state])
;;define Person record
(defrecord Person [firstname lastname ^Address address])
;;buid the constructor
(defn make-person ([fname lname city state]
(->Person fname lname (->Address city state))))
;;create a person
(def person1 (make-person "John" "Doe" "LA" "CA"))
;;retrieve values
(:firstname person1)
(:city (:address person1))
Clojure allows you to create records, which are custom, maplike data types.
They’re maplike in that they associate keys with values, you can look up their values the same way you can with maps, and they’re immutable like maps.
(defrecord Person [last first address])
;=> core.Person
(defrecord Ad [street city zip])
;=> core.Ad
(def p1 (Person. "Jhon" "Mick"
(Ad. "US187956" "NY" 3369)))
;=> #'core/p1
(update-in p1 [:address :zip] inc)
;=> #core.Person{:last "Jhon", :first "Mick", :address #playsync.core.Ad{:street "US187956", :city "NY", :zip 3370}}
(assoc p1 :last "Adam")
;=> #core.Person{:last "Adam", :first "Mick", :address #playsync.core.Ad{:street "US187956", :city "NY", :zip 3370}}

Filter for Lists in Clojure

I am having a bit of difficulty with Lists in Clojure
I have a quick question concerning the filter function
Let's say I have a List composed of Maps
My code is:
(def Person {:name Bob } )
(def Person2 {:name Eric } )
(def Person3 {:name Tim } )
(def mylist (list Person Person2 Person3))
How would i go about filtering my list so that , for example: I want the list minus Person2 (meaning minus any map that has :name Eric)
Thank you very much to everybody helping me out. This is my last question I promise
For this purpose, it's better to use the 'remove' function. It takes a sequence, and removes elements on which it's predicate returns 'true'. It's basically the opposite of filter. Here is an example of it, and filter's usage for the same purposes, that I worked up via the REPL.
user> (def m1 {:name "eric" :age 32})
#'user/m1
user> (def m2 {:name "Rayne" :age 15})
#'user/m2
user> (def m3 {:name "connie" :age 44})
#'user/m3
user> (def mylist (list m1 m2 m3))
#'user/mylist
user> (filter #(not= (:name %) "eric") mylist)
({:name "eric", :age 32})
user> (remove #(= (:name %) "eric") mylist)
({:name "Rayne", :age 15} {:name "connie", :age 44})
As you can see, remove is a little bit cleaner, because you don't have to use not=. Also, when working with maps, you don't have to use the 'get' function unless you want it to return something special if a key isn't in the map. If you know the key you're looking for will be in the map, there is no reason to use 'get'. Good luck!
Suppose you have something like this:
(def Person {:name "Bob" } )
(def Person2 {:name "Eric" } )
(def Person3 {:name "Tim" } )
(def mylist (list Person Person2 Person3))
This would work:
(filter #(not= "Eric" (get % :name)) mylist)
user=> (filter (fn [person] (not= (person :name) "Eric")) mylist)
({:name "Bob"} {:name "Tim"})
or using a more compact syntax:
user=> (filter #(not= (% :name) "Eric") mylist)
({:name "Bob"} {:name "Tim"})