flix / flix

The Flix Programming Language
https://flix.dev/
Other
2.15k stars 150 forks source link

Channel Operations Syntax #1479

Closed magnus-madsen closed 1 year ago

magnus-madsen commented 3 years ago

A gentleman on Reddit humbly suggested that our syntax is sometimes confusing:

image

I generally disagree and as someone pointed out:

Yet, on the last point, it could be argued that channel operations are much much less frequently used than all of the other constructs. Hence, why not have a keyword for them? (I am definitely not changing anything else).

What do you think?

magnus-madsen commented 3 years ago

The thread was here: https://www.reddit.com/r/programming/comments/jtiva1/flix_the_flix_programming_language/

mlutze commented 3 years ago

I agree that the <- syntax feels strange. I think the reason is that it looks very much like an infix operator. In my experience, arrows typically either:

That all said, I think the syntax is get-used-to-able.

magnus-madsen commented 3 years ago

Proposal 1:

let c1 = Channel.bounded(5);
let c2 = Channel.unbounded();
Channel.get(c1);
Channel.put(c2, value);
let n = Nursery.new();
spawn(n, 1 + 2);
select {
  case x = Channel.get(c1) => ...
  case Channel.put(c2, value) => ...
}

Not really a fan. But its an idea.

magnus-madsen commented 3 years ago

Proposal 2 using more keywords:

let c1 = Channel.bounded(5);
let c2 = Channel.unbounded();
recv c1;
send c2 v;
let n = Nursery.new();
spawn n (1 + 2);
select {
  case x = recv c => ...
  case c2 send v => ...
}

Also not a fan.

mlutze commented 3 years ago

I have a stylistic preference against multi-parameter keywords outside of infixes.

magnus-madsen commented 3 years ago

I have a stylistic preference against multi-parameter keywords outside of infixes.

Agreed. I don't like any of the proposals so far.

magnus-madsen commented 1 year ago

@paulbutcher Pinging you so you can have a quick laugh and maybe offer some suggestions for alternative channel syntax.

magnus-madsen commented 1 year ago

(Or perhaps you think the current syntax is OK)

JonathanStarup commented 1 year ago

I think definitely the chan <type> <size> should be changed. It feels foreign to Flix and its weird to have the type in the expression.

magnus-madsen commented 1 year ago

I think definitely the chan <type> <size> should be changed. It feels foreign to Flix and its weird to have the type in the expression.

It is very ugly!

paulbutcher commented 1 year ago

For my part, I have no problem with the current syntax with the single exception of channel creation as mentioned by @JonathanStarup above.

What would we think about:

chan[Int32](100)

Are there other options?

magnus-madsen commented 1 year ago

Here is my new proposal with some example code:

let c1 = Channel.bounded(5);
let c2 = Channel.unbounded();
Channel.send(c1, 123) // Note channel is first argument.
Channel.send(c2, 456)
Channel.recv(c1) 
Channel.recv(c2) |> Channel.send(c1) // Works because channel is first argument to send.

select {
  case x <- recv(c1) => ... // strange because recv is not imported, but okay...
  case y <- recv(c2) => ... // ... do we also allow Channel.recv(c2) ?
}

Why use <- in case? I think to highlight something "fishy" is going on; like with for-each and for-yield. Also I find it pleasing.

(Sidenote: Later I want to split channels into sender and receiver).

magnus-madsen commented 1 year ago

Rust style:

select {
  case recv(r1) -> x => ... 
  case recv(r2) -> y => ...
}

without case:

select {
  recv(r1) -> x => ... 
  recv(r2) -> y => ...
}
JonathanStarup commented 1 year ago

Note channel is first argument.

This doesn't allow c !> send(12) |> send(13). But Channel.recv(c2) |> Channel.send(c1) is nice

magnus-madsen commented 1 year ago

Note channel is first argument.

This doesn't allow c !> send(12) |> send(13)

Ah right, so you are saying the channel argument should be last and we should use !> instead?

JonathanStarup commented 1 year ago

Note channel is first argument.

This doesn't allow c !> send(12) |> send(13)

Ah right, so you are saying the channel argument should be last and we should use !> instead?

Maybe, its just what we discussed previously. It wouldn't allow element first in the pipe element |> ...

How would these functions be implemented? They have to return the fake Channel[a] type?

paulbutcher commented 1 year ago
let c1 = Channel.bounded(5);

How do we know what type the channel is?

magnus-madsen commented 1 year ago

How do we know what type the channel is?

From type inference, like everywhere else. Its a legacy thing that channels required types.

paulbutcher commented 1 year ago

Ah cool. Even better 👍

magnus-madsen commented 1 year ago

Maybe, its just what we discussed previously. It wouldn't allow element first in the pipe element |> ...

How would these functions be implemented? They have to return the fake Channel[a] type?

I don't understand at all :) Are you saying that |> solves our problems or no?

magnus-madsen commented 1 year ago

A bit more detail on the proposed API:

type alias Sender[t] = Channel[t]   // later Sender and Receiver will become actual types in the Flix compiler.
type alias Receiver[t] = Channel[t] // also later they will be equipped with regions.

namespace Channel {
  def buffered(n: Int32): (Sender[t], Receiver[t])
  def unbuffered(): (Sender[t], Receiver[t])

  def send(m: a, s: Sender[a]): Unit
  def recv(r: Receiver[a]): a

  def tryRecv(r: Receiver[a]): Option[a] // internally uses select with default case.
  // trySend does not exist because we do not support send in select (yet).

  def timeout(d: Duration): Receiver[Unit] // returns a channel that sends unit after `d`.
}

I wonder if we can support a drain operation, but that probably requires us to know when the channel is closed.

EDIT: timeout is like Timer, and I don't think we should have Ticker.

JonathanStarup commented 1 year ago

I don't understand at all :) Are you saying that |> solves our problems or no?

  1. If channel is last then you can easily send multiple things c !> send(1) !> send(2) |> send(3)
  2. If channel is first then you can send on a channel as a postfix operation complexComputation |> send(c)

1 is most consistent with the rest of flix

I don't think we should have Ticker

I like ticker, I feel many would have to implement it themselves otherwise. It seems common to do something every X time interval.

I wonder if we can support a drain operation

maybe def drainUntill(c: Channel[a], f: a -> Bool) or it could drain until no further elements are present (with select)

A bit more detail on the proposed API:

Is this a library based on the current one or a replacement? I guess we could remove the inbuilt channel type now right? Channels are really just a library, not an inbuilt thing (except expression shorhands)

magnus-madsen commented 1 year ago

What's the perceived (or actual) value in separating a channel into sender and receiver?

JonathanStarup commented 1 year ago

Its easier to reason about deadlocks I think. If a create an channel that's meant to signal you, I will now block if you spam the channel with things. If I can restrict you to receiving, I only have to make sure I don't block myself

magnus-madsen commented 1 year ago

Continued in https://github.com/flix/flix/issues/4755