gleam icon indicating copy to clipboard operation
gleam copied to clipboard

Re-export types and values from other modules

Open lpil opened this issue 4 years ago • 23 comments

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.

lpil avatar May 28 '20 20:05 lpil

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.

CrowdHailer avatar Jun 01 '20 15:06 CrowdHailer

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

Things to consider:

  • When reexporting a function we need to generate a function definition for it.

lpil avatar Nov 30 '20 20:11 lpil

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 avatar Feb 02 '23 15:02 lpil

Adding to v1 as this may require syntax changes

lpil avatar Jun 29 '23 15:06 lpil

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}

pvdrz avatar Jun 30 '23 01:06 pvdrz

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:

  • it far far better to read and
  • the formatter could group into two parts
    1. alphabetically sorted pub imports and
    2. alphabetically sorted non-pub imports.

inoas avatar Jul 01 '23 09:07 inoas

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

timjs avatar Jul 04 '23 05:07 timjs

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

inoas-nbw avatar Aug 17 '23 09:08 inoas-nbw

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

lpil avatar Aug 18 '23 11:08 lpil

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

inoas avatar Aug 18 '23 13:08 inoas

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

lpil avatar Aug 18 '23 13:08 lpil

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?

lpil avatar Oct 06 '23 13:10 lpil

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.

massivefermion avatar Nov 09 '23 03:11 massivefermion

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?

lpil avatar Nov 09 '23 14:11 lpil

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.

massivefermion avatar Nov 09 '23 15:11 massivefermion

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.

lpil avatar Nov 16 '23 10:11 lpil

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

dmmulroy avatar Mar 10 '24 23:03 dmmulroy

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 }

leostera avatar Mar 11 '24 11:03 leostera

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

dmmulroy avatar Mar 11 '24 15:03 dmmulroy

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.

iacore avatar Mar 15 '24 16:03 iacore

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

lpil avatar Mar 17 '24 16:03 lpil

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

iacore avatar Mar 19 '24 19:03 iacore