RFC: private by default for values

do you have a spec of how this is going to work under the hood?

This is done in the type checking

And do the compiler changes impact the general performance of projects without .resi files?

The impact should be not observable, can you elaborate a bit why you have such concern?

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.

8 Likes

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.

1 Like

It would be great if you could expand on this. How would concrete code look like? What are the limitations?

So letā€™s say I create the following file:

// Demo.res
module Button = {
  @react.component
  let make = () => {
     <button/>
  }
}

@react.component
let make = () => {
  <div/>
}

So. What happens? I guess Demo.Button and make is hidden, unless I mark it as public:

export module Button = {
  @react.component
  let make = () => {
     <button/>
  }
}

@react.component
export let make = () => {
  <div/>
}

As we can see above, Demo.Button and Demo.make would now be visible. But not Demo.Button.make, we would need to export that explicitly?

This also means that types are hidden by default:

// private
type t

export type something

Here you can see that types are private, unless I mark them as public. Or something like that.

For more advanced features like Functors I guess one would continue to use module signatures as we did before (so nothing new to learn).


I guess this kind of ā€œspecā€ is what @Maxim is asking for.

Iā€™d imagine there are quite some edge-cases that need to be considered here.

4 Likes

I would expect the whole module Button to be private unless it is pub module Button.

3 Likes

How would concrete code look like?

It would be

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

4 Likes

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.

10 Likes

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).

Donā€™t we have pri and wouldnā€™t that be confusing with pub since they would be for doing two different things (pri being used in interfaces)?

What is the status of this?

3 Likes

Please tell me this is something that is actually coming!

Private by default is a far wiser way of designing modules. Itā€™s much like ā€œsecure by defaultā€.

2 Likes