gleam-lang / gleam

⭐️ A friendly language for building type-safe, scalable systems!
https://gleam.run
Apache License 2.0
17.8k stars 745 forks source link

Re-export types and values from other modules #614

Open lpil opened 4 years ago

lpil commented 4 years ago

It would be nice to be able to re-export a type or function from another module.

Here's an idea for a syntax:

// one.gleam
pub type MyType {
  MyConstructor
}

pub fn my_function() -> Nil {
  Nil
}
// two.gleam

import two.{pub MyType, pub MyConstructor, pub my_function}

Questions

Should it be permitted to export only a subset of a type's constructors? This would mean it would not possible for users to pattern match on all records as only some have been exported. The above syntax would allow that.

CrowdHailer commented 4 years ago

I like the syntax, but only rexporting some constructors would be very awkward. (i'm sure I was discussing this somewhere previously but canot find it)

I'm keen that import option.{Option} should import Some and None, but I understand that it would mean you couldn't just search in a file to see where the Some and None constructors where defined.

lpil commented 3 years ago

Let's use the syntax above as there's no other suggestions or major complaints.

Things to consider:

lpil commented 1 year ago

Should it be permitted to export only a subset of a type's constructors? This would mean it would not possible for users to pattern match on all records as only some have been exported. The above syntax would allow that.

This makes me think of @hayleigh-dot-dev's suggestion to use a nested syntax for importing record constructors. I previously dismissed it but there's a second reason to have it here.

lpil commented 1 year ago

Adding to v1 as this may require syntax changes

pvdrz commented 1 year ago

I'd favor having the pub as a qualifier for import instead as it would be easier to read which imports are public and which ones are not:

import two.{pub MyType, my_function, pub MyConstructor}

vs

pub import two.{MyType, MyConstructor}
import two.{my_function}
inoas commented 1 year ago

I'd also prefer to have separation between pub and non-pub imports as @pvdrz shows. Reasoning, it may be a bit more verbose but:

timjs commented 1 year ago

Fully agree with @pvdrz and @inoas here for reasons they already stated!

inoas-nbw commented 1 year ago

I'd love to import/export all functions / all types except a list (opt-out instead of opt-in).

lpil commented 1 year ago

We won't have that I'm afraid, it will always be explicit.

inoas commented 1 year ago

another question I'd have is: if you re-export a function (or type) but also define it in the same file, what happens? imho compiler error or newly defined is being exported > reexported is being exported

lpil commented 1 year ago

Yup same as today, it would be a compile error as two items cant have the same name.

lpil commented 1 year ago

Considering pub import a.{b} and import a.{pub b} syntaxes.

It seems that pub import a.{b} implies you can import a from this module in some fashion.

pub import means that we have to permit module a module to be imported multiple times, or would the programmer be forced to pub import a.{b} as _ to not assign a name? What would the rules be here?

massivefermion commented 11 months ago

Sorry to bring this up again but I think it's relevant to your comment. I've said it before that to my brain, import a.{b, c} suggests that I'm only importing b and c. But gleam imports a too.

This is the actual root of the ambiguity you're talking about, not pub. Because even with import a.{pub b}, a is still in scope. So even with import a.{pub b}, you should still resolve that ambiguity.

I think given the circumstance, whether we use pub import a.{b} or import a.{pub b}, we should have as _ to make it clear to the developer that only b is being reexported. But then it would raise the question, are we reexporting a too if as _ is not used? It's important to realize that even if we're not doing that, this would be a question that will come to the developer's mind, which is not ideal.

I know introducing new syntax/keywords is frowned upon, but given the mishmash of all the ambiguities here, the best and most clear option is to have a new keyword for this feature:

export a.{b, type MyType, MyConstructor}

This would mean that if you want to use something in the module but also reexport it, you would need to use both import and export. This may seem more verbose, but is far more clear as to what is actually going on. This would also remove the need to use pub and also allows us to do anything we want with this feature regardless of how import is behaving.

P.S. The more I think about it, the more I realize what I said is not completely true. import a.{pub b, c} is more clear and to some extent pub import a.{b} can be considered as the real problem(which pub import a.{b} as _ does not solve!), but I still think a new keyword for this feature is better:

import a.{pub type MyType, type MyType2, pub MyConstructor, MyConstructor2, pub func1, func2}

vs

import a.{type MyType, type MyType2, MyConstructor, MyConstructor2, func1, func2}
export a.{type MyType, MyConstructor, func1}

To my eyes there is not even a competition here, the second one is more clear and less convoluted. You don't have to read through the imports and parse out which one is reexported and which one is not! Also, if you want to know what is used in the current module, you go to imports, if you want to know what this module is reexporting, you go to exports, clear separation of concerns.

lpil commented 11 months ago
import a.{type MyType, type MyType2, MyConstructor, MyConstructor2, func1, func2}
export a.{type MyType, MyConstructor, func1}

This seems to imply you can re-export a type without importing it. Is that right?

massivefermion commented 11 months ago

Yes, I think that would be more understandable and I don't see any issues. But if you think that may cause issues, you could make it a compile error if something that is not in scope is being exported. Although, that would couple the behavior of import and export to some extent, which is not ideal.

lpil commented 11 months ago

To import something it should also be brought into scope as otherwise you'd have a situation in which you can import an item from a module when its not actually in that module's scope.

dmmulroy commented 7 months ago

Considering pub import a.{b} and import a.{pub b} syntaxes.

It seems that pub import a.{b} implies you can import a from this module in some fashion.

pub import means that we have to permit module a module to be imported multiple times, or would the programmer be forced to pub import a.{b} as _ to not assign a name? What would the rules be here?

What if rather than this syntax and trying to "overload" or extend what "import" we introduced something like

include from module { type X, Y }

that would work like an explicit version of OCaml's module inclusion: https://ocaml.org/docs/modules#module-inclusion

This would prevent the confusion of importing a module multiple times or having to rename via as. It would also have the benefit of not having to deal with introducing the pub keyword to imports.

// foo.gleam
pub type Foo {
  Foo(String)
}

pub fn print_foo(foo: Foo) -> Nil { ... }

// bar.gleam
import project/foo.{print_foo}
include from project/foo { type Foo, Foo }

pub type Bar {
  Bar(String)
}

// baz.glem
import project/bar.{type Foo, type Bar, Foo, Bar}
...
leostera commented 7 months ago

You could probably get away just by swapping the keyword from import to include, or potentially even a pub modifier on the entire import:

import project/foo.{print_foo}
pub import project/foo.{ type Foo }
dmmulroy commented 7 months ago

I think the problem with pub import project/foo.{ type Foo } is that it bring foo into scope. So potentially just swapping import with include is enough to change the semantics.

include project/foo.{ type Foo }

You could use the pub keyword to effectively do the same, but I think having a difference between import and include is nice and might be more clear they work differently

iacore commented 7 months ago

Referencing https://github.com/gleam-lang/gleam/issues/2712#issuecomment-1998681052, we would not have this problem if types are first-class in Gleam.

Should it be permitted to export only a subset of a type's constructors? This would mean it would not possible for users to pattern match on all records as only some have been exported. The above syntax would allow that.

Having constructor not qualified by the type name is a bit weird here. If Some is normally used as Option::Some, then only Option need to be imported.

lpil commented 7 months ago

@iacore If you have a proposal for a language addition please write and share the proposal in GitHub discussions, thank you.

iacore commented 7 months ago

Is this invalid or too verbose?

import two.{MyType, MyConstructor, my_function}

pub type MyType = two.MyType
pub const MyConstructor = two.MyConstructor
pub const my_function = two.my_function
lpil commented 1 week ago

Could work! That design seems to imply that one could export a subset of the constructors, or use different names. Those might not be desirable properties.