The Only Real Design Point in PORT!: Implicit PORT!s

Thanks to what I've been finally getting around to looking at...I can guarantee you that I now officially know more about Rebol PORT!s than anyone in the world. (Which is not really anything to brag about.)

:worried: :globe_with_meridians: :see_no_evil:

Due to what I've learned, I can tell you that my long-ago suspicions turned out to be completely correct. There is only one idea to salvage: implicit ports.

Though It's Only One Idea, It's a Pretty Good One

This feature is PORT!s saving grace. So why would we settle for this wimpy behavior?

r3-alpha>> append %foo.txt "abc"
== %foo.txtabc

That's lackluster! So it needs to act like:

let p: open/write/new %foo.txt
append p "abc"
close p

But not every operation that receives a FILE! as a first argument would turn it into a PORT!. For instance, JOIN would not:

>> join %foo/ "abc.txt"
== %foo/abc.txt

Unlike APPEND, JOIN doesn't take PORT!s as a first argument. And it would explicitly take FILE! as an argument. Hence the rule doesn't apply... the enforcement of implicit port behavior only applies when an argument can be a PORT!, e.g. port actions.

Does this mean you can't write a PORT! action called JOIN? I think you could write one if you wanted. But that's not what comes in the box... the JOIN you get in LIB is just a function.

Complexities of the OPEN and CLOSE Keywords

A rather important detail that is overlooked (most of the time) in the discussions of implicit ports is that they're not just [OPEN -> ACTION -> CLOSE]. The OPEN call is different based on the action!

When you say read %foo.txt what you are actually saying is equivalent to:

port: open/read %foo.txt  ; notice the /READ
data: read port
close port

While if you say append %foo.txt "appendme" you're saying:

port: open/write %foo.txt  ; notice the /WRITE
append port "appendme"
close port

When you say write %foo.txt "writeme" what you are actually saying is:

port: open/write/new %foo.txt   ; notice the /WRITE *and* the /NEW
write port "writeme"
close port

This was bumming me out a bit, because it means that every port action gets wound up in having to implement the implicit port behavior.

The involvement of the actions in their decisions about what to do in implicit port behavior seems inevitable. But I think we have to look at things a slightly different way.

Let's instead say operations like APPEND and WRITE and READ are always ready to operate on PORT!s that have only gone through MAKE PORT!, and do the opening and closing themselves. Then we get something that looks like this:

read %foo.txt
port: make port! %foo.txt
read port

write %foo.txt
port: make port! %foo.txt
write port "writeme"

append %foo.txt "appendme
port: make port! %foo.txt
append port "appendme"

Arguably even more satisfying, this reduces down to:

read %foo.txt
read (make port! %foo.txt)

write %foo.txt
write (make port! %foo.txt) "writeme"

append %foo.txt "appendme
append (make port! %foo.txt) "appendme"

This Formulation Has a Perk!

The verbs OPEN and CLOSE have been removed from the picture for implicit ports.

Here it says that if a port has gone through MAKE and it's specified, it's ready to use...and it's those who have an interest in doing longer-spanning operations who get involved with concepts like OPEN or LOOKUP or CONNECT or CLOSE.

Maybe you have a door:// scheme, and OPEN and CLOSE mean something completely different. When the implicit port formulation has dropped the idea of involving open and close, then there's no real reason you can't say close door://garage and have that be able to mean actually closing the garage door...without tripping up the implicit ports.

Pitfalls Of Operating on "Merely Made" PORT!s?

Looked at this way, we see implicit ports as being the willingness of various port types to interpret actions on a port that has been specified, but not opened.

This could cause confusion:

>> write %hello.txt "Hello World"

>> p: make port! %hello.txt

; the port has gone through MAKE and not OPEN
; so a READ is interpreted as opening the file, reading, closing
>> read/part p 5
== "Hello"

>> read/part p 5
== "Hello"  ; did not remember its position as an opened port would

If we allow you to do READ or WRITE from a port that is not opened and forget the state every time, that provides a good mechanic for implicit ports...but could be confusing.

We could say that if a port goes through an implicit opening and closing, it keeps its current position as an offset. Whereas going through CLOSE and OPEN would reset that.

It's something to think about. Generally speaking, I don't know how many cycles of concern should be spent on people using low level MAKE PORT! operations instead of OPEN.

Could We Generalize This Beyond PORT! Actions?

We might be tempted to say that any function argument that can take a PORT! would be able to harness implicit port semantics. What about this:

read-two-lines: func [p [port!]] [
    collect [
        keep read/line p
        keep read/line p

Wouldn't it be nice if you could say:

read-two-lines %some-file.txt

...and if the opening and closing were handled for you automatically?

There's some friction there, if the port is designed to be returned. Consider this:

read-line-and-wait: func [
    return: [port!]
    p [port!]
    line: read/line p
    wait 10
    return p

So if you say read-line-and-wait %some-file.txt, then if the implicit-port mechanic closed the port after the READ-LINE-AND-WAIT call was over, the port given back would wind up being closed.

Besides being returned, the port could have been put anywhere...a global variable, poked into some object. Closing it upon return from READ-LINE-AND-WAIT may not be what was wanted.

You can't really get good automatic behavior in these situations unless you have some kind of good "RAII" mechanic.

Rebol's very loose language semantics really make it tough to do good features in this vein.

Of course no language is perfect here, you often have to break the paradigm to solve real problems. Not everything in Rust can be done under the purview of the borrow-checker, for example, so you end up with atomic reference-counted "Arc"...and get cycles, etc.

But Rebol is down in the dumps with the worst languages in this regard. Which is something I've said is worth stopping to think about. BLOCK!s and WORD!s and such could be handled differently from OBJECT!s and PORT!s...and we could have a borrow checker too! If it's a good idea it should be thought about.

Moral of the Story Is: Implicit Ports Are A Keeper

By hook or by crook, we should make them work...because having the choice to operate with them implicitly or explicitly is a good idea.