Stream: beginners

Topic: How to define an interface?


view this post on Zulip Bryce Miller (Jul 12 2026 at 22:18):

In roc-pg, Pg.Client has this type:

    Client :: {
        stream : TcpStream,
        backend_key : Try(ProtocolBackend.KeyData, [Pending]),
    }

In the old implementation, TcpStream was just Tcp.Stream imported from the platform (not sure how that worked).
I was hoping I could just define TcpStream to be some type which has the methods write!, read!, etc. but I can't use a where clause on a type definition so I'm not sure how to do this, or whether it's possible.

view this post on Zulip Bryce Miller (Jul 12 2026 at 22:20):

Would the current idiomatic way to handle this be to have Pg.Client.connect!, et. al just accept a tcp_stream_write function?

view this post on Zulip Bryce Miller (Jul 12 2026 at 22:23):

Or should the type become something like this?

    Client :: {
        backend_key : Try(ProtocolBackend.KeyData, [Pending]),
        stream_read! : _ => _,
        stream_write! : _ => _,
    }

view this post on Zulip Bryce Miller (Jul 13 2026 at 00:52):

So I did find this: #beginners > library depending on platform types @ 💬

Looks like this is type checking:

    Client(stream) :: {
        backend_key : Try(ProtocolBackend.KeyData, [Pending]),
        stream : stream,
    }.{
        connect! : {
            host : Str,
            port : U16,
            user : Str,
            auth : [None, Password(Str)],
            database : Str,
            tcp_connect! : Str, U16 => Try(stream, _),
        } => Try(Client, _)
            where [stream.write! : stream, List(U8) -> Try({}, _)]
        connect! = |{ host, port, user, auth, database, tcp_connect! }| {
            stream = tcp_connect!(host, port)?
            stream.write!(ProtocolFrontend.startup({ user, database }))?
            crash "todo"
        }
    }

view this post on Zulip Bryce Miller (Jul 13 2026 at 11:22):

It looks like I have to repeat the where [stream.write! : stream, List(U8) -> Try({}, _)] for every function that needs to write to a TCP stream.

Am I missing a better way to do this?

view this post on Zulip Bryce Miller (Jul 13 2026 at 13:03):

My concern here is that I when I have a constraint like the one on stream.write! above, I want to use the same constraint for all functions. I don't want to accidentally have one helper with a different type signature on the constraint.

I tried this, but it doesn't seem to work:

send_with_sync! : tcp_stream, List(U8) => Try({}, _)
    where [tcp_stream.write! : TcpWrite(tcp_stream)]
send_with_sync! = |stream, bytes| {
    content = Bytes.Encode.sequence([bytes, ProtocolFrontend.sync])
    stream.write!(content)
}

TcpWrite(stream) : stream, List(U8) => Try({}, [SomeErr(Str)])

view this post on Zulip Anton (Jul 13 2026 at 13:52):

Am I missing a better way to do this?

I think there is no better way and I suspect it is a deliberate choice @Richard Feldman

view this post on Zulip Richard Feldman (Jul 13 2026 at 13:55):

we've talked about having aliases for this, e.g. so you could do where [tcp_stream.Write] (and then we could have a similar thing for .Eq and whatnot) but haven't implemented it yet

view this post on Zulip Richard Feldman (Jul 13 2026 at 13:57):

I wanted to try to see how it felt to do without it before adding it...I think for now it's best to try it as-is and then see how much of a pain point it is in practice after there are actual users of the package, and then we can reevaluate based on how things go in practice!

view this post on Zulip Bryce Miller (Jul 13 2026 at 14:01):

Ok fair enough, I can appreciate that approach!

view this post on Zulip Bryce Miller (Jul 13 2026 at 14:03):

Also, perhaps I should have spent more time digging through Zulip before asking this question. If so, I apologize. I did finally find the messages that explained the rationale behind disallowing where clauses for type declarations, it just took a bit of digging :sweat_smile:

view this post on Zulip Anton (Jul 13 2026 at 14:03):

All good :)

view this post on Zulip Anton (Jul 13 2026 at 14:04):

The zulip-export repo can also be useful to automate the search with an LLM.

view this post on Zulip Richard Feldman (Jul 13 2026 at 14:05):

yeah it's all good, thanks for posting about it! :smiley:

view this post on Zulip Bryce Miller (Jul 13 2026 at 14:08):

Do you think it might be worth adding something to the error message for where clauses on type declarations that suggests what to do instead? (put the where clauses on function annotations)

view this post on Zulip Richard Feldman (Jul 13 2026 at 14:08):

yeah, I like that idea! Want to open an issue for it?

view this post on Zulip Bryce Miller (Jul 13 2026 at 14:09):

Sure!

view this post on Zulip Jared Ramirez (Jul 13 2026 at 18:23):

Bryce Miller said:

Also, perhaps I should have spent more time digging through Zulip before asking this question. If so, I apologize. I did finally find the messages that explained the rationale behind disallowing where clauses for type declarations, it just took a bit of digging :sweat_smile:

if you still have the tab or thread name, can you link it here too? to make it a bit easier for someone searching for this same thing in the future :smile:

view this post on Zulip Bryce Miller (Jul 13 2026 at 18:49):

Here's what I found, though this references outdated syntax: #ideas > where clause changes @ 💬

view this post on Zulip Jonathan (Jul 14 2026 at 11:00):

Also in that where-clause thread - is the .module lookup syntax on pause? Concretely, I have wanted to be able to generically use the default function of numbers, or the highest and lowest associated with their modules.

# Ofc, you could just have an extra argument for the fill value
MyContainer.fill(n) : () -> MyContainer(t) where [t.module.default : () -> t]

The other one I had in mind was generic conversions

A.to_b : A -> B
B.from : x -> B where [x.to_b : x -> B]
conv : g -> h where [h.module.from : g -> h]

which may not be something you want to allow anyhow :sweat_smile:

view this post on Zulip Anton (Jul 15 2026 at 11:47):

I am guessing we just forgot about it :p
Do we still want to add x.module.mod_function support inside where @Richard Feldman?

view this post on Zulip Richard Feldman (Jul 15 2026 at 13:07):

oh we just ended up going with the syntax where we skip the .module part :smile:

view this post on Zulip Richard Feldman (Jul 15 2026 at 13:08):

so the type signature in that example would be:

conv : g -> h where [h.from : g -> h]

view this post on Zulip Jonathan (Jul 15 2026 at 13:09):

How do you call from though in the body of the function?

view this post on Zulip Richard Feldman (Jul 15 2026 at 13:13):

conv : g -> h where [h.from : g -> h]
conv = |arg| {
    H : h

    H.from(arg)
}

view this post on Zulip Richard Feldman (Jul 15 2026 at 13:13):

H : h creates a type alias of the h type variable that's in scope from the annotation

view this post on Zulip Jonathan (Jul 15 2026 at 13:13):

Ooooooh, that's nifty. Thanks :thumbs_up:

view this post on Zulip Richard Feldman (Jul 15 2026 at 13:14):

at which point you can call H.from normally

view this post on Zulip Richard Feldman (Jul 15 2026 at 13:14):

sure! :smiley:


Last updated: Jul 23 2026 at 13:15 UTC