I am working on a problem to read in a file with lines like:
A abcdefg
B bcdefgh
But I keep getting errors about Lazy Sequence not compatible with Java Charseq ..
I tried:
(def notlazy (doall lyne2))
Then thought I verified:
(realized? notlazy)
true
But still:
(str/split notlazy #" ")
ClassCastException class clojure.lang.LazySeq cannot be cast to class
java.lang.CharSequence (clojure.lang.LazySeq is in unnamed module of
loader 'app'; java.lang.CharSequence is in module java.base of loader
'bootstrap') clojure.string/split (string.clj:219)
Help please!
The first argument to str/split must be a CharSequence to be split. Presumably you want to split each input line in the sequence for which you can use map without needing to eagerly evaluate the input sequence:
(map (fn [line] (str/split line #" ")) lyne2)
Expanding on the previous result a bit, we have this example. You can reproduce the following using this template project.
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test)
(:require
[clojure.java.io :as io]
[tupelo.string :as str]
))
(def data-file
"A abcdefg
B bcdefgh
")
(dotest
; Version #1
(let [lines (line-seq (io/reader (str/string->stream data-file)))
lines2 (remove str/blank? lines)
lines3 (map str/trim lines2)
line-words (mapv #(str/split % #"\s+") lines3) ; "\s+" => "one or more whitespace chars"
]
(spyxx lines)
(spyxx lines2)
(spyxx lines3)
(spyxx line-words))
with result:
--------------------------------------
Clojure 1.10.2-alpha1 Java 15
--------------------------------------
Testing tst.demo.core
lines => <#clojure.lang.Cons ("A abcdefg" " B bcdefgh" " ")>
lines2 => <#clojure.lang.LazySeq ("A abcdefg" " B bcdefgh")>
lines3 => <#clojure.lang.LazySeq ("A abcdefg" "B bcdefgh")>
line-words => <#clojure.lang.PersistentVector [["A" "abcdefg"] ["B" "bcdefgh"]]>
This shows the type of each result along with its value. We use string->stream so we don't need to set up a dummy file to read from.
The following shows how it would typically be written in real code (not as a demo exercise like Version #1). We use the "thread-last" operator, and write a unit test to verify the result:
; Version #2
(let [result (->> data-file
(str/string->stream)
(io/reader)
(line-seq)
(remove str/blank?)
(map str/trim)
(mapv #(str/split % #"\s+"))) ; "\s+" => "one or more whitespace chars"
]
(is= result [["A" "abcdefg"] ["B" "bcdefgh"]])))
Related
I want to convert the string into an array,
I have tried some steps but not getting the desired result.
(:require [clojure.string :as str])
(def stringval "fruit==Mango,fruit==Papaya;veggie==Onion,veggie==Potato")
(defn formatting [strFilters]
(let [filters (str/split strFilters #";")]
(for [filter filters]
(let [eachFilter (str/split filter #",")]
(for [each eachFilter]
(let [items (str/split each #"==")]
items
))))))
(formatting stringval)
I am getting below output
((["fruit" "Mango"] ["fruit" "Papaya"]) (["veggie" "Onion"] ["veggie" "Potato"]))
I want clojure function which returns the below array
Array
(
[fruit] => Array
(
[0] => Mango
[1] => Papaya
)
[veggie] => Array
(
[0] => Onion
[1] => Potato
)
)
You want a list of maps, so you have to turn your current intermediate
results into a map. You can do this with group-by and some some
post-processing, or you can use merge-with conj if you shape the
result from the innermost for in preparation for it. Also note, that
for can have :let in it.
(require '[clojure.string :as str])
(def s "fruit==Mango,fruit==Papaya;veggie==Onion,veggie==Potato")
(for [g (str/split s #";")]
(apply merge-with into
(for [kv (str/split g #",")
:let [[k v] (str/split kv #"==")]]
{k [v]})))
; → ({"fruit" ["Mango" "Papaya"]} {"veggie" ["Onion" "Potato"]})
And in case your target-output there is from PHP or some other language,
that got their basic data structures wrong, and you actually just want
a map with the keys to arrays of values, you just have to to shift the
merge-with into out and you can also split for ; and , one swoop.
(apply merge-with into
(for [kv (str/split s #"[;,]")
:let [[k v] (str/split kv #"==")]]
{k [v]}))
; → {"fruit" ["Mango" "Papaya"], "veggie" ["Onion" "Potato"]}
one more option is to get all the pairs with re-seq and reduce it with grouping:
(->> stringval
(re-seq #"([^,;].+?)==([^,;$]+)")
(reduce (fn [acc [_ k v]] (update acc k conj v)) {}))
;;=> {"fruit" ("Papaya" "Mango"), "veggie" ("Potato" "Onion")}
I've got a Clojure file which I'm importing other library functions into with :require and :refer in the ns declaration.
I now want to eval some code in that namespace and have it call those libraries.
But when I call it, I get an "Unable to resolve symbol" for the referred function I'm trying to use.
I'm guessing I have to pass it explicitly in to the eval somehow but can't find any examples.
Second question. I'd ideally like to not use Clojure's ordinary eval at all but to run Babashka SCI. Is there a way to pass libraries from my Clojure environment into this?
Update. Code example.
(ns clj-ts.card-server
[:require
...
[patterning.layouts :refer [framed clock-rotate etc]]
...
)
...
(defn one-pattern
"Evaluate one pattern"
[data]
(let [pattern
(try
(eval (read-string data))
(catch Exception e
(let [sw (new java.io.StringWriter)
pw (new java.io.PrintWriter sw) ]
(.printStackTrace e pw)
(str "Exception :: " (.getMessage e) (-> sw .toString) ))) )
]
...
)
Then when calling one-pattern with the following as the data argument
(let [a-round
(fn [n lc fc]
(clock-rotate
n (std/poly
0 0.5 0.4 n
{:stroke lc
:fill fc
:stroke-weight 3})))
]
(a-round 8 (p-color 140 220 180) (p-color 190 255 200 100) )
)
I get an error
Caused by: java.lang.RuntimeException: Unable to resolve symbol: clock-rotate in this context
I am able to eval a form that has a refer'ed function. As the following code demonstrates.
user> (join " " ["foo" "bar"])
Syntax error compiling at (*cider-repl Projects/example:localhost:59334(clj)*:43:7).
Unable to resolve symbol: join in this context
user> (require '[clojure.string :refer [join]])
nil
user> (join " " ["foo" "bar"])
"foo bar"
user> (eval (read-string "(join \" \" [\"foo\" \"bar\"])"))
"foo bar"
user>
EDIT:
eval performs the evaluation in the context of the "current" namespace that is bound to *ns*. There are three ways I can think of to address your question - with examples below. I've tried to match what I think your code structure is.
Here are the contents of the file foo.clj that defines the eval function evalit and refers a library function (in this case join from clojure.string).
(ns myorg.example.foo
(:require [clojure.string :refer [join]]))
(defn evalit [s]
(eval (read-string s)))
We first load this myorg.example.foo namespace:
user> (require 'myorg.example.foo)
nil
First alternative: use the fully qualified symbol for the join as follows:
user> (myorg.example.foo/evalit "(clojure.string/join \" \" [\"boo\" \"baz\"])")
"boo baz"
Second alternative: Temporarily bind *ns* to the namespace that contains the refer'ed join. Note that in this case we can just use "join" rather than the fully qualified "clojure.string/join". But we still have to use the fully qualified symbol for evalit, since we are referencing it from a different namespace.
user> (binding [*ns* (find-ns 'myorg.example.foo)]
(myorg.example.foo/evalit "(join \" \" [\"boo\" \"baz\"])"))
"boo baz"
Third alternative: Switch to the namespace with the refer'ed function.
user> (in-ns 'myorg.example.foo)
#namespace[myorg.example.foo]
myorg.example.foo> (evalit "(join \" \" [\"boo\" \"baz\"])")
"boo baz"
Here we can use the unqualified symbols since we are in the namespace with the definition and refer.
OK.
I ended up doing this with https://github.com/babashka/babashka SCI
In my file I basically took all the functions I had imported and put them into a new map like this
(def patterning-ns
{'clojure.core
{
'clock-rotate clock-rotate
'poly poly
...
}
}
)
Once the function names that got imported from elsewhere are in the map called patterning-ns (under the namespace name of clojure.core) then they are visible by default to any code eval-ed with
(sci/eval-string data {:namespaces patterning-ns })
Eg in
(defn one-pattern
"Evaluate one pattern"
[data]
(try
(let [
pattern
(sci/eval-string
data
{:namespaces patterning-ns })
svg (make-svg 400 400 pattern)
]
svg
)
)
(catch java.lang.Exception e
(let [sw (new java.io.StringWriter)
pw (new java.io.PrintWriter sw) ]
(.printStackTrace e pw)
(println e)
(str "Exception :: " (.getMessage e) (-> sw .toString) )) )))
I have a string school_name_1_class_2_city_name_3 want to split it to {school_name: 1, class:2, city_name: 3} in clojure I tried this code which didn't work
(def s "key_name_1_key_name_2")
(->> s
(re-seq #"(\w+)_(\d+)_")
(map (fn [[_ k v]] [(keyword k) (Integer/parseInt v)]))
(into {}))
You are looking for the ungreedy version of regex.
Try using #"(\w+?)_(\d+)_?" instead.
user=> (->> s (re-seq #"(\w+?)_(\d+)_?"))
(["key_name_1_" "key_name" "1"] ["key_name_2" "key_name" "2"])
When faced with a problem, just break it down and solve one small step at a time. Using let-spy-pretty from the Tupelo library allows us to see each step of the transformation:
(ns tst.demo.core
(:use tupelo.core tupelo.test)
(:require [clojure.string :as str]))
(defn str->keymap
[s]
(let-spy-pretty
[str1 (re-seq #"([a-zA-Z_]+|[0-9]+)" s)
seq1 (mapv first str1)
seq2 (mapv #(str/replace % #"^_+" "") seq1)
seq3 (mapv #(str/replace % #"_+$" "") seq2)
map1 (apply hash-map seq3)
map2 (tupelo.core/map-keys map1 #(keyword %) )
map3 (tupelo.core/map-vals map2 #(Long/parseLong %) )]
map3))
(dotest
(is= (str->keymap "school_name_1_class_2_city_name_3")
{:city_name 3, :class 2, :school_name 1}))
with result
------------------------------------
Clojure 1.10.3 Java 11.0.11
------------------------------------
Testing tst.demo.core
str1 =>
(["school_name_" "school_name_"]
["1" "1"]
["_class_" "_class_"]
["2" "2"]
["_city_name_" "_city_name_"]
["3" "3"])
seq1 =>
["school_name_" "1" "_class_" "2" "_city_name_" "3"]
seq2 =>
["school_name_" "1" "class_" "2" "city_name_" "3"]
seq3 =>
["school_name" "1" "class" "2" "city_name" "3"]
map1 =>
{"city_name" "3", "class" "2", "school_name" "1"}
map2 =>
{:city_name "3", :class "2", :school_name "1"}
map3 =>
{:city_name 3, :class 2, :school_name 1}
Ran 2 tests containing 1 assertions.
0 failures, 0 errors.
Passed all tests
Once you understand the steps and everything is working, just replace let-spy-pretty with let and continue on!
This was build using my favorite template project.
Given
(require '[clojure.string :as str])
(def s "school_name_1_class_2_city_name_3")
Following the accepted answer:
(->> s (re-seq #"(.*?)_(\d+)_?")
(map rest) ;; take only the rest of each element
(map (fn [[k v]] [k (Integer. v)])) ;; transform second as integer
(into {})) ;; make a hash-map out of all this
Or:
(apply hash-map ;; the entire thing as a hash-map
(interleave (str/split s #"_(\d+)(_|$)") ;; capture the names
(map #(Integer. (second %)) ;; convert to int
(re-seq #"(?<=_)(\d+)(?=(_|$))" s)))) ;; capture the integers
or:
(zipmap
(str/split s #"_(\d+)(_|$)") ;; extract names
(->> (re-seq #"_(\d+)(_|$)" s) ;; extract values
(map second) ;; by taking only second matched groups
(map #(Integer. %)))) ;; and converting them to integers
str/split leaves out the matched parts
re-seq returns only the matched parts
(_|$) ensures that the number is followed by a _ or is at an end position
The least verbose (where (_|$) could be replaced by _?:
(->> (re-seq #"(.*?)_(\d+)(_|$)" s) ;; capture key vals
(map (fn [[_ k v]] [k (Integer. v)])) ;; reorder coercing values to int
(into {})) ;; to hash-map
I would like to replace the UUIDs with a string using Clojure but not sure how to create a function for it.
This is the original value: /v1/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964
and would like to see as below
/v1/user-1/user-1-id/user-2/user-2-id/user-3/user-3-id
But dynamic enough to handle the below examples as well,
Case 1.
input:
/v1/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964/car
expected result:
/v1/user-1/user-1-id/user-2/user-2-id/user-3/user-3-id/car
Case 2.
input:
/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964/car
expected result:
/user-1/user-1-id/user-2/user-2-id/car
Case 3.
input:
/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964/car/tire
expected result:
/user-1/user-1-id/user-2/user-2-id/car/tire
or just literally do the simple string replace
(clojure.string/replace
"/v1/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964"
#"/(user-\d+)/[0-9a-f\-]+(?=/|$)"
"/$1/$1-id")
;;=> "/v1/user-1/user-1-id/user-2/user-2-id/user-3/user-3-id"
(defn normalize-path [path]
(clojure.string/replace path #"/(user-\d+)/[0-9a-f\-]+(?=/|$)" "/$1/$1-id"))
(map normalize-path
["/v1/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964/car"
"/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964/car"
"/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964/car/tire"])
;;=> ("/v1/user-1/user-1-id/user-2/user-2-id/user-3/user-3-id/car"
;; "/user-1/user-1-id/user-2/user-2-id/user-3/user-3-id/car"
;; "/user-1/user-1-id/user-2/user-2-id/user-3/user-3-id/car/tire")
you can also update regex to match only valid uids like this:
(defn normalize-path [path]
(clojure.string/replace
path
#"/(user-\d+)/[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}(?=/|$)"
"/$1/$1-id"))
(map normalize-path
["/v1/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964/car"
"/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964/car"
"/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964/car/tire"
"/v2/user-101/asd-asd-asd/user-2/xxx"
"/v3/user-404/aa44ddss-3333-4444-aaaa/user-505/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964"])
;; ("/v1/user-1/user-1-id/user-2/user-2-id/user-3/user-3-id/car"
;; "/user-1/user-1-id/user-2/user-2-id/user-3/user-3-id/car"
;; "/user-1/user-1-id/user-2/user-2-id/user-3/user-3-id/car/tire"
;; "/v2/user-101/asd-asd-asd/user-2/xxx"
;; "/v3/user-404/aa44ddss-3333-4444-aaaa/user-505/user-505-id")
Here is one way to do it, based on my favorite template project
(ns tst.demo.core
(:use tupelo.core tupelo.test)
(:require
[tupelo.string :as str]
[tupelo.uuid :as uuid]
))
(defn replace-uuids
[input-str]
; use `let-spy-pretty` below to see step-by-step results
(let [segments (str/split input-str #"/")
pairs (partition-all 2 segments)
; looks like:
; [["" "v1"]
; ["user-1" "4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964"]
; ["user-2" "7badb866-44fc-43c7-9cf4-aa3c2f8cc964"]
; ["user-3" "27ebd241-44fc-43c7-9cf4-aa3c2f8cc964"]]
version-pair (first pairs)
user-pairs (rest pairs)
user-pairs-mod (for [pair user-pairs]
(let [user-part (first pair)
uuid-part (second pair)]
(if (uuid/uuid-str? uuid-part)
[user-part (str user-part "-id")]
pair)))
all-pairs-mod (flatten [version-pair user-pairs-mod])
result (str/join (interpose "/" all-pairs-mod))]
result))
(dotest
(is= "/v1/user-1/user-1-id/user-2/user-2-id/user-3/user-3-id"
(replace-uuids "/v1/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964"))
(is= "/v1/user-1/user-1-id/user-2/user-2-id/user-3/user-3-id/car"
(replace-uuids "/v1/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964/car"))
)
For a more general solution, you may just want to use a regular expression , which is what uuid-str? does internally.
(ns tst.demo.core
(:use tupelo.core tupelo.test)
(:require
[schema.core :as s]
[tupelo.string :as str]
))
(def full-regex
#"(?x) # expanded form
/ # a slash
([a-zA-Z0-9-_]+) # repeated identifier char in a capture group
/ # a slash
\p{XDigit}{8} # 8 hex digits
- # hyphen
\p{XDigit}{4} # 4 hex digits
- # hyphen
\p{XDigit}{4} # 4 hex digits
- # hyphen
\p{XDigit}{4} # 4 hex digits
- # hyphen
\p{XDigit}{12} # 12 hex digits
")
(s/defn replace-uuids :- s/Str
[s :- s/Str]
(str/replace s full-regex
(fn [arg]
(let ; use `let-spy` to see intermediate steps
[x1 (second arg)
result (str "/" x1 "/" x1 "-id")]
result))))
with result
(dotest
(is= "/v1/user-1/user-1-id/user-2/user-2-id/user-3/user-3-id"
(replace-uuids "/v1/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964"))
(is= "/v1/user-1/user-1-id/user-2/user-2-id/user-3/user-3-id/car"
(replace-uuids "/v1/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964/car"))
(is= "/v1/user-1/user-1-id/user-2/user-2-id/user-3/user-3-id/car/tire"
(replace-uuids "/v1/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964/car/tire"))
)
Please see also the list of documentation sources.
Clojure Spec is convenient for these sorts of things. Here is one way of doing it:
(require '[clojure.spec.alpha :as spec]
'[clojure.string :as cljstr])
(spec/def ::fixkey-format
(spec/cat :root #{""}
:version #{"v1"}
:pref1 #{"user-1"}
:user-1 string?
:pref2 #{"user-2"}
:user-2 string?
:pref3 #{"user-3"}
:user-3 string?))
(defn replace-values [input new-values]
(cljstr/join
"/"
(spec/unform
::fixkey-format
(merge (spec/conform ::fixkey-format (cljstr/split input #"/")) new-values))))
(replace-values "/v1/user-1/4bcbe877-44fc-43c7-9cf4-aa3c2f8cc964/user-2/7badb866-44fc-43c7-9cf4-aa3c2f8cc964/user-3/27ebd241-44fc-43c7-9cf4-aa3c2f8cc964"
{:user-1 "x"
:user-2 "y"
:user-3 "z"})
;; => "/v1/user-1/x/user-2/y/user-3/z"
Input: "Michael" "Julia" "Joe" "Sam"
Output: Hi, Michael, Julia, Joe, and Sam. (pay attention to the commas and the word "and")
Input: nil
Output: Hi, world.
Here is my first attempt:
(defn say-hi [& name]
(print "Hi," name))
user> (say-hi "Michael")
Hi, (Michael)
nil
user> (say-hi "Michael" "Julia")
Hi, (Michael Julia)
nil
Question:
How to implement default: (no input, say "Hi World!")
How to get rid of the parents around names in output?
How to implement the commas separation and add the conjunction word "and"?
First off, Clojure supports multi-arity functions, so you could do something like this to achieve default behaviour:
(defn say-hi
([] (say-hi "World"))
([& names] ...))
Then, what you want is to take a seq and join all the strings it contains together, using ", " in between. The clojure.string namespaces contains lots of string manipulation functions, one of them being clojure.string/join:
(require '[clojure.string :as string])
(string/join ", " ["Michael", "Julia"])
;; => "Michael, Julia"
But the last element of the seq should be concatenated using " and " as a separator, so you'll end up with something like this:
(require '[clojure.string :as string])
(defn say-hi
([] (say-hi "World"))
([& names]
(if (next names)
(format "Hi, %s, and %s!"
(string/join ", " (butlast names))
(last names))
(format "Hi, %s!" (first names)))))
Note that you have to differentiate between the single- and multi-name cases and (next names) basically checks whether the seq contains more than one element. (You could achieve the same by adding another arity to the function.)
(say-hi)
;; => "Hi, World!"
(say-hi "Michael")
;; => "Hi, Michael!"
(say-hi "Michael" "Julia" "Joe" "Sam")
;; => "Hi, Michael, Julia, Joe, and Sam!"
You can use clojure.string/join:
(use '[clojure.string :only [join]])
(defn sentencify [& elems]
(->>
[(join ", " (butlast elems)) (last elems)]
(remove empty?)
(join " and ")))
(defn say-hi [& name]
(print "Hi," (if name
(sentencify name)
"World!")))
A concise solution:
(defn say-hi [& names]
(let [names (case (count names)
0 ["world"]
1 names
(concat (butlast names) (list (str "and " (last names)))))]
(->> names, (cons "Hi"), (interpose ", "), (apply str))))
(say-hi)
;"Hi, world"
(say-hi "Michael")
;"Hi, Michael"
(say-hi "Michael" "Julia" "Joe" "Sam")
;"Hi, Michael, Julia, Joe, and Sam"
For long lists of names, you would want to eschew count, last, and butlast, maybe by pouring names into a vector first.
To print (as the question does) rather than return the formatted string, append print to the final form:
(->> names, (cons "Hi"), (interpose ", "), (apply str), print)