Right now, as far as I understood the situation, resi files help the compiler to determine the public / private interfaces, and therefore helps the compiler finding connected modules that require recompilation. When everything is public by default, the compiler canāt tell which functionality is exposed to the outside, therefore it recompiles everything defensively.
Now, letās say we turn that around and make every entity, submodule, type etc. hidden by default, does that mean that even without resi files, weād get the same benefits of preventing unnecessary recompilations?
If thatās the case, that would mean we could shape the language in a way that doesnāt require any resi files. In a similar way as other modern languages like Rust would work.
This would give us a lot of benefits:
Documentation, annotations, etc could all be located in the implementation files.
Jump to definition in editor-support would be straight-forward.
There would be less questions on how to handle zero-cost bindings whenever I try to hide certain apis.
No weird switching between res and resi syntax rulesets.
No weird copying of type definitions between res and resi.
No weird confusing attempts on trying to write resi files on res files that use macros like react.component.
Easy distribution of single res files without the need for resi files whenever we want to hide stuff
Thatās why it would be interesting if this change would actually help the compiler making the same decisions on recompilation as if Iād use resi files.
I think the plan is to make it private by default not public
Reading your comment more fully. I did wonder if the natural follow on to this proposal, private by default would lead to the possibility of resi files being a lot more redundant. Good to see the benefits you listed. Itās a major change though considering not having interface files. So definitely worthwhile thinking it through.
module Button = {
@pub
@react.component
let make = () => {
<button/>
}
}
@pub
@react.component
let make = () => {
<div/>
}
Note that @pub would only apply to values, I think this is fine, since only values matter in JS, everything else is erased.
Since it is private by default for values, so for your module, if you dont add any attributes, it would be private since none of the values are going to be exported.
Right now, as far as I understood the situation, resi files help the compiler to determine the public / private interfaces, and therefore helps the compiler finding connected modules that require recompilation. When everything is public by default, the compiler canāt tell which functionality is exposed to the outside, therefore it recompiles everything defensively.
This is one step towards that. We need some more engineering work to normalize the interface.
For others:
This does avoid some use cases of resi, but there is no plan to remove resi since resi is more expressive and serve some advanced use cases and it is great for library documentation. The current issue to solve is that for application development, most people donāt bother to write resi
I donāt like the proposed design for multiple reasons:
Export visibility should be part of the language, and not just a decorator (indicated by a proper keyword). Even if weād modify our syntax, desugaring to a @pub attribute feels wrong (but I know that we use that approach for some other features already).
The inconsistency for types and values is a no-go for me. This is confusing and different to how it works in JS / TS. Even if types donāt matter much in the compile output, it matters much for encapsulation and program design.
Itās troublesome that resi files still provide some functionality that @pub canāt do (e.g. hiding types). The limitations mentioned above also indicate that we canāt replace one for another, therefore they must coexist and we have to learn multiple concepts to do kinda similar things. I am generally in favour of private-by-default, but if the design would end up like this, Iād rather keep using resi files until we get rid of all the inconsistencies in the proposed design.
Regarding the wording: I donāt think that pub is the right keyword to go. We actually want to compile to a JavaScript equivalent of export const foo. One of our core goals is to look familiar to JS users and also compile to readable JSā¦ so in that sense, export would be the only sensible wording here if you ask me.
(also, pub would kinda break wording consistency with any other keyword format we have right nowā¦ we dropped pub and pri when we removed the object system during the Reason ā ReScript transition, and I think this was a good thingā¦ why reintroduce it?)
This sounds like a lot of non-trivial work without any possible time estimation?
I wished the design would look more like what I drafted in my previous post, then it would have been easier to wait for a proper interface normalization mechanism even if it took several years to implement.
Does that include constructors for variants? Currently to represent an opaque type we have to do this:
module ValidEmail: {
type t
let make: string => option<t>
} = {
type t = string
let make = unvalidatedEmail => {
if Js.String.length(unvalidatedEmail) > 10 {
None
} else {
Some(unvalidatedEmail)
}
}
}
This works, but is a little bit verbose. Even more if we add more functions inside. But with an export, private, @pub or whatever syntax I could do something like this:
module ValidEmail = {
@unboxed
type t = ValidEmail(string)
// or @pub or export, or any syntax. IMO pub is concise.
pub let make = unvalidatedEmail => {
if Js.String.length(unvalidatedEmail) > 10 {
None
} else {
Some(ValidEmail(unvalidatedEmail))
}
}
}
This alone is greatly useful for making invalid states unrepresentable. In F# these are really easy to represent. Itās a pleasure to make these, as they are really syntax light.
In typescript aswell:
namespace Email {
type t = string & { readonly _: unique symbol }
export const create = (str: string) => {
if (str.length < 10) {
return str as t
}
return null
}
}
Agree with everything youāve said above @ryyppy!
I think @nkrkv makes a good point about exporting though; if I can export, why canāt I import and have to open or let {x} = module(Foo) instead?
I think itās worth touching on the sub-module example again:
// Demo.res
module Button = {
@react.component
let make = () => {
<button/>
}
}
Having to mark Button and Button.make as public is not ideal, but this may be a specific issue with React components. Components, from the consumerās perspective, are one unit ie. They are just <Demo.Button />. It could be confusing to require marking that singular unit as public in two different places (the module and the make function).