10

How do I structure Raku code so that certain symbols are public within the the library I am writing, but not public to users of the library? (I'm saying "library" to avoid the terms "distribution" and "module", which the docs sometimes use in overlapping ways. But if there's a more precise term that I should be using, please let me know.)

I understand how to control privacy within a single file. For example, I might have a file Foo.rakumod with the following contents:

unit module Foo;

sub private($priv) { #`[do internal stuff] }

our sub public($input) is export { #`[ code that calls &private ] }

With this setup, &public is part of my library's public API, but &private isn't – I can call it within Foo, but my users cannot.

How do I maintain this separation if &private gets large enough that I want to split it off into its own file? If I move &private into Bar.rakumod, then I will need to give it our (i.e., package) scope and export it from the Bar module in order to be able to use it from Foo. But doing so in the same way I exported &public from Foo would result in users of my library being able to use Foo and call &private – exactly the outcome I am trying to avoid. How do maintain &private's privacy?

(I looked into enforcing privacy by listing Foo as a module that my distribution provides in my META6.json file. But from the documentation, my understanding is that provides controls what modules package managers like zef install by default but do not actually control the privacy of the code. Is that correct?)

[EDIT: The first few responses I've gotten make me wonder whether I am running into something of an XY problem. I thought I was asking about something in the "easy things should be easy" category. I'm coming at the issue of enforcing API boundaries from a Rust background, where the common practice is to make modules public within a crate (or just to their parent module) – so that was the X I asked about. But if there's a better/different way to enforce API boundaries in Raku, I'd also be interested in that solution (since that's the Y I really care about)]

codesections
  • 8,900
  • 16
  • 50
  • 4
    I don't believe that you can. One thing to do could be to (a) mark them as `is implementation-detail` (which signals *here be dragons*) and (b) only provide the sub via an export with a package key like, `use Foo::Secret :I-hereby-understand-that-foo-secret-is-designed-for-internal-use-only-and-agree-to-in-hold-the-module-author-harmless-for-any-and-all-damages-thereby-caused-in-sæcula-sæculorum`, or something similarly cleverly written (you are a lawyer after alll) – user0721090601 Mar 16 '21 at 03:38
  • Another alternative could be to `EVAL` from a resources file it into a `my` scoped value, but I've had issues with storing code blocks made that way in precompiled files so you might have to have that done at run time, losing the benefits you're no doubt wanting (but maybe jnhtn's new dispatch stuff will have a sideeffect of fixing that) – user0721090601 Mar 16 '21 at 03:48

3 Answers3

11

I will need to give it our (i.e., package) scope and export it from the Bar module

The first step is not necessary. The export mechanism works just as well on lexically scoped subs too, and means they are only available to modules that import them. Since there is no implicit re-export, the module user would have to explicitly use the module containing the implementation details to have them in reach. (As an aside, personally, I pretty much never use our scope for subs in my modules, and rely entirely on exporting. However, I see why one might decide to make them available under a fully qualified name too.)

It's also possible to use export tags for the internal things (is export(:INTERNAL), and then use My::Module::Internals :INTERNAL) to provide an even stronger hint to the module user that they're voiding the warranty. At the end of the day, no matter what the language offers, somebody sufficiently determined to re-use internals will find a way (even if it's copy-paste from your module). Raku is, generally, designed with more of a focus on making it easy for folks to do the right thing than to make it impossible to "wrong" things if they really want to, because sometimes that wrong thing is still less wrong than the alternatives.

Jonathan Worthington
  • 29,104
  • 2
  • 97
  • 136
  • 1
    > "no matter what the language offers, somebody sufficiently determined to re-use internals will find a way (even if it's copy-paste from your module)." That's an entirely fair point. I guess I'm coming at it from a [Hyrum's law](https://www.hyrumslaw.com/) point of view: if someone copy-pastes my code (and I hope they do, or at least read it!), then they have a _fork_, and there's no way changes to my implementation details can break their code. But if they ignore my "strong hints" not to use something, then I can break them – that's now part of my API in a Hyrum's Law sense. – codesections Mar 16 '21 at 13:34
  • I'm still partial to my legal disclaimer export tag ;-) – user0721090601 Mar 16 '21 at 13:34
6

Off the bat, there's very little you can't do, as long as you're in control of the meta-object protocol. Anything that's syntactically possible, you could in principle do it using a specific kind of method, or class, declared using that. For instance, you could have a private-class which would be visible only to members of the same namespace (to the level that you would design). There's Metamodel::Trusting which defines, for a particular entity, who it does trust (please bear in mind that this is part of the implementation, not spec, and then subject to change).

A less scalable way would be to use trusts. The new, private modules would need to be classes and issue a trusts X for every class that would access it. That could include classes belonging to the same distribution... or not, that's up to you to decide. It's that Metamodel class above who supplies this trait, so using it directly might give you a greater level of control (with a lower level of programming)

jjmerelo
  • 22,578
  • 8
  • 40
  • 86
  • Using `trust` (and the `Metamodel::Trusting`) is very difficult, because things must be done at compile time and require forward declarations of some type (see https://tio.run/##RY6xCsIwFEX3fMUtZGiXDCIOKQ4pDgqOipsS2icdWluaiJXSf/Fb/LEYmhbHdw733tdSV22cyyttDBQGBpTagEeG8o5s6u@abNkUyM6H4y6Wi0kwjH9pu6exiHnvMTwGDFV3cdVFcZsckZfislenxOuRjYyFzWwOzEXUt42huen7MfoN3kdKyrAakrWHGlso8aCXhFyvlm@VmNYkspRlIpTFXCfO/QA for how it would look at runtime — it doesn't work, though, because `^add_trustee` is a no-op at runtime) – user0721090601 Mar 16 '21 at 07:13
  • @user0721090601 but I think that the OP does not mention explicitly runtime. Theoretically, since it's done at the distribution level, it could be done at compile time, right? – jjmerelo Mar 16 '21 at 07:16
  • 1
    Compile time is tricky because each module needs to reference the other. That's impossible to do entirely at compile time because it'd create a circular dependency. Maybe at a CHECK stage you could do an indirect reference in the main (non-private) module, but I'm not sure how well that reference would work. (I haven't tested it) – user0721090601 Mar 16 '21 at 13:23
  • @user0721090601 you can still use the MOP at runtime... – jjmerelo Mar 16 '21 at 16:33
3

There is no way to enforce this 100%, as others have said. Raku simply provides the user with too much flexibility for you to be able to perfectly hide implementation details externally while still sharing them between files internally.

However, you can get pretty close with a structure like the following:

# in Foo.rakumod
use Bar;
unit module Foo;

sub public($input) is export { #`[ code that calls &private ] }
# In Bar.rakumod
unit module Bar;

sub private($priv) is export is implementation-detail {
    unless callframe(1).code.?package.^name eq 'Foo' {
        die '&private is a private function.  Please use the public API in Foo.' }
    #`[do internal stuff] 
}

This function will work normally when called from a function declared in the mainline of Foo, but will throw an exception if called from elsewhere. (Of course, the user can catch the exception; if you want to prevent that, you could exit instead – but then a determined user could overwrite the &*EXIT handler! As I said, Raku gives users a lot of flexibility).

Unfortunately, the code above has a runtime cost and is fairly verbose. And, if you want to call &private from more locations, it would get even more verbose. So it is likely better to keep private functions in the same file the majority of the time – but this option exists for when the need arises.

codesections
  • 8,900
  • 16
  • 50
  • 2
    "There is no way to enforce this 100%, as others have said. Raku simply provides the user with too much flexibility" That summary seems profoundly misleading. You could say the same thing of *any* PL. A Haskell module will be subject to exactly the same sort of considerations; as jnthn noted -- as a key point, not a throw away point -- folk can always cut/paste code. So what can a PL do? Raku does the *opposite* of what you say. It provides the flexibility of a Turing complete GPL, which is to say total flexibility, which is true of any other GPL, but *limits* what is *ordinarily* available. – raiph Mar 16 '21 at 16:59
  • 2
    To avoid the run time cost, in Foo you could do `my &private = Bar::get-private; sub public ($input) is export { private }` and then in Bar, use `my sub private { … }; method get-private is implementation-detail { die "NO" unless callframe(1).code.?package.^name eq 'Foo'; &private }` (untested code, might be a typo in there). That way the run time cost is much smaller (only once checking the stack). – user0721090601 Mar 16 '21 at 17:07
  • "a determined user could overwrite the `&*EXIT` handler!". Even if the user could *not* overload the `&*EXIT` handler they could just copy all of your modules, make whatever changes they wish to make, and even publish them in the ecosystem under precisely the same names. – raiph Mar 16 '21 at 17:08
  • "as a key point, not a throw away point -- folk can always cut/paste code." @raiph, I disagree that that's key (as I mentioned to jnthn earlier). Yes, someone can fork the code, but then they have _a different program_ and can do whatever they want with it. API privacy is about not breaking users code, and someone who copy-pastes my code will never have there code broken by any changes I make. And, sure, warnings can help discourage people from relying on implementation details. But, at the end of the day, they still will, and I still won't want to break their code. – codesections Mar 16 '21 at 17:29
  • "I disagree that that's key (as I mentioned to jnthn earlier)." That's fair enough. But I think you're wrong, and instead agree with jnthn. "someone who copy-pastes my code will never have their code broken by any changes I make." If they cut/paste, and then do so again to track your changes, their code may break due to your changes. If devs deliberately ignore your public API, they knowingly break the contract. And you can't stop them doing whatever they want to do. All you can do is make your end of the contract clear. You can add lawyers, and a police force too, but why? – raiph Mar 16 '21 at 18:54