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.
Would the current idiomatic way to handle this be to have Pg.Client.connect!, et. al just accept a tcp_stream_write function?
Or should the type become something like this?
Client :: {
backend_key : Try(ProtocolBackend.KeyData, [Pending]),
stream_read! : _ => _,
stream_write! : _ => _,
}
So I did find this:
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"
}
}
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?
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)])
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
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
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!
Ok fair enough, I can appreciate that approach!
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:
All good :)
The zulip-export repo can also be useful to automate the search with an LLM.
yeah it's all good, thanks for posting about it! :smiley:
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)
yeah, I like that idea! Want to open an issue for it?
Sure!
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:
Here's what I found, though this references outdated syntax:
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:
I am guessing we just forgot about it :p
Do we still want to add x.module.mod_function support inside where @Richard Feldman?
oh we just ended up going with the syntax where we skip the .module part :smile:
so the type signature in that example would be:
conv : g -> h where [h.from : g -> h]
How do you call from though in the body of the function?
conv : g -> h where [h.from : g -> h]
conv = |arg| {
H : h
H.from(arg)
}
H : h creates a type alias of the h type variable that's in scope from the annotation
Ooooooh, that's nifty. Thanks :thumbs_up:
at which point you can call H.from normally
sure! :smiley:
Last updated: Jul 23 2026 at 13:15 UTC