Do we consider the utf16/utf8 string handling that we have inherited into clojure from java a good thing or bad thing? And do we consider it essential (if we leave the interop with java aside)? This definitely creates mixed feelings in me:
user=> (count "a")
1
user=> (count "π")
2
user=> (subs "frog π" 1)
"rog π"
user=>
user=> (subs "π frog" 1)
"? frog"
I understand why it happens due to the code points of the symbol and how utf16 is built, but i am wondering if it's really essential for an implementation of the language to being considered "clojure enough"> the code points of the symbol A slight but potentially important correction - you probably meant "code units". Sometimes there are multiple code points as well (and for those symbols code-point-oriented languages like Python also work that way, with "longer" lengths and content-breaking substrings), but not in this case. Anyway, even if we consider it to be non-essential and even undesirable, it's not something that can be changed. But a library can implement code-point-oriented string operations.
java.lang.Stringβs are immutable and pervasive. You get a lot for reusing the same basic datatype. I think if clojure string literals used a different class, it would make clojure less useful.
i agree that it makes no sense to change it if the host system is java or javascript.
fwiw, code points arenβt even that useful. grapheme clusters are where itβs at.
I once worked on a chat system where code on both ends encoded strings as utf16 (js does too) but sent the data over the wire as utf8, but prepended a prefix byte count by taking the string length in js
half of a flag emoji is only slightly more useful than half of a code unit.
> grapheme clusters are where itβs at. As always, everything is full of nuance. :) Plenty of software doesn't support clusters. Just tried - my terminal emulator spits out them as separately printed code points, so e.g. flags become two separate emoji letters.
So I monkey patched the Java library using reflection to derive a utf16 character count scanning utf8 bytes
To give more context to this, why i am asking, is that in zig utf8 is the first class citizen unlike in java ...
$ ./main
Clojure VM in Zig
user=> (+ 25 (* 4 2))
33
user=> (count "π")
1
user=>
Now try π¨βπ©βπ§βπ¦. :)
and i'm just wondering, if i should beat this poor kid back to past of utf16 just because many parts of clojure might expect it to be utf16 by default
@p-himik - what you included here is rendered as a picture for me, not as characters ...
if you have a unicode text of that actually, i could test ...
FWIW I think you should simply rely on the host's strings, without any extra processing. > what you included here is rendered as a picture for me, not as characters That's my point. Does Zig or Clojure VM in Zig count it as 7 code points or a single cluster?
no you lost my point here, you sent an html tag <img> ... not the unicode text for that character
i do not know what you referred to
I did not send an image. I sent a grapheme cluster for the "family" emoji. On my end, it renders like a regular emoji. In my terminal, it renders like four separate emojis.
slack in browser says it's an image
not text. image.
Because that's a hack for browsers that don't support grapheme clusters, either due to the impl or fonts, I presume.
You cannot inline images like that in Slack, as far as I'm aware. If you paste/add an image, it will be attached to the message as a separate entity - not inline.
Here's a simple example, an aforementioned flag - π¨π¦.
i meant from the browser perspective - i have no way to access the charcodes that you might have referred to
$ ./main Clojure VM in Zig user=> (count "πͺ") 1
works as expected
the emojipedia link worked well
Cool! Same as in Swift apparently. Although my terminal/Swift REPL from Docker disagrees.
1> ("\U+1F468\U+200D\U+1F469\U+200D\U+1F467\U+200D\U+1F466").count
$R0: Int = 1> works as expected Depends on your expectations. :) Very, very few languages do that.
this of course does not help me much with my dilemma π
it feels like a disservice to the current implementation to tell it "hey, back in 1995 somebody thought this is a great for java vm, so we should go back to that and punish anyone working with modern character codes ..."
I donβt think counting grapheme clusters is a good default since it changes over time
There is no clojure "spec" , so to be clojure rich has to say it is regardless of behavior, everything else is just sparkling lisp
So just do what you want
Some string-handling software goes kinda midway even - it supports grapheme clusters but selects and/or changes text based on code points. So you can enter π¨, then enter π¦ and get π¨π¦. And then you can select the first thing you entered, with the selection covering the first half of the flag, and erase it, getting only π¦ in return.
you usually want the logic for counting grapheme clusters to be a library, so you can update it independently of your language runtime
> so you can update it independently of your language runtime LMAO, Swift doesn't give a damn. Wonder what the Zig's deal is, though.
also, grapheme clusters can be arbitrarily many bytes/code units/code points due to zero width joiners
I think it would stink to have to require a new language update every time the unicode grapheme cluster data tables update
does swift backport new grapheme cluster data tables to old language versions?
> I think it would stink to have to require a new language update every time the unicode grapheme cluster data tables update Mm, but in the case of JVM it already happens with the timezone DB. Not that it needs to be exacerbated, of course. > does swift backport new grapheme cluster data tables to old language versions? No clue, but I assume it doesn't. Move fast and break things is the motto, apparently. Oh, and "stop supporting older hardware if it's inconvenient" as well.
a lot of the java libraries can actually accept CharSequenceβs, so you can replace strings with other data types in some cases. Strings also have built in support for working with code points. An example reason for implementing CharSequence is to have access to Javaβs regular expression library.
For context, Iβve been working on a code editor and a design goal was to have good support for editing text with emoji. The fact that clojureβs string literal uses java.lang.String didnβt really matter one way or another.
thanks for all the input π
Was building a terminal tool some time ago, and the byte-length vs. visible-width is a whole other can of worms too. Some characters have multiple bytes, but show up as a single-width character. And then it becomes even worse when you consider that not all fonts show the same visual character/symbol for the same byte sequence, thus do not have the same visual width.
This thread started with an open question but I'm more interested in what particular problem youβre trying to solve. What's your objective? What obstacle have you encountered? Strings are perhaps the most optimized part of Java and the JVM. Wrapping or even using a different CharSequence impl is a huge tradeoff and not something I can imagine being a real option at this point (Groovys GString took this path).
@alexmiller - I'm trying to create a minimalistic clojure implementation on top of Zig instead of JVM. I have used clojure in the past, on top of the jvm as a former java fanboy i never had a problem with it. But java's overall direction over the years is still rather disappointing and i was looking for other opportunities to write in the beautiful language without paying the jvm tax. Zig compiles nicely on several platforms, is able to create rather minuscule binaries, even when statically linked. But once i got past some basics of clojure i stumbled on the string handling and i was like "hmmm ..." - since i already break a bunch of things by not having a jvm ... should i break this barrier as well.
the value prop of Clojure as a hosted language is to lean on the host's strong points. If you're building on Zig, I would embrace Zig's notion of strings. You will not necessarily get exactly compatible answers for cases like count or reduce over strings but I don't necessarily see that as a problem.
I agree. From one point of view it's a shame that some pieces of code or java-ignorant libraries (referring to them as they would accidentally work to some extent) would work unpredictably, but from another angle it would be weird to swim against the flow within zig and fight on every string handling ... I will stick to utf8 for now and work on more annoying topics (and return to this one when other more important issues have been ironed out.
https://hsivonen.fi/string-length/ is a really good writeup about this issue, tldr; the whole notion of 'string length' is way more complicated and less actually useful than one might think
(count "π€¦πΌββοΈ") ;=> 7
(-> "π€¦πΌββοΈ" (.getBytes "UTF-8") count) ;=> 17
(-> "π€¦πΌββοΈ" .codePoints .count) ;=> 5
To agree with Alex and summarize it: Clojure doesn't have its own string type, it just uses the host's type. Return what makes sense for a Zig string.
I did live coding in a clojure course that Iβm teaching. I wanted to write code using update and reduce to compute a so-called adjacency list.
Here is what I wrote. I was unable to find the error, until a beginner student finally found it.
(defn build-adj [n edges]
(let [adj (mapv (fn [_] #()) (range n))]
(reduce (fn [acc [src dst]]
(update acc src #(conj % dst)))
adj
edges)))
(build-adj 6 [[1 2] [1 3] [2 5]])
here is the error message
1. Unhandled java.lang.ClassCastException
class user$build_adj$fn__11004$fn__11005 cannot be cast to class
clojure.lang.IPersistentCollection
(user$build_adj$fn__11004$fn__11005 is in unnamed module of loader
clojure.lang.DynamicClassLoader @4aa8077a;
clojure.lang.IPersistentCollection is in unnamed module of loader
'app')
core.clj: 84 clojure.core/conj
core.clj: 84 clojure.core/conj
REPL: 377 user/build-adj/fn/fn
core.clj: 6259 clojure.core/update
core.clj: 6251 clojure.core/update
REPL: 377 user/build-adj/fn
PersistentVector.java: 418 clojure.lang.PersistentVector/reduce
core.clj: 6964 clojure.core/reduce
core.clj: 6947 clojure.core/reduce
REPL: 376 user/build-adj
REPL: 374 user/build-adj
REPL: 384 user/eval11017is this a question? I think you want
(mapv (fn [_] #{}) (range n))
instead of
(mapv (fn [_] #()) (range n))My thoughts exactly. You're updating a vector of fns. More specifically, you can't conj onto a fn.
Also, can't you just do (repeat n #{}) ?
Aside from the point of course, but maybe it'd be even clearer that you're looking at #() and not #{} π
Ah, but then you also need vec
(vec (repeat n #{})) . Or you can't (update list .
This seems like a good opportunity for fnil since you want a default (of () or #{} -- unclear) for conj:
(update acc src (fnil conj #{}) dst)
And adj would be [] so you don't need the mapv-over-range (I think, untested)Oh, I guess not here since you want adj to be longer than edges... nm
You can't update an arbitrary nth item in a vector with fewer than n items... Right? So you need to initialize adj somehow π
EDIT: as per my own discovery and @oscarlinusericsson nudging, you can update n on a vector with at least n items. Not as straightforward as you think due to zero-based indicing π So you can "add" an item after of the last one with update.
Maybe you could decide on n dynamically, though.
Like (let [n (->> edges (map first) max)]
@reefersleep actually it is possible to use update on the +1 item of a vector (but not +2 etc): (update [100] 1 (fnil inc 0)) [100 1]
Yeah, I discovered that π
Yes, I need a vector because users of the vector will address by index. index n of the vector should be the set of all vertices of the graph which are neighbors of vertex n
I donβt find it a bad idea to start with a vector of size n containing n copies of the empty set. Thatβs just the initial value for the reduce β¦ perfectly understandable.
arguable (mapv (constantly #{}) (range n)) might be better
@seancorfield the the input seq of edges might not have a pair mentioning a certain vertex. e.g. maybe there is no edge whose source is 3. But nevertheless I need vector index 3 to exist and have #{} as value in that case.
BTW my students were impress at how few lines of code it was. In Python I think it is less typing but normally the Python idiom uses mutating data structures.
BRAVO! to everyone who solved the riddle
This is golf but we can do with less typing than python, I think:
(defn build-adj [n edges]
(reduce
(fn [acc [src dst]]
(update acc src conj dst)) ; update threads the value as the first arg
(vec (repeat n #{})) ; we're immutable so we can inline the empty vec and return it after all the updates
edges))If we're not married to the output being a vector, it could be even simpler in my eyes:
(defn build-adj [edges]
(reduce
(fn [acc [src dst]]
(update acc src (fnil conj #{}) dst))
{}
edges))that's better but fnil is such a footgun (I always forget it then want to shoot my foot off)
That's what I was going for above, but the OP specifically wants a vector with empty elements where there are no edges.
I thought of another version with merge-with instead of update:
(defn build-adj [edges]
(reduce
(fn [acc [src dst]]
(merge-with into acc {src #{dst}}))
{}
edges))@reefersleep yes of course however that puts an extra burden on the person using the library function to always be careful when using the object. If there are many call sites it's better in my opinion to make it as user friendly as is reasonable.
Sure π
@jimka.issy you say that users need to address the return value by index.
You could do (get result index #{}) π
Unsure how / when this happened but emacs/lsp is reindenting my function args from
(defn foo
[a {:keys [x y] :as b} c])
to
(defn foo
[a {:keys [x y]
:as b} c])
and I'm not loving it. I also noticed cljfmt enforces it too if :split-keypairs-over-multiple-lines? is enabled so I'm curious if lsp is finding a cljfmt config at the root and that's why it's happening? I mean, I understand why it's happening given the meaning of the linter but I prefer single-line function args so I'm wondering if there's any way to exclude this behavior on destructured and aliased args?https://clojure-lsp.io/settings/ clojure-lsp looks for a .cljfmt.edn so when formatting using lsp-format-buffer it will consider that config, besides that it could be other clojure-mode or so config
You're correct that the reformatting comes from cljfmt, which clojure-lsp uses for formatting as you type or on save. The specific setting at play is :split-keypairs-over-multiple-lines?, which controls whether map key-value pairs are broken into separate lines. A straightforward way to stop this behavior is to set that option to false in your project's cljfmt configuration file. This will disable keypair splitting for all maps in your entire project.