Fork me on GitHub
#clojure
<
2021-05-27
>
Oliver George00:05:01

Hello all. I created a #show-and-tell channel today. #announcements channel is lovely for libs but I want to be inspired by the systems and products people are building. What are you building with clojure today?  Show a screenshot and tell us about it. Few fun screenshots at #show-and-tell already.

Joel04:05:11

how can i use the threaded form but avoid particular parts of the thread based on the current threaded value, I'm aware of cond-> but that doesn't test on current value, eg:

(-> data 
    (fn1) 
    (fn2) 
    (thread-if #(= (:needs-doing %)) (needs-doing)) 
    (fn5)

Joshua Suskalo13:05:15

You could make a function for this pretty easily.

(defn when-pred
  [val pred then]
  (if (pred val)
    (then val)
    val))

(defmacro if->
  [val sym test clause]
  `(when-pred ~val (fn [~sym] ~test) (fn [~sym] ~clause)))

(-> data
    fn1
    fn2
    (when-pred :needs-doing needs-doing)
    fn4)

(-> data
    fn1
    fn2
    (if-> val (= (:some-key val) :some-thing) (update val :some-key some-update))
    fn4)

👍 1
Alex Miller (Clojure team)04:05:39

Or lift the if into a fn above

kirill.salykin08:05:28

hi please advice is there a smart trick to know if it is a last iteration inside the run! (the consumed sequence is lazy and it is important to keep it lazy)

borkdude08:05:03

there is no way to know if you're at the last element without looking ahead one element

kirill.salykin08:05:31

is there a run! which accepts two elements? 🙂

borkdude08:05:54

I would just implement this using loop probably

borkdude08:05:34

almost. the nil? check would not be enough if the seq also can contain nils, so I would use:

(loop [xs (seq xs0)]
  (let [next-xs (next xs)
         last? (not next-xs)]
   (recur next-xs)

Ed09:05:13

You could also do it with a transducer/eduction

(run! identity (eduction (fn [rf] (fn ([] (rf)) ([r] (prn 'done r) (rf r)) ([r i] (rf r i)))) (range 5)))
but I guess it kinda depends on why you need to know it's the last element

kirill.salykin09:05:14

it is interesting idea with a transducer thanks!

dpsutton13:05:23

Could you run! Over but-last and special case the last one?

kirill.salykin13:05:37

seems like custom loop as simplier solution

kirill.salykin13:05:40

thanks for the input

Ed13:05:27

you could also pair each element with the next one and look for where the paired elements run out:

(let [data (range 5)]
    (run! (fn [[a b]] (if (= a ::last) (do (prn b) b) b)) (map vector (concat (drop 1 data) [::last]) data)))
maybe?

sweb14:05:34

I just got this. Who do I need to poke (if anyone)?: > WARNING: Illegal reflective access by clojure.lang.InjectedInvoker/0x0000000800232040 (file:/C:/Users/x/.m2/repository/org/clojure/clojure/1.10.1/clojure-1.10.1.jar) to method sun.security.x509.X509CertImpl.getSubjectDN() > WARNING: Please consider reporting this to the maintainers of clojure.lang.InjectedInvoker/0x0000000800232040 > WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations > WARNING: All illegal access operations will be denied in a future release

ghadi14:05:32

can you provide more context around the code that did that?

ghadi14:05:03

--illegal-access=debug @U5QB8D59A add ^ to your JVM to get some better stacktrace

sweb14:05:29

The actual code creating the issue is this: (->> cert (.getSubjectDN) (.getName) (drop 3) (apply str))] cert is an X509CertImpl pulled from an sslContext Solution is to use a different method but it asked to warn someone..so here I am.

sweb14:05:18

So, I have fixed the issue in my code to not use a deprecated function.

Alex Miller (Clojure team)14:05:23

unfortunately, because this is in code generated by the compiler on your code's behalf, it will always get reported as if it is Clojure ;)

sweb14:05:41

Okies. Thanks

Alex Miller (Clojure team)14:05:28

the issue is not actually one of deprecation, but rather calling into a function that you cannot access due to module visibility.

sweb14:05:11

And it's true that switching from getSubjectDN to getSubjectX500Principal still gives an error

Alex Miller (Clojure team)15:05:39

this being one of the internal sun jdk methods, you're not supposed to use this class directly

ghadi15:05:25

can you post the full stacktrace from --illegal-access=debug?

ghadi15:05:42

I'd like to know if it's the Reflector at fault or something else

sweb15:05:04

I'll have a look

Alex Miller (Clojure team)15:05:10

yeah, would be curious to see the code

Alex Miller (Clojure team)15:05:37

probably should be using java.security.cert.X509Certificate ?

sweb15:05:56

ok here is what I got for you: > WARNING: Illegal reflective access by clojure.lang.InjectedInvoker/0x0000000800232040 (file:/C:/Users/sweb/.m2/repository/org/clojure/clojure/1.10.1/clojure-1.10.1.jar) to method sun.security.x509.X509CertImpl.getSubjectX500Principal() > at clojure.lang.Reflector.canAccess(Reflector.java:49) > at clojure.lang.Reflector.invokeMatchingMethod(Reflector.java:156) > at clojure.lang.Reflector.invokeNoArgInstanceMember(Reflector.java:438) > at url_check.core$wildcard_QMARK_.invokeStatic(core.clj:70) > at url_check.core$wildcard_QMARK_.invoke(core.clj:67) > at url_check.core$eval8394.invokeStatic(form-init16933106916591026780.clj:205) > at url_check.core$eval8394.invoke(form-init16933106916591026780.clj:205) > at clojure.lang.Compiler.eval(Compiler.java:7177) > at clojure.lang.Compiler.eval(Compiler.java:7132) > at clojure.core$eval.invokeStatic(core.clj:3214) > at clojure.core$eval.invoke(core.clj:3210) > at clojure.main$repl$read_eval_print__9086$fn__9089.invoke(main.clj:437) > at clojure.main$repl$read_eval_print__9086.invoke(main.clj:437) > at clojure.main$repl$fn__9095.invoke(main.clj:458) > at clojure.main$repl.invokeStatic(main.clj:458) > at clojure.main$repl.doInvoke(main.clj:368) > at clojure.lang.RestFn.invoke(RestFn.java:1523) > at nrepl.middleware.interruptible_eval$evaluate.invokeStatic(interruptible_eval.clj:79) > at nrepl.middleware.interruptible_eval$evaluate.invoke(interruptible_eval.clj:55) > at nrepl.middleware.interruptible_eval$interruptible_eval$fn__3452$fn__3456.invoke(interruptible_eval.clj:142) > at clojure.lang.AFn.run(AFn.java:22) > at nrepl.middleware.session$session_exec$main_loop__3553$fn__3557.invoke(session.clj:171) > at nrepl.middleware.session$session_exec$main_loop__3553.invoke(session.clj:170) > at clojure.lang.AFn.run(AFn.java:22) > at java.base/java.lang.Thread.run(Thread.java:834) > WARNING: Illegal reflective access by clojure.lang.Reflector (file:/C:/Users/sweb/.m2/repository/org/clojure/clojure/1.10.1/clojure-1.10.1.jar) to method sun.security.x509.X509CertImpl.getSubjectX500Principal() > at clojure.lang.Reflector.invokeMatchingMethod(Reflector.java:167) > at clojure.lang.Reflector.invokeNoArgInstanceMember(Reflector.java:438) > at url_check.core$wildcard_QMARK_.invokeStatic(core.clj:70) > at url_check.core$wildcard_QMARK_.invoke(core.clj:67) > at url_check.core$eval8394.invokeStatic(form-init16933106916591026780.clj:205) > at url_check.core$eval8394.invoke(form-init16933106916591026780.clj:205) > at clojure.lang.Compiler.eval(Compiler.java:7177) > at clojure.lang.Compiler.eval(Compiler.java:7132) > at clojure.core$eval.invokeStatic(core.clj:3214) > at clojure.core$eval.invoke(core.clj:3210) > at clojure.main$repl$read_eval_print__9086$fn__9089.invoke(main.clj:437) > at clojure.main$repl$read_eval_print__9086.invoke(main.clj:437) > at clojure.main$repl$fn__9095.invoke(main.clj:458) > at clojure.main$repl.invokeStatic(main.clj:458) > at clojure.main$repl.doInvoke(main.clj:368) > at clojure.lang.RestFn.invoke(RestFn.java:1523) > at nrepl.middleware.interruptible_eval$evaluate.invokeStatic(interruptible_eval.clj:79) > at nrepl.middleware.interruptible_eval$evaluate.invoke(interruptible_eval.clj:55) > at nrepl.middleware.interruptible_eval$interruptible_eval$fn__3452$fn__3456.invoke(interruptible_eval.clj:142) > at clojure.lang.AFn.run(AFn.java:22) > at nrepl.middleware.session$session_exec$main_loop__3553$fn__3557.invoke(session.clj:171) > at nrepl.middleware.session$session_exec$main_loop__3553.invoke(session.clj:170) > at clojure.lang.AFn.run(AFn.java:22) > at java.base/java.lang.Thread.run(Thread.java:834)

Alex Miller (Clojure team)15:05:34

line #s don't seem right there though

sweb15:05:10

indeed I am doing something along the same lines. I hadn't seen this repo before though

Alex Miller (Clojure team)15:05:47

or maybe that's your code? I'm just guessing from the ns

sweb15:05:14

ha, no it's not although we have the same ns.

Alex Miller (Clojure team)15:05:47

ok, well what's your code at line 70?

sweb15:05:07

he seems to be checking for a well formed response from a server. I am checking cookie compliance, form usage, cert validity (where I am running into the interop issues) etc

sweb15:05:08

(let [n (->> cert (.getSubjectX500Principal) (.getName) (drop 3) (apply str))]

sweb15:05:02

as per your faq, changing it to: > (let [n (->> ^java.security.cert.X509Certificate cert > (.getSubjectX500Principal) > (.getName) > (drop 3) > (apply str))] worked without error.

Endre Bakken Stovner14:05:29

What would be the best way to run command-line tools from Clojure? I do not just want to dispatch jobs, but also be notified when they are finished and learn the exit-code, std(out/err), and so on.

Ivar Refsdal14:05:17

I like babashka/process https://github.com/babashka/process as the name implies, it's included in babashka

Endre Bakken Stovner14:05:33

Thanks. I'll look at both of them!

Endre Bakken Stovner14:05:08

For now clojure.ava.shell.sh is enough. It is synchronous though. But I am sure I can just use Thread to make it non-synchronous.

Endre Bakken Stovner14:05:17

I am building a (Snake)make-like workflow management system where users provide the rules in text files to be parsed by my program to create a DAG for execution (phew!). The rules look like this (there would obviously be many more rules in a file):

(defrule samtools-sort
  "Sort the bams."
  {:wildcards [:sample :genome]
   :input     "bwa-map.bam"
   :output    ["bam/sorted.bam"]
   :shell     "samtools sort -T {{wildcards.sample}} -O bam {{input.0}} > {{output.0}}"}) 
It would be cool if I could include a :clojure-directive as an alternative to the :shell directive in case users wanted to do their data processing in Clojure rather than in shell, like:
:clojure (-> infile slurp process-data write-result-to-file)
This in itself would not be hard. But it would be cool if it were possible to let users include/require external libraries in the code too. I do not see any easy way to do this as the workflow files are fed to a precompiled program. Any suggestions/ideas?

Ed15:05:58

how are you starting the java process? if you want to download code from the internet and add it to your classpath, you may want to look at something like the add-lib branch of the clj tool (which is currently work in progress)

Endre Bakken Stovner15:05:18

My Clojure program will be a jar 🙂

Endre Bakken Stovner15:05:40

Cool! Will keep my eyes open for updates on the tool 😎

Ed15:05:45

yeah ... that's the bunny 😉

Ed15:05:51

I've used this a little in the past too: https://github.com/pallet/alembic

🙏 3
Ivar Refsdal15:05:12

for what it's worth: nREPL 0.7 got sideloader: #97: Added a sideloader, a network classloader that allows dependencies to be added even when the source/class files are not available on the server JVM's classpath (e.g. supplied by the client). https://github.com/nrepl/nrepl/releases (I haven't tried this feature myself)

Endre Bakken Stovner15:05:02

Cool to know about 🙂

isak14:05:22

Call a build tool from your program

Endre Bakken Stovner14:05:25

I guess. This would require me to learn lots more about tooling. Up to this day I've just cargo-culted and used luminus-recipes XD

3
Endre Bakken Stovner15:05:23

It seems like this will be really easy with the clj command-line tool :thumbsup:

isak15:05:27

Yea exactly, I was gonna say may not be as hard as you think

Endre Bakken Stovner05:05:06

I had only worked with lein and that would be a bit more involved. Plus it would add another rather large dependency.

3
Eugen15:05:40

any hints on how to debug a memory leak / performance leak in a clojure app on production ? The normal way to look on gc generation does not really help because clojure does not have classes.

borkdude15:05:06

Clojure does produce classes

Eugen15:05:51

bash-5.0# jcmd 1 GC.class_histogram -all > GC.class_histogram.csv bash-5.0# head GC.class_histogram.csv 1: num #instances #bytes class name (module) ------------------------------------------------------- 1: 96854702 3099350464 clojure.lang.LazySeq 2: 64655045 2068961440 clojure.lang.Cons 3: 5122755 394906984 [Ljava.lang.Object; ([email protected]) 4: 3382334 157481032 [B ([email protected]) 5: 3323357 79760568 java.lang.String ([email protected]) 6: 1509001 60360040 clojure.lang.PersistentVector 7: 2059526 49428624 clojure.lang.PersistentVector$Node

borkdude15:05:11

hmm, this seems to indicate holding onto the head of lazy sequences

☝️ 3
Eugen15:05:10

yes, and because clojure lacks types it's hard to know where this is comming from

Eugen15:05:46

in Java, with types you would see the name of the class somewhere there

borkdude15:05:19

@U011NGC5FFY did you try to force a GC before taking this data?

Ed15:05:29

if you take a thread dump can you see something in the list of things that the process is doing that's a common code path that looks like it might be holding onto the head of something large?

Eugen15:05:20

I don't have one now

Eugen15:05:19

it's a bit difficult since it's hard to reproduce the performance issue that triggers the issue and the app runs remotely inside docker

Ed15:05:25

what kind of things is it connected to? if it's selecting large amounts of data from a db, and trying to serve that up over http it might be calling doall on something before trying to convert it to an http response?

Ivar Refsdal15:05:55

Can you dump the heap on out of memory exception and have it written to a mounted docker volume to be inspected later? https://www.baeldung.com/java-heap-dump-capture#automatically

Eugen15:05:37

thanks - the app does not throw memory exception - it starts lots of threads and it slows down to a halt

Eugen15:05:02

at the same time memory consumption is very high and these are related

Eugen15:05:14

but because there is no OOM it might not be a leak ?!

Ivar Refsdal15:05:34

are you catching and logging/printing uncaught thread exceptions?

Ed15:05:40

if there's a leak, you'll eventually get an OomException

Ed15:05:26

which gc algorithm are you using?

Ed15:05:37

have you tried the old strategy of doubling the heap size and seeing what happens over longer periods of time?

borkdude15:05:47

@U011NGC5FFY > it starts lots of threads can you see what the app is doing in those threads?

Eugen16:05:23

@U0P0TMEFJ default with java 11 - G1

Eugen16:05:42

@U04V15CAJ: I am working on that

Eugen16:05:10

@UGJE0MM0W: not sure how to reply

Eugen16:05:11

thanks @U0P0TMEFJ, will check

Eugen16:05:50

thanks for the feedback so far, I'll see what I can do further

Ed16:05:52

if you've got multiple docker processes to play with, you could always have one start up with the serial collector, which is a bit easier to follow what's getting cleaned up, because it just stops the world rather than trying to let all the threads progress ... (just as an idea of trying to work out what's going on 😉 )

Ivar Refsdal16:05:56

Yes that's what I was suggesting

Eugen16:05:36

the performance issue is difficult to trigger, the app uses ~ 12 - 23 GB of ram and needs a cache, db and queue - these make things difficult to debug

ghadi16:05:55

class histo indicates a 96 million entry seq -- at least

ghadi16:05:44

you could take a memory dump and examine any instance of the LazySeq, check its .s field

Eugen16:05:02

I will do that, thanks

ghadi16:05:55

your step 1 class_histo was the absolute correct thing to do

Eugen16:05:07

thank you, I found a very good presentation on the subject: https://www.youtube.com/watch?v=_gineh_HcoQ (shorter version available online someplace)

ghadi16:05:36

wow 3hrs 🙂

ghadi16:05:02

jcmd <pid> GC.heap_dump file_destination

Eugen16:05:44

thanks, I am putting some load on the system now (trying, I can't seem to be able 😄 ) .

rmcv16:05:14

https://github.com/clojure-goes-fast/clj-async-profiler. I found the flame graph quite useful in analysing both space and time issues.

Eugen16:05:18

this looks interesting, thanks rmcv

ghadi17:05:18

@U011NGC5FFY if you can download the heapdump, you can offline analyze it with https://visualvm.github.io/download.html

ghadi17:05:47

I just re-tested it out and it was easy to tell what's in the LazySeq

Eugen17:05:10

thanks. Will try it out. I don't lknow how useful it will be for me now since I can't seem to reproduce the issue

Eugen17:05:21

I think it's affraid 🙂

ghadi17:05:28

you definitely have a thing hanging onto a LazySeq -- it's absolutely clear

Eugen17:05:03

seems I can't create a heap dump - size is 0. tried with both jcmd and jmap - files have size 0

Ivar Refsdal17:05:14

Because the system is stalled or wrong pid? I did it like this some time ago:

$ docker exec my-service ps
PID   USER     TIME   COMMAND
    1 java       0:00 /dev/init -- java -Xmx8g -Dcom.sun.management.jmxremote.rmi.port=19090 -Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=19090 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.local.only=true -Djava.rmi.server.hostname=localhost -jar /my-jar.jar
    8 java       0:26 java -Xmx8g -Dcom.sun.management.jmxremote.rmi.port=19090 -Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=19090 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.local.only=true -Djava.rmi.server.hostname=localhost -jar /my-jar.jar
  256 java       0:00 ps

# Make a heap dump for PID 8
$ docker exec my-service jcmd 8 GC.heap_dump /tmp/docker.hprof

Ivar Refsdal17:05:12

and then:

docker cp my-service:/tmp/docker.hprof .

Eugen17:05:58

it works now, I changed the path

Endre Bakken Stovner15:05:27

I am following the https://clojure.org/guides/deps_and_cli#_using_a_main but I cannot find out how to run the example program from a different folder:

clj -X hello/run 
Like, my code resides at ~/code/hello/, but I want to run it from ~/whatevz/some/bogus/folder. Is there a way? I know I could just use pushd ~/code/hello; clj -X hello/run; popd, but I was hoping there existed a way that did not involve jumping between paths.

Alex Miller (Clojure team)15:05:56

no, not with using clj -X

🙏 6
Endre Bakken Stovner15:05:24

I see there is a -Jopt. Will read up on the possible flags I can send there.

Alex Miller (Clojure team)16:05:07

that just lets you use any jvm option

Endre Bakken Stovner05:05:51

But I thought one of them might be relative-to or something 🙂

Charlie Briggs16:05:11

Any advice on propagating errors when you have multiple long-running threads in a clojure service on the jvm? i.e. my-process returns [input-ch done-ch] and starts some background some work somehow -`ExecutorService`, async/thread, future, etc. The problem is all these wrappers to background work end up with uncaught exceptions not being handled (i.e. by top-level Thread$UncaughtExceptionHandler unless a specific uncaught exception handler is added to the thread, and this then needs to exit the process. It seems possible to return a future and deref futures from all these processes, but this doesn’t seem to be the way, as the deref order on the main thread then matters or you end up with the same problem :thinking_face: I’ve read this from a few years ago, but I’m not sure what the best approach is https://stuartsierra.com/2015/05/27/clojure-uncaught-exceptions

phronmophobic17:05:41

Most (all?) of the ExecutorService implementations allow you to provide a ThreadFactory which would allow you to set an uncaught exception handler

phronmophobic17:05:46

The error handling strategy will depend on the use case. Generally, it's hard to recover from unexpected errors from arbitrary processes.

phronmophobic17:05:32

As far as using something like core.async, I would generally turn all "outputs" (including exceptions) into data. That can mean wrapping work in a try/catch

Joshua Suskalo17:05:10

+1 on this, the ideal here is to start all your processes with core.async and then use alt to wait for the first one to fail and then exit on that.

noisesmith19:05:41

regarding using deref to test future failure, you can use the second and third args to deref to time out and provide a placeholder value if the future isn't ready, then move to the next future

noisesmith19:05:47

contrived example:

(cmd)user=> (def futs (map future-call (repeat 10 #(doto (rand-int 20000) (Thread/sleep)))))
#'user/futs
(cmd)user=> (do (doall (take-while #{:waiting} (map #(deref % 1000 :waiting) (cycle futs)))) (map #(deref % 1000 :waiting) futs))
(6165 :waiting 7143 1631 :waiting 1091 :waiting :waiting :waiting :waiting)

emccue17:05:45

There are some types in the transit spec like "point in time" that are listed as "extension" types

emccue17:05:48

but i'm unclear if making a type like "~y123" is doable

emccue17:05:27

most of the examples make it seem like the one letter, string prepending tagged values are an internal thing

emccue17:05:43

and custom types i support myself would have to be

emccue17:05:52

["~#y" "123"]

ghadi18:05:25

I'm not clear what your question is

borkdude18:05:48

@emccue these types should be handled by registering transit handlers for those with a tag

emccue18:05:34

for context, I am circling back to writing a transit impl for Elm

👍 3
borkdude18:05:07

cool, I like transit and would like to see it be extended to other languages

emccue18:05:13

One aspect is that it won't be idiomatic to have dynamic handlers registered

emccue18:05:37

so its important to understand how these special cases work

emccue18:05:00

like Symbol is listed as an extension type

emccue18:05:12

but it affects how caching works

emccue18:05:19

so that makes it an intrinsic thing

emccue18:05:30

but ~i123 is a ground type...

Alex Miller (Clojure team)18:05:50

transit has been extended to many languages

emccue18:05:11

yeah one of the challenges is that all the community implementations seem to take the same "pass a map of handlers" approach so I don't have much prior art to look at

emccue18:05:23

the simplest case would be something like

emccue18:05:00

[["~#point", [3, 4]], ["~#point", [5, 6]]]

emccue18:05:33

in most implementations you would do something like

emccue18:05:15

(transit/read {"point" point-handler} transit-val)

emccue18:05:41

and how to interpret a value tagged with "point" is encoded all the way down the tree

emccue18:05:53

but in elm, we don't have much of a choice but to use an api similar to elm/json where you put explicit expectations at each point

dg18:05:47

Does anyone know if there's an existing macro/library/idiom for using something like let with channels for fan-in? For example:

(let* [thing1 (<!! (do-thing1-returning-channel))
       thing2 (<!! (do-thing2-returning-channel))]
  [thing1 thing2])
The naive way to write this does thing1 and thing2 sequentially. It seems so natural to want to do them in parallel and allow the let to gather results that I wonder if someone has written the macro already...

ghadi18:05:02

core.async/merge

ghadi18:05:18

(one way to do it)

dg18:05:54

Yeah, I can think of several good ways to get it done, just don't want to recreate someone's prior art needlessly

p-himik18:05:19

Manifold has its own version of let , IIRC.

dg18:05:13

ooh, that's very promising, thanks

emccue18:05:00

import Array
import Transit.Decode as TD

type alias Point =
    { x : Int
    , y : Int
    }

decoder : TD.Decoder (List Point)
decoder =
    TD.vector 
      (TD.map2 Point
         (TD.tagged "point" (TD.index 0 ))
         (TD.tagged "point" (TD.index 1 )))
    |> TD.map Array.toList

emccue18:05:04

so how I would handle TD.tagged "y" is an open question

emccue18:05:18

since a since character could be a string tag

emccue18:05:36

or it could be a tag for a composite value

Oliver George21:05:58

Hello all. I created a #show-and-tell channel today. (Resend for us time zones) #announcements channel is lovely for libs but I want to be inspired by the systems and products people are building. What are you building with clojure today?  Show a screenshot and tell us about it. Few fun screenshots at #show-and-tell already.

zendevil.eth21:05:53

If I’m applying a filter on a set, what is the time complexity, where the size of the set n?

noisesmith18:06:30

for a sorted-set you can get better time complexity for a split based on the ordering if you use subseq