Fork me on GitHub
#clojure
<
2022-06-09
>
zimablue13:06:13

not an important question, just wondering: why doesn't clojure implement one-way ports? where one can only put and not take or only take and not put?

delaguardo13:06:26

If you can put but can't take how would you get data from it?

zimablue13:06:09

Assume that someone else is already getting data from it, eg I set up a real channel then split the ports and hand pish to one guy and pull to another

zimablue13:06:54

I know one can already do that by convention but there's something a bit odd to me about all these channel combinators purely relying on convention that some are only to be read and some only written

zimablue13:06:26

I guess there's probably some reason why these two things are tied together but I don't know it

p-himik13:06:44

In general, Clojure doesn't try to close things off from you.

zimablue13:06:07

I thought that, like python philosophy, there are exceptions though I think

zimablue13:06:55

Pragmatically, there do seem to be a lot of times in code that I've written that "only I push to this channel" is a fundamental assumption that there's no possibility of carving open, and if one did want to having the ports tied together only makes it easier for your readers to do that, and in a weird way they're maybe less likely to become alternate write sources as they're downstream in the information flow

zimablue13:06:19

I don't have a string opinion about this was just curious if anyone knew a deep reason rich chose it to be this way

Jan K13:06:26

Internally there are different interfaces for read/write, but the built-in channels implement both. https://github.com/clojure/core.async/blob/master/src/main/clojure/clojure/core/async/impl/protocols.clj#L15-L19

zimablue13:06:23

I saw that, they're under "impl" though which I maybe misinterpreted as indicating "do not touch"

zimablue13:06:35

Or implement

ghadi13:06:03

the simple reason is that if someone has to be able to write to a channel, presumably someone else is reading

ghadi13:06:18

in golang this distinction is enforced by types, I think

ghadi13:06:41

and not something like capability tokens

ghadi13:06:03

or a proxy interface that only allows one thing

zimablue14:06:41

I guess one could easily implement a record that does this in clojure, just hide the channel and forward the reduced interface members

Joshua Suskalo14:06:51

@U63D7UXJB this is just a general thing in clojure, sometimes called garbage-in, garbage-out, but maybe more accurately it's just that in Clojure code we can and do make assumptions about our inputs and then don't enforce those assumptions, because in a dynamic language enforcement costs performance. All the functions that work with channels will work in a way where they assume the channel is used in a certain way, and it's up to the programmer to link them up in a sensible manner. The functions in clojure.set can take any kind of clojure data structure, but it's undefined behavior if you pass anything other than a set. Channel functions assume that the channel passed will be used in a certain way, and it's undefined behavior if the user breaks that invariant. These invariants are usually just encoded in the docstring, and it's up to the programmer to ensure their code works. Having a one-way channel could work with the existing apis, but there's not much motivation to include it into core.async directly because doing so would be doing a kind of validation clojure doesn't do elsewhere, so it's just more code to maintain for little benefit.

Joshua Suskalo14:06:55

The actual protocols you need to implement to make your own channel aren't that bad, so you could just make your own one-way channel implementation by wrapping core.async channels and only providing some functions on each type, but doing that is just not something that's in core.async directly.

zimablue15:06:25

Thanks for the information and thoughts

ghadi15:06:25

i disagree that this is about GIGO

ghadi15:06:09

It's closer to: > In general, Clojure doesn't try to close things off from you.

Joshua Suskalo15:06:02

Yeah, that's fair

zimablue15:06:13

The python saying is "we're all consenting adults"

didibus17:06:23

Is that something that causes you a lot of defects? It's never happened to me that I like put on a channel I wasn't supposed to by mistake. I feel in practice it's pretty obvious what you care about.

didibus17:06:48

Also, the semantics are that you can do two way communication on the channel, so technically you can have two process ping pong using one channel. First process put and other take then the other puts while the prior takes. That makes them perfectly alternate between one another.

didibus17:06:34

As to why core.async doesn't, I don't think there's a philosophy at play, it's just that it didn't find a need to do so. If there's good use cases with good rational, you can cut a ticket, but chances are here the pros/cons probably don't add up to it. Every added feature adds complexity and maintenance cost. So I think it tries to be weighted against that.

zimablue17:06:21

Two processes ping ponging is interesting, I see what you mean that it's a pattern which the coupling of read and write enables, intuitively it feels like an antipattern, do you know any examples of two processes push/pulling on the same chan?

zimablue17:06:36

I wasn't about to write a ticket, just trying to learn

zimablue17:06:37

I agree that convention "works" but one can be immutable by convention and yet we still enforce immutability by default, it's not quite clear cut

zimablue17:06:46

An argument would be that universal or near-universal convention might drive an implementation even if it's not needed, just to semantically+programmatically enforce the good practise

didibus19:06:31

Ya, that's why I don't think this is philosophical. Clojure is just super pragmatic, no practical benefit, no feature 😛

didibus19:06:46

The posturing of hypothetical: "oh but what if stupid me don't realize and now I'm putting to the same channel when I shouldn't" isn't a convincing practical argument. It would need to be: Me was doing such and such, and I accidentally put in the wrong channel, nothing failed, test didn't catch, went to prod, company lost money, we had downtime. This happened to my coworker too, and my friend, and someone else, and it happens often. Ok, now you might have a case. But still, what's the downsides? Maybe you have another set of people saying, I often thought I wouldn't need to put from anywhere else, and then I did, it was awesome that I could just do that without needing to majorly refactor.

didibus19:06:16

This is what made the case for immutable by default for example, too many real issues caused by it, with real measurable impact and complexity.

zimablue19:06:52

I didn't make that hypothetical, to be fair

didibus19:06:08

For my example, well, I think for a lot of true concurrent algorithms, you need such behavior often. If you're just trying to do some concurrent IO, probably not. But actual concurrent computation you might need too, which is kind of what CSP was first designed for. I think for it to make more sense, you have to understand its about Processes, which are things that compute/do stuff. You have many Processes all computing stuff or doing things concurrently. Now if what each Process is computing/doing is independent from one another, no big deal, but what if they depend on each other in some way? Either one need data from another, or the order in which they do stuff can't be random, it has to make sure that one Process does something only after another has done something else. Each Process does its own compute/stuff sequentially. So within a Process, everything is sequential. But across Processes, it's all concurrent. That's why it's called Communicating Sequential Processes (CSP). You have these Processes computing/doing stuff sequentially, but they might need to Communicate with one another to coordinate when they do stuff with regards to where other people are at, or to pass data to one another. From that point of view, you can model a lot of stuff. For example, say you have a Cook making food and putting it on a plate, and a waiter taking the plate and bringing it to a customer. Say you only have 1 plate. You have two processes, Cook is cooking, and Waiter is serving, but they share the same plate, and need to coordinate. Chef would cook and wait for Plate to be on channel before putting the food on the plate. Waiter would put the Plate on the channel once last customer is done with it and after they cleaned it. Now waiter would wait for Cook to put food on the plate back on the channel. So they ping pong the plate between each other.

didibus19:06:54

So that's one example, passing of a shared resource between two Processes. Only one can use it at a time, each have to wait for the other to be done with it.

didibus19:06:09

> I didn't make that hypothetical, to be fair Ya, sorry, it's just often the hypotheticals that are made when it comes to "safeguards" in general. Any restrictions in what you can do with some construct generally is a question of, there should be some safety mechanism protecting from accidental misuse. You can posture about "accidental misuse" really easily for anything. So it's easy to fall in a trap of thinking since hypothetically everything can be misused, you need to put safeguards everywhere. But safeguards have downsides too, putting them everywhere isn't practical in a lot of cases, or it's really involved. So you want to really measure the risk of accidental misuse, how often, are there nothing else to catch it already, what the impact of the misuse is, etc., before going to the effort and loss in flexibility that adding safeguards often brings.

hiredman21:06:46

it would be good if core.async used read only ports

hiredman21:06:22

like for example timeout returns a read only port, but it implements both read, write, and close

hiredman21:06:15

that is bad

hiredman21:06:09

like, nothing is perfect, and there are any number of path dependent ways we could arrive at it being the way it is, up to and possibly including no one has tried to submit a patch to make the change (who knows if that would be accepted and how long it would take to get in)

didibus21:06:54

Right, but it's only up to some quantifiable good/bad, like its probably not a mistake you see often that someone tries to put to a timeout chan. That's my point with saying it would be good in a hypothetical, it would require effort, and possibly provide no value in practice, because maybe no one ever wrongly puts on a timeout chan, so throwing a NotImplementedError if you do might not really be practically worth it.

hiredman21:06:05

putting on a timeout chan is not something I see much of, true, but closing them comes up very often, and if it just returned a ReadPort, then either would throw an error

didibus21:06:22

That's fair, but it has zero upvotes: https://ask.clojure.org/index.php/348/clarify-timeout-mention-close-should-called-timeout-channel?show=348#q348 I'm not saying these things are inherently bad, sometimes there are no downsides to safeguards, like in this case, seems it would be zero cost and not affect use or extensibility, but it still takes effort, and my impression over the years is that the core team values ROI for their effort when prioritizing.

hiredman21:06:59

my new timeout impl on https://clojure.atlassian.net/browse/ASYNC-234 also just returns a ReadPort

Joshua Suskalo21:06:26

When implementing both overloads of java.util.Collection#toArray I'm getting compile errors saying that Clojure expected an object array but found an object. Is there something specific that I'm missing to make this act correctly? 🧵

Joshua Suskalo21:06:44

The implementation I've currently got going is the following:

(toArray [this]
    (object-array (seq this)))
  (toArray [this ^"[Ljava.lang.Object;" kind]
    (let [^Class array-clazz (class kind)
          _ (assert (.isArray array-clazz) "array argument is an array")
          ^Class clazz (.getComponentType array-clazz)]
      (object-array (map (partial cast clazz) (seq this)))))

Joshua Suskalo21:06:02

And I get a compile error of the following:

Mismatched return type: toArray, expected: [Ljava.lang.Object;, had:
   java.lang.Object

Joshua Suskalo21:06:16

This persists even if I type hint the return type of both methods and their bodies.

Joshua Suskalo21:06:30

Is there something I'm missing about this?

hiredman21:06:04

you cannot differentiate the methods via argument type

hiredman21:06:32

so you have to have one method that takes either, and in the method test the argument type and then do whatever

Joshua Suskalo21:06:37

Alright, so is there just no way for me to override both overloads? They take different numbers of arguments so it feels like it shouldn't cause an issue with differentiating on types.

Joshua Suskalo21:06:58

One takes only this while the other takes a second argument used to get type information.

dpsutton21:06:04

Clojure itself used this: (^objects toArray [this ^objects arr]

hiredman21:06:29

oh, yeah, sorry, yes it will disambiguate based on the number of args

Joshua Suskalo21:06:50

I guess my problem was that I was trying to annotate the return type on the arg vector like defns and not on the method name.

Alex Miller (Clojure team)21:06:33

yeah, protocol impls follow "Java style" and put the type hint there

Joshua Suskalo21:06:44

Good to know, thanks

didibus21:06:09

Oof, that's confusing, but would make for a good clj-kondo linter

Alex Miller (Clojure team)21:06:42

you almost never need those as they typically just take on the type of the interface method being implemented

Joshua Suskalo21:06:22

Right, I'd have hoped it'd work out fine. It's a bit of a weird case with Collection though it seems.

Alex Miller (Clojure team)21:06:54

yep, they managed to break several of our collections when they added that. the methods are not ambiguous with types so it's a case that was backwards compatible in Java but not in Clojure