Fork me on GitHub
#beginners
<
2021-07-30
>
Jakub Šťastný00:07:43

Is there the opposite function of filter? By that I mean filter filters items for which the predicate is true, I'm looking for a fn that would filter items for which the predicate is false. Example: (filter even? [1 2 3]) => (2), whereas (hypothetical-reject-fn even? [1 2 3]) => (1 3).

Patrix00:07:56

no idea if the best answer but this works: (filter (complement even?) [1 2 3])

Patrix00:07:19

of course in this case complement is a fancy not lol

tschady00:07:10

Remove

💯 3
Patrix00:07:51

much better

tschady00:07:11

I highly recommend reviewing functions on the clojure cheat sheet. Great primer.

Jakub Šťastný00:07:36

Haven't seen the cheatsheet before, nice!

sova-soars-the-sora00:07:24

some might call it a cheat sheet... others, a guidebook to life

sova-soars-the-sora01:07:59

Can someone help me diagnose this porblam?

2021-07-29 20:16:40.289:WARN:oejs.HttpChannel:qtp1307785347-2565: /authenticate
java.lang.NullPointerException
        at java.base/java.util.regex.Matcher.getTextLength(Matcher.java:1770)
        at java.base/java.util.regex.Matcher.reset(Matcher.java:416)
        at java.base/java.util.regex.Matcher.<init>(Matcher.java:253)
        at java.base/java.util.regex.Pattern.matcher(Pattern.java:1135)
        at clojure.core$re_matcher.invokeStatic(core.clj:4856)
        at clojure.core$re_find.invokeStatic(core.clj:4898)
        at clojure.core$re_find.invoke(core.clj:4898)
        at jpdrills.server$convert_to_int.invokeStatic(server.clj:106)
        at jpdrills.server$convert_to_int.invoke(server.clj:105)
        at jpdrills.server$fn__53358.invokeStatic(server.clj:2877)
        at jpdrills.server$fn__53358.invoke(server.clj:2875)
        at compojure.core$wrap_response$fn__22505.invoke(core.clj:158)
        at compojure.core$wrap_route_middleware$fn__22489.invoke(core.clj:128)
        at compojure.core$wrap_route_info$fn__22494.invoke(core.clj:137)
        at compojure.core$wrap_route_matches$fn__22498.invoke(core.clj:146)
        at compojure.core$routing$fn__22513.invoke(core.clj:185)
        at clojure.core$some.invokeStatic(core.clj:2701)
        at clojure.core$some.invoke(core.clj:2692)
        at compojure.core$routing.invokeStatic(core.clj:185)
        at compojure.core$routing.doInvoke(core.clj:182)
        at clojure.lang.RestFn.applyTo(RestFn.java:139)
        at clojure.core$apply.invokeStatic(core.clj:667)
        at clojure.core$apply.invoke(core.clj:660)
        at compojure.core$routes$fn__22517.invoke(core.clj:192)
        at clojure.lang.Var.invoke(Var.java:384)
        at ring.middleware.anti_forgery$wrap_anti_forgery$fn__24296.invoke(anti_forgery.clj:94)
        at compojure.response$eval20620$fn__20621.invoke(response.clj:47)
        at compojure.response$eval20542$fn__20543$G__20533__20550.invoke(response.clj:7)
        at compojure.core$wrap_response$fn__22505.invoke(core.clj:158)
        at compojure.core$wrap_route_middleware$fn__22489.invoke(core.clj:128)
        at compojure.core$wrap_route_info$fn__22494.invoke(core.clj:137)
        at compojure.core$wrap_route_matches$fn__22498.invoke(core.clj:146)
        at compojure.core$routing$fn__22513.invoke(core.clj:185)
        at clojure.core$some.invokeStatic(core.clj:2701)
        at clojure.core$some.invoke(core.clj:2692)
        at compojure.core$routing.invokeStatic(core.clj:185)
        at compojure.core$routing.doInvoke(core.clj:182)
        at clojure.lang.RestFn.applyTo(RestFn.java:139)
        at clojure.core$apply.invokeStatic(core.clj:667)
        at clojure.core$apply.invoke(core.clj:660)
        at compojure.core$routes$fn__22517.invoke(core.clj:192)
        at ring.middleware.flash$wrap_flash$fn__22966.invoke(flash.clj:39)
        at ring.middleware.session$wrap_session$fn__24034.invoke(session.clj:108)
        at ring.middleware.keyword_params$wrap_keyword_params$fn__24076.invoke(keyword_params.clj:36)
        at ring.middleware.nested_params$wrap_nested_params$fn__24134.invoke(nested_params.clj:89)
        at ring.middleware.multipart_params$wrap_multipart_params$fn__24382.invoke(multipart_params.clj:173)
        at ring.middleware.params$wrap_params$fn__22900.invoke(params.clj:67)
        at ring.middleware.cookies$wrap_cookies$fn__23913.invoke(cookies.clj:175)
        at ring.middleware.absolute_redirects$wrap_absolute_redirects$fn__24488.invoke(absolute_redirects.clj:47)
        at ring.middleware.resource$wrap_resource$fn__24398.invoke(resource.clj:37)
        at ring.middleware.content_type$wrap_content_type$fn__24436.invoke(content_type.clj:34)
        at ring.middleware.default_charset$wrap_default_charset$fn__24460.invoke(default_charset.clj:31)
        at ring.middleware.not_modified$wrap_not_modified$fn__17924.invoke(not_modified.clj:53)
        at ring.middleware.x_headers$wrap_x_header$fn__22929.invoke(x_headers.clj:22)
        at ring.middleware.x_headers$wrap_x_header$fn__22929.invoke(x_headers.clj:22)
        at ring.middleware.x_headers$wrap_x_header$fn__22929.invoke(x_headers.clj:22)
        at ring.middleware.session$wrap_session$fn__24034.invoke(session.clj:108)
        at ring_range_middleware.core$wrap_range_header$fn__25623.invoke(core.clj:548)
        at ring.middleware.reload$wrap_reload$fn__25114.invoke(reload.clj:39)
        at ring.adapter.jetty$proxy_handler$fn__22800.invoke(jetty.clj:27)
        at ring.adapter.jetty.proxy$org.eclipse.jetty.server.handler.AbstractHandler$ff19274a.handle(Unknown Source)
        at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
        at org.eclipse.jetty.server.Server.handle(Server.java:501)
        at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:383)
        at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:556)
        at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:375)
        at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:273)
        at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311)
        at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105)
        at org.eclipse.jetty.io.ChannelEndPoint$1.run(ChannelEndPoint.java:104)
        at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:806)
        at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:938)
        at java.base/java.lang.Thread.run(Thread.java:832)

sova-soars-the-sora01:07:55

does that likely indicate a regex problem? or maybe a cant-cast-to-integer problem?

phronmophobic01:07:06

just off-hand, it looks like it's trying to cast a nil to an integer that it expects to be a string

sova-soars-the-sora01:07:30

Thank you. You are wise in the jedi way.

phronmophobic01:07:58

just make lots of mistakes and eventually you can start to appreciate all the different kinds of nullpointers in nature

3
sova-soars-the-sora01:07:16

Yeah, I have this thing:

(defn convert-to-int [s]
   (Integer/parseInt (re-find  #"\d+" s )))

sova-soars-the-sora01:07:43

but apparently, it's sometimes getting passed a nil ? I mean, you haven't seen the whole codebase but I can't imagine it's happening elsewhere...

sova-soars-the-sora01:07:11

so yeah, maybe if it's nil have it return 0 ?

phronmophobic01:07:34

I don't think convert-to-int should necessarily coerce nils

phronmophobic01:07:00

where is it getting the value from?

phronmophobic01:07:18

why isn't it there?

sova-soars-the-sora01:07:39

it's a web <form> element

phronmophobic01:07:49

if you do explicitly want 0 as the default when the value isn't found, then I would make providing the default an explicit step

phronmophobic01:07:35

it's ok for someone to provide a default for "not found", but I don't think that's convert-to-int 's job

phronmophobic01:07:37

also, I find this convert-to-int function suspicious. Why is it silently truncating the end of the string after the numbers?

phronmophobic01:07:16

> (Integer/parseInt (re-find  #"\d+" "123asdf" ))
123

sova-soars-the-sora05:07:09

Thanks for your help. Apparently the person who wrote this code months ago (me) was thinking something very different xD

😆 6
seancorfield05:07:45

I often look at code and think "what idiot wrote this?" (me, five years ago) 🙂

💯 2
dgb2308:07:00

Hey I recently found a function that I wrote that I actually still like, well at least the interface, but still! 😁

Lennart Buit08:07:25

haha, I have git blame inline in my editor[s]. Three years ago, when I started this job, it was always someone else. Now… its sometimes me…

dgb2308:07:56

That’s a good sign! It implies that we improve our understanding of what we’re doing and how we do it.

Henry11:07:27

I was reading Joy of Clojure (ch5) and came across this function:

(defn neighbors [size yx]
  (let [delta [[0 1] [1 0] [0 -1] [-1 0]]]
    (filter
      (fn [new-yx] (every? #(< -1 % size) new-yx))
      (map #(vec (map + yx %)) delta))))
The function works equally well (similar time) if, on the last line, `#(vec (map + yx %))` is replaced by `#(map + yx %)`. As both of them implements the seq API, I wonder if leaving out `vec` is just fine in such cases?

Ed11:07:41

how are you deciding that they work in similar time? I ask because it returns a lazy seq, and if you're not timing the consumption of that seq and the consumption is done by the repl printing the result, you'll get the same timing results no matter what you make that fn do 😉 ... but vec converts the incoming sequence to a vector, which involves consuming the whole sequence and will be slower for larger collections. Reading the code though, it does seem like and extra call that isn't needed 😉

Henry14:07:03

@U0P0TMEFJ Thank you very much for your help. Insightful point about the timing indeed! I am still wrapping my head around the minutiae of the sequence abstraction. The minutiae of which seems to be all over the place for beginners, but I do hope the bits and pieces will "compose" for me through practice so that the understanding becomes second nature. Would really appreciate it if anyone knows of any good learning resources (book, blogpost, summary, etc) on this topic. Thanks a lot for sharing.

Henry06:07:04

Now that I am reading ch7 and trying out the A* implementation which utilizes the above neighbors function from ch5, I understand why it is necessary for the neighbors function to use #(vec (map + yx %)) which returns a vector instead of #(map + yx %) which returns a lazy sequence. This is because in the A* implementation, the output of the neighbors function would ultimately be fed into a sorted-set and the sorted-set requires every item of the input collection (including the nested items) to have implemented the Comparable interface.

Jakub Šťastný13:07:40

How do I check whether a file on path (that may or may not actually exist) is executable?

Jakub Šťastný13:07:14

I'm trying to implement functionality of the which Linux command. (first (filter HERE-WHAT? (str/split (System/getenv "PATH") #":"))))

borkdude14:07:14

you can just call executable? from babashka.fs though instead of dealing with the interop horror from java.nio, I think @jakub.stastny.pt_serv might already be in bb anyway (if I remember correctly from #babashka)

Noah Bogart14:07:08

i’ve run into the java.lang.RuntimeException: Method code too large! error. is there a good way to debug which part of the code has generated the too-large method?

dpsutton14:07:57

does the stacktrace give any hints?

dpsutton14:07:56

i've got a likely culprit in mind but lets wait for the stacktrace because that will point directly at it unless things get weird with this kind of error

Jakub Šťastný14:07:41

Yeah that's correct, I'm in Babashka.

Noah Bogart14:07:27

the stacktrace shows each of the namespaces loaded: clojure.main.main (main.java:37) and then a bunch of internal calls into at web.core$loading__6434__auto____262.invoke(core.clj:1) which is the entrypoint for my app, then more internal calls into at web.system$loading__6434__auto____603.invoke(system.clj:1) and then more internal calls into Exception in thread "main" java.lang.RuntimeException: Method code too large!, compiling:(agendas.clj:1:1) , which has

(ns game.cards.agendas
  (:require [game.core :refer :all]
            [game.utils :refer :all]
            [jinteki.utils :refer :all]
            [clojure.string :as string]
            [clojure.set :as clj-set]))

Noah Bogart14:07:03

game.core exports a lot of functions. will that do it?

dpsutton14:07:22

do you mind pasting the stacktrace?

👍 2
dpsutton14:07:15

try running (require 'your-ns :verbose :reload)

dpsutton14:07:45

so the compiler is compiling the dependencies of that namespace and is running into an error because something is expanding to a single function that is too long

Noah Bogart14:07:29

i can’t get a repl running because of this. i’ll see if :verbose works in the namespace definition

dpsutton14:07:29

i don't believe your problem is in how many functions are in a particular namespace but that one individual one is too large

Noah Bogart14:07:45

okay, it shows the output. the output is quite large because of all of the required namespaces

dpsutton14:07:38

sure. hopefully right before the error it will say which namespace it is trying to load?

Noah Bogart14:07:50

the final bit is (clojure.core/refer 'potemkin :refer '[import-vars])

Noah Bogart14:07:15

i should say that this has been working fine up until i created two new namespaces and used import-vars to import/export them as well

Noah Bogart15:07:34

if i comment out those two import-vars calls, the project loads again

Noah Bogart15:07:12

and the require verbose call shows this:

(clojure.core/refer 'potemkin :refer '[import-vars])
(clojure.core/refer 'game.core :refer ':all)

Noah Bogart15:07:18

so that (clojure.core/refer 'game.core :refer ':all) call is where it’s failing

dpsutton15:07:00

that prevents the code from being required which prevents the code from being compiled.

Noah Bogart15:07:23

What do you mean?

dpsutton15:07:32

there's a clojure form that when expanded and compiled produces too much bytecode for a single jvm method. It is not compiled until you require its namespace somewhere. By not requiring the namespace you avoid this problem

dpsutton15:07:44

but now i'm starting to doubt if refer does the requiring. so perhaps i am wrong here

Noah Bogart15:07:43

clojure.core/refer definition is here: https://github.com/clojure/clojure/blob/clojure-1.10.1/src/clj/clojure/core.clj#L4217 and i can’t tell exactly how it works

Noah Bogart15:07:04

the final line seems to call a different refer

Noah Bogart14:07:03

i’m using the potemkin function import-vars to export all of the methods from the numerous sub-namespaces in the engine. using a quick-and-dirty search, looks like i’m exporting 576 functions lol

cp4n15:07:51

Total beginner here. Trying to practice some problems at work during lunch break on Code Wars and working out my code in a Clojurescript online REPL. I have a problem where I need a function that takes two strings, finds only the unique characters in both, sorts them alphabetically and returns that new string. So far I have this which evaluates what I want in the REPL: (str/join "" (sort (distinct (apply concat (str/split s1 #"") (str/split s2 #""))))) But then when I add the rest of the function, I get back nil in the REPL and in the Code Wars evaluation I get the error: "java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Character" (defn longest [s1 s2] (str/join "" (sort (distinct (apply concat (str/split s1 #"") (str/split s2 #"")))))) Can anyone help me with what I am not properly comprehending? (apologies as I am sure my formatting surely is incorrect)

schmee15:07:37

@U01BQJWGV16 evaluate the forms from the inside out: start with (str/split...) then (apply concat (str/split...)) and so on

schmee15:07:46

see if you can figure it out that way! 🙂

schmee15:07:53

otherwise I can give some pointers

schmee15:07:29

ahh, you said you are in a Clojurescript REPL! that explains why you’re not getting the error. here’s what happens: when you call

; eval (current-form): (apply concat (str/split "asdf" #"") (str/split "dfgh" #""))
("a" "s" "d" "f" \d \f \g \h)
you can see that the first half looks different

schmee15:07:09

when you do (apply concat ...) the second (str/split...) will pass each character of the String to concat, and in Java chars are not the same as Strings (first half of the array are Strings, second half are chars) so after apply the call will look like this: (concat (str/split "asdf" #"") "d" "f" "g" "h"), so each 1 character String is interpreted as a collection of 1 char!

schmee15:07:39

changing the code to use just concat instead:

; eval (current-form): (concat (str/split "asdf" #"") (str/split "dfgh" #""))
("a" "s" "d" "f" "d" "f" "g" "h")
and you see that they now look the same

schmee15:07:01

this doesn’t happen on Cljs because Javascript doesn’t have a separate types for Strings and Characters, it’s all String.

schmee16:07:41

my take 😄

(defn longest [s1 s2]
  (->> (into (str/split s1 #"") (str/split s2 #""))
       distinct
       sort
       str/join))

cp4n16:07:09

Loving all the valuable insight here, sure beats being confused by yourself 🙂 Thank you both!

💯 4
lsenjov15:07:19

Firstly, why are you using apply?

cp4n15:07:23

oh yeah, I guess that isn't needed. Not exactly sure, just sort of fumbling around through the cheatsheet

lsenjov15:07:16

Actually I'm pretty sure that's the only issue (I'm not near a repl to test things out right now)

cp4n15:07:40

Thank you!

👍 2
cp4n15:07:43

Wow, yep, that is it. Took that out and all tests passed and all errors gone.

👍 2
pinkfrog15:07:25

How to handle name collision like this: db_ because of db is already used. Is there some macro to bypass it ?

(fx/defn remove-account
  {:events [:account/remove]}
  [{:keys [db]} uuid]
  (let [db_ (update-in db [:accounts] dissoc uuid)]
    (persist-accounts {:accounts (:accounts db_)})
    {:db db_}))

indy15:07:47

You can use db instead of db_ in the let, quite common to do it

pinkfrog15:07:29

By common, you mean what codebases are using it?

lsenjov15:07:39

Alternatively, we tend to use db' to denote a shadowed but modified var

pinkfrog15:07:18

I saw the db’ flavour to. But I cannot remember where it is advocated. I know in Python, mostly people will use the underscore such as db_.

lsenjov15:07:55

I think it's a haskell thing. It was introduced by my other engineer and I just kinda picked it up

dpsutton15:07:30

Your persist accounts should be an effect I believe

pinkfrog15:07:46

yes. it is.

emccue15:07:56

its a physics thing

emccue15:07:26

x -> x' is how you describe a tranformation

👍 2
emccue15:07:02

in "normal" math, x' is the derivative of x, not the successor to x

👍 2
indy15:07:51

@UGC0NEP4Y Hard to find open source examples, but using the same name tells me that I'm transforming the value. The db value travels starting as the arg, then is transformed in the let and finally returned.

indy15:07:37

If one had another transformation of db_ in the let, one wouldn't bother finding another name for it, I guess the same logic applies when it starts as the arg itself

👍 2
dpsutton15:07:38

if this were an effect (and maybe i'm using the wrong terminology, its been a bit since i've been doing re-frame) i would expect to see {:db db' :persist-accounts (:accounts db_)} and dispatch to that effect from what i think is an event here?

👍 2
jaju15:07:53

There’s also an ergonomic reason for db' over db_ - the underscore needs an extra keystroke. But yes, as @UMPJRJU9E suggests, using the same symbol is fine if we are transforming to a next version of the same thing, and we don’t need to refer to both values at once.

Jakub Šťastný16:07:59

What am I doing wrong? (letfn [(^{:doc "Doc string"} doc-fn [] "body")] (meta #'doc-fn)) => nil. I expected the hash with :name, :doc and the likes. For context, why I'm not using the defn macro: This is just some code in the rich comment block, I don't want anything to bubble up into the global namespace. (Plus I want to be able to C-x C-e to eval last sexp.)

Russell Mull16:07:48

Running this is a clean repl, I get:

Russell Mull16:07:50

Syntax error compiling var at (REPL:1:50).
Unable to resolve var: doc-fn in this context

Russell Mull16:07:23

Which makes sense, because #'doc-fn is specifically looking for a var, but you don't get a var with letfn

Russell Mull16:07:25

I'm not sure I understand your use case 100%, but using regular 'let' and 'fn' works fine:

Russell Mull16:07:34

(let [doc-fn ^{:doc "Doc string"} (fn [] "body")] (meta doc-fn))
{:doc "Doc string"}

Russell Mull16:07:23

letfn is not just a shortcut combining let and fn; it's a special construct that allows for a local recursive function definition.

Jakub Šťastný17:07:20

Thanks @U7ZL911B3, I'll go with let then.

ChillPillzKillzBillz11:07:07

@jakub.stastny.pt_serv: Please if you find some time, can you explain the syntax you are using.... ? I am new and I don't follow your code at all... I'll greatly appreciate it!!

Jakub Šťastný14:07:20

@U022LK3L4JJ sure man. So my original code was built on the assumption that letfn is simply a macro for let + fn (just like defn is a macro for def + fn), but as @U7ZL911B3 pointed out, it's not. My objective was to get an alternative of (defn doc-fn "Doc string" [] "body"), but as a local variable using let, rather than a global one using def. Now (defn doc-fn "Doc string" ...) is a shorthand for (defn doc-fn ^{:doc "Doc string" ...). This is CJ syntax for metadata. The :doc is a common one, :private would mark a private function (there's a shortcut for it as defn-) and :name would contain the fn name, so doc-fn in here. And (meta '#<fn-name>) is how you get the metadata. For instance you can try (meta #'str) in the REPL and see what comes out. A thing to know about the metadata is that they are bound to the reference itself, not to its value. So if you do (let [my-fn str] (meta my-fn)), it will return nil, as the metadata are in the reference of str and not its value and don't get copied when we assign str to another variable my-fn. Does it make sense? Feel free to ask more. Anyway this is my understanding, I'm new to CJ, so take with a grain of salt.

ChillPillzKillzBillz14:07:19

ok so ^ is for metadata... Why would you need to use something like this? ... if you don't mind me asking...

Jakub Šťastný23:08:38

@U022LK3L4JJ the code is here https://github.com/jakub-stastny/dev/blob/literate.dev/build.org#give-for-review, look for defn fun if you want some context. Essentially I have these helpers to either run a shell command (`run`) or a CLJ fn (`fun`). Both are meant to print out what they are running. I wanted fun to print out name of the function it is running and it's doc string. So (defn download-github-gpg-key "Gets the GH GPG key that's required for installation of GH CLI tools" ... ) has both :name and :doc in its metadata. I was wondering whether I could simply pass the fn name and get metadata from it like so: (fun download-github-gpg-key), but it's not possible, because the metadata is attach to the reference itself and once you bind it in the definition to another reference (say fn-name), its metadata won't be bind to it. So that's why I have to pass the metadata separately from the fn-name thus: (fun (meta #'download-github-gpg-key) download-github-gpg-key)

Jakub Šťastný23:08:49

Of course here I use it in a (comment) block (rich comments), for testing only, so I do not want to define a global fn, but only a local one, that's why all the weird things with let.

ChillPillzKillzBillz09:08:15

thanks for the explanation Jakub. I'll look it up!!

Noah Bogart16:07:39

is there a library/plugin that does what slamhound did? I want to expand my :refer :all namespace requires into fully specified :refer vectors

dpsutton17:07:03

(->> (ns-publics (find-ns 'clojure.set))
                              (map (comp symbol key))
                              (sort)
                              (into []))

dpsutton17:07:45

but just a bit of personal style that i think is far better and might even be widespread, this is a bad pattern and just use an alias.

dpsutton17:07:13

(actually don't need (comp symbol key) just key is sufficient

👍 2
dpsutton17:07:16

and of course shorter, (sort (keys (ns-publics (find-ns 'your-ns)))

Ryan20:07:06

Hey all, trying to store some data compactly (some pen and paper RPG character sheet stuff for some fun learning projects) and am wondering about the most idiomatic way to store a collection of data. The map version would be something like { :ns.skill/name "Name of the Skill" :ns.skill/base-stat :ns.stat/agility :ns.skill/value 13} ... was thinking it'd be nice to write it as a vector like ["Name of the Skill" :agility 13] then process it, but not sure if that's suitably idiomatic. Any thoughts?

dpsutton20:07:20

i would for sure stay with a map. make your keys as simple or as verbose as you want

2
Ryan20:07:56

awesome, thanks

dpsutton20:07:04

remember that in your example :ns.skill/name, ns does not have to be a clojure namespace that exists. You could do your own shorthand, a shortening of your org, whatever. or you could leave a namespace off but i'm finding i usually regret those decisions

💯 2