chapel icon indicating copy to clipboard operation
chapel copied to clipboard

writeln, spaces, and literals

Open mppf opened this issue 6 years ago • 7 comments

It's common to see Chapel code like this:

writeln(a, " ", b, " ", c);

because otherwise, writeln will glom together the output:

writeln(1, 2, 3); // 123

In Python 3, the print function automatically adds spaces between multiple elements:

$ python3
>>> print(1,2)
1 2

Could Chapel do this as well?

The immediate counter-argument is something like this - well, wouldn't printing with a different separator become awkward? Like the following:

writeln(1, "+", 2, "+", 3); // 1+2+3 or 1 + 2 + 3 ?

My first reaction would be that writeln could recognize an ioLiteral and in that event not add spaces between arguments:

writeln(1, new ioLiteral("+"), 2, new ioLiteral("+"), 3); // 1+2+3

Then, my second thought is that string literals could be treated automatically as ioLiterals within writeln. That would make the first "+" case above be equivalent to the second. Additionally, it would make

readln(int1, "+", int2, "+", int3);

actually function reasonably, because the "+" will be interpreted as delimeters, rather than being a compilation error (can't read into a string literal).

mppf avatar Sep 16 '19 14:09 mppf

As someone who used both Python's print and Chapel's writeln, I very much like Chapel's writeln, personally. I believe 'writeln' gives more control without having to go through additional steps (i.e. you have to pass something to optional sep argument in Python's print). So, I'd prefer things to stay the same. I don't mind adding spaces manually as much as I mind trying to remove them.

However, I am more against the second part of the proposal where we are special-casing string literals. It feels like one more quirk to learn without too much benefit (to me, at least).

If I have an application where I have a lot of stuff similar to something like:

writeln(myData1, ",", myData2, ",", myData3);

and, say, I decided to make my separator a configuration constant:

config const sep = ",";
writeln(myData1, sep, myData2, sep, myData3);

A similar refactor in the world where we special-case string literals would be a lot more work. I may be missing something here, but regardless the direction seems to be too open for confusion to me.

e-kayrakli avatar Sep 16 '19 16:09 e-kayrakli

I like the control writeln() gives now. If I want to concatenate three strings, I can do that. If I want to separate them with spaces, I can do that.

Having special behavior depending on whether a string is a literal or not seems surprising and weird. And could trigger unexpectedly, especially if a param string counts as a literal.

How about a new function that acts like python's print() and always adds spaces?

Or a version of string.join() that accepts any args that writeln() accepts? -- writeln(" ".join(1, 2, 3)); seems flexible and expressive without much extra typing. It has the drawback that it doesn't work today as join() accepts only strings. (Or " ".writeln(1, 2, 3)? I'm not sure how I feel about that.)

Or give writeln() (and readln()?) a named argument writeln(1, 2, 3, delim=" ")?

cassella avatar Sep 18 '19 06:09 cassella

Or give writeln() (and readln()?) a named argument writeln(1, 2, 3, delim=" ")?

This sounds like a good direction to me.

e-kayrakli avatar Sep 18 '19 16:09 e-kayrakli

When I started reading this, I thought @mppf was going to propose that we support a print() routine that would put a space delimiter between its arguments. I'm far less opposed to that than I am to changing the behavior of writeln() at this point in time. I'm also open to the optional named argument change that @cassella proposes (whether on write[ln] or on print).

bradcray avatar Sep 23 '19 17:09 bradcray

FWIW I would probably prefer a print routine that adds a space delimiter; as opposed to the separator argument... if I have to choose just one.

mppf avatar Sep 23 '19 20:09 mppf

Between the two options, I'd probably (weakly) prefer the separator approach because it'll be hard to remember whether writeln or println emits a space separator when jumping between different languages.

BryantLam avatar Sep 28 '19 03:09 BryantLam

If available, I think I would definitely use a routine like print() or println() that puts spaces between items, because I often forget to put additional parentheses in write((...)) (and notice that after looking at the output).

My empirical impression of the routine names "write" vs "print" is that "write" is somewhat more machine-oriented with support of file output, while "print" is more standard-output oriented and often inserts spaces between items (for more readability). But this varies depending on languages, so just my impression...

ty1027 avatar Aug 06 '24 06:08 ty1027

Using the workaround I posted about in https://github.com/chapel-lang/chapel/issues/17188#issuecomment-2638116448, I can make a writeln(..., sep=" ") work (similar to the proposals in https://github.com/chapel-lang/chapel/issues/25695 and how python works).

writeln("Hello", "There", sep=" "); // prints 'Hello There'

var x = 10, y = 20, z = 30;
writeln(x, y, z, sep=", "); // prints '10, 20, 30'

jabraham17 avatar Feb 06 '25 01:02 jabraham17

@jabraham17 : Very cool! t

Related: @benharsh and I also wanted this sort of capability in order to use a different ser/deser in a given writeln over on #23528 (or maybe I was the only one wanting it and he kindly filed the issue for me?). I'm trying to guess how easy or hard it would be to have writeln() support two optional trailing arguments. That is, do we end up needing a geometric number of overloads, or can we do it with a similar number as what you have in your sketch above.

bradcray avatar Feb 06 '25 01:02 bradcray

I'm trying to guess how easy or hard it would be to have writeln() support two optional trailing arguments. That is, do we end up needing a geometric number of overloads, or can we do it with a similar number as what you have in your sketch above.

To make it work with this workaround, it required more overloads. Note that extra where clauses, otherwise it does not work. If mySerializer or sep are fully generic, it still compiles but it is incorrect (the kwargs get mixed with the varargs). Putting a small restriction on their types (for sep, a string. for mySerializer, any record) fixes that.

pragma "last resort"
@chpldoc.nodoc
proc fileWriter.writeln(const args ...?k, mySerializer:?t=none) throws where isRecordType(t) {
  this.writelnHelper(args, none, mySerializer);
}

pragma "last resort"
@chpldoc.nodoc
proc fileWriter.writeln(const args ...?k, sep:string="") throws {
  this.writelnHelper(args, sep, none);
}

pragma "last resort"
@chpldoc.nodoc
proc fileWriter.writeln(const args ...?k, mySerializer:?t=none, sep:string="") throws where isRecordType(t) {
  this.writelnHelper(args, sep, mySerializer);
}

jabraham17 avatar Feb 06 '25 16:02 jabraham17