The only thing I hate in purescript and haskell - imports (discussion)

Polls

Problem

  1. DuplicateModule error is possible when I add two libs and they define the same module (Data.Percent for example)

  2. I have to write

module Foo.Bar where

all the time. Why should I write it when it can be perfectly derived from file path src/Foo/Bar.purs

  1. When I import module
import Data.String.Extra

I don’t actually know the name of the module. Is it purescript-strings OR purescript-strings-extra? Or maybe it’s even purescript-string?


Proposal

  1. Make all imports qualified by package name, i.e. it should be not possible to write non-package-name-qualified import
  2. Make module name derivable from path
  3. enforce domain - reason in this issue
  4. allow relative imports within the same package

spago.dhall before

{ name = "my-app"
, dependencies = [ "strings", "strings-extra", "other-strings" ]
, packages = ./packages.dhall
, sources = [ "src/**/*.purs", "test/**/*.purs" ]
}

spago.dhall after

-- this is a name of a package that is published to package-sets / pursuit
-- it's fixed to "publishedSource" dir below, this dir won't be ignored
{ name = "my-app"
, domain = "me" -- enforce domain / namespace
, dependencies = [ "@core/strings", "@contrib/strings-extra", "@noname/other-strings" ]
, packages = ./packages.dhall
, publishedSource = "src" -- this is what end up in a package in pursuit
, localPackages =
    -- these dirs are not publised to pursuit, they are ignored to preserve space (if we need to)
    -- type in haskell - `type LocalPackages = Map String String`
    [ { packageName = "my-app-test", directory = "test" } -- imported as `import "@me/my-app-test/..."`
    , { packageName = "my-app-bench", directory = "bench" } -- imported as `import "@me/my-app-bench/..."`
    , { packageName = "my-app-demo", directory = "demo" } -- imported as `import "@me/my-app-demo/..."`
    ]
}

before

purs compile --output output \
  .spago/strings/v9.9.9/src/**/*.purs \
  .spago/strings-extra/v9.9.9/src/**/*.purs \
  .spago/other-strings/v9.9.9/src/**/*.purs \
  src/**/*.purs \
  test/**/*.purs

after

purs compile --output output \
  --package @core/strings:.spago/@core/strings/v9.9.9/src \
  --package @contrib/strings-extra:.spago/@contrib/strings-extra/v9.9.9/src \
  --package @noname/other-strings:.spago/@noname/other-strings/v9.9.9/src \
  --package @me/my-app:src \
  --package @me/my-app-test:test \
  --package @me/my-app-bench:bench

  ## no, it's not possible to specify the same package name two times,
  ## because it will make DuplicateModule possible and we end up where we stopped
  # --package @noname/other-strings:.spago/@noname/other-strings/v9.9.9/other-src

  ## no, you cannot define aliases, i.e. import already imported dir
  ## (well, we could allow aliases, but how the ./output dir will be generated?)
  # --package @me/my-utils:src/utils

having

| .spago/
  | @core/strings/v9.9.9/src/
    | index.purs
    | bar.purs -- this file will be imported on `import "strings/bar"`
    | bar/
      | index.purs -- only imported on `import "strings/bar/index"`
      | baz.purs
  | @contrib/strings-extra/v9.9.9/src/
    | index.purs
  | @noname/other-strings/v9.9.9/src/
    | index.purs
| src/
| test/
| bench/

the output dir will be the following

| output/
  | @core/strings/...
  | @contrib/strings-extra/...
  | @noname/other-strings/...
  | @me/my-app/...
  | @me/my-app-test/...
  | @me/my-app-bench/...

-- `module ... where` is replaced with `export` (or maybe just `where`?)
-- because module name is calculated automatically from path
-- `export` is optional (by default exports everything)
export
  ( Foo(..)
  , bar
  )

import "@core/prelude"

-- will import from ".spago/@core/strings/v9.9.9/src/index.purs"
import "@core/strings"

-- will import from ".spago/@contrib/strings-extra/v9.9.9/src/index.purs"
import "@contrib/strings-extra" as StringsExtra
  ( Foo(Bar)
  )

-- will import
-- from ".spago/@noname/other-strings/v9.9.9/src/String.purs"
-- or
-- from ".spago/@noname/other-strings/v9.9.9/src/String/index.purs"
import "@noname/other-strings/String" as OtherString

-- relative imports are possible only within the same package
import "./foo/bar"

-- will import the same file as the relative import above
import "@me/my-app/foo/bar"

So you’re basically saying that you want the compiler to become package-aware, don’t want to write the module declaration, and want the package qualification to be enforced. I agree with the first one, but the second and third one have drawbacks.

Sometimes you don’t want the module path to follow the file path. For example, in most PureScript test modules, they prefix the module path with Test.. But there is no Test directory anywhere! I also think that there might be other scenarios where you might want this. If writing module declarations annoys you so much, I don’t think writing a plugin or extension for your editor which adds a module declaration based on the file path is difficult.

I don’t want to force myself to write package names for every import. It’s also a little hard to read, though not too much. I believe that this should be optional and optionally enforced by your project code style.

Also, let’s not forget that this will break every PureScript codebase in existence. I imagine writing a codemod for that won’t be too difficult though. But tools that work with PureScript code will have to accommodate these changes. I don’t know enough to judge how bad that is.

For example, in most PureScript test modules, they prefix the module path with Test. . But there is no Test directory anywhere!

it would be fine for me to change test/Main.purs to test/Test/Main.purs

I don’t think writing a plugin or extension for your editor which adds a module declaration based on the file path is difficult

but it won’t solve DuplicateModule

I don’t want to force myself to write package names for every import. … I believe that this should be optional

but it’s easier to read and understand code, and solves DuplicateModule

this will break every PureScript codebase in existence

we can write a script that changes module ... where to export ... and make prs for all packages in pursuit, we did this before

But tools that work with PureScript code will have to accommodate these changes.

true. this could be managed as part of updating to purescript v1

Lots of people won’t be fine with it. Don’t forget that’s your opinion doesn’t always apply to others. Now that I think about it, it’s not as bad as I thought for the test case. However, my point still stands that it might have some scenarios. I might change my opinion if actually nobody does this.

That’s true.

Again, that’s not everyone’s opinion.

I did note that we can write a codemod.

1 Like

I think a good middle ground for package qualification is to be able to not write the package name unless there’s ambiguity.

1 Like

I have added polls to the top comment

1 Like

The way the first poll is worded makes it a little confusing.

  • imports should be optionally qualified
  • all imports should be qualified
  • I don't need qualification

A “qualified” import is one that looks like import FooBar as Foo. In Haskell it would look like import qualified FooBar as Foo, which is where “qualified” comes from.

In Haskell, your first proposal is implemented in the PackageImports extension.

The poll makes it sound like you’re talking about qualified imports, when really I think you’re talking about package imports. I answered it as written (about qualified imports), not as I think you are intending it (as package imports). I imagine others might make the same mistake.

it’s still called qualification

With the PackageImports extension, GHC allows import declarations to be qualified by the package name that the module is intended to be imported from

but qualification by package name

will update

1 Like

I completely agree that the fact that modules from different packages can conflict and that it’s impossible to tell what package a module comes from are both serious problems which need addressing. This is why I made the issue https://github.com/purescript/purescript/issues/2437 - we haven’t quite reached an agreement there, the devil is in the details. The migration is admittedly going to be painful, but with the CST parser and printer it hopefully won’t be too difficult to automate.

4 Likes

good

the psc 'src/**/*.purs' --package 'prelude:.psc-package/0.10.5/prelude/src/**/*.purs' is almost what I proposed


and I dreamed about

provide syntax sugar so that import prelude becomes, say, import prelude.Default , and encourage libraries to always provide a Default module

I always hated the directory structure

| Foo/
  | Foo1.purs
  | Foo2.purs
| Bar/
  | Bar1.purs
  | Bar2.purs
| Foo.purs
| Bar.purs

and wanted to write about

| Foo/
  | Foo1.purs
  | Foo2.purs
  | Default.purs
| Bar/
  | Bar1.purs
  | Bar2.purs
  | Default.purs

too, but decided that it would be too much


@hdgarrood, what do you think about module names should be derived from path automatically proposal?

I think the benefit of getting rid of the module Whatever where syntax is less obvious, and is too big a breaking change to be justifiable to me.

2 Likes

After seeing Harry’s issue, I’ve moved to the dark side. If we’re gonna replace the Data prefix and its fellows, I’m all for it. I was originally against it because @srghma’s code sample has a lot of repetition in its imports. So you may want to change your example @srghma.

2 Likes

Make module name derivable from path

Here’s what I currently have in my learning repo:

module Syntax.Basic.Foo where
--

via the file Syntax/src/Basic/01-Learn-Foo.purs

Shifting to your proposal would break my repo’s syntax folder as Syntax.Basic.01-Learn-Foo isn’t a valid module name. If hyphens were dropped and numbers could be included, it would no longer be a problem.

1 Like

I want to also add this post that saw about 4 months ago complaining about package, and I do agree with what the author’s argument here. I also like a lot of some of the points that @hdgarrood pointed in his github issue.

Looking at the issue, there seems to be a solution for most cases except for some possible edge cases that we might be missing. Now, I feel there is a bit resistance when it comes to aesthetic and also not wanting to be aware of the package when importing modules.

Personally, I think that the voting system that @srghma is proposing is good first step. If we don’t vote on it will just be another floating issue. I think that it’s important that we agree about the package system earlier on before 1.0 - whether you like or not its important that it’s clear early on.

Would it not be better if we collect all the points in this post and then create a separate post for votes? Of course, the voting post would display all the points.

1 Like

Wow, actually I was thinking too why not enforce domain for packages (e.g. @core/prelude), and right, there is a Java that do this

Updated proposal

Will do polls later

moved proposal to the top

added polls The only thing I hate in purescript and haskell - imports (polls), tell if you have more poll points

1 Like

I think we can get most of the benefit of package-qualified imports by requiring, that published packages have the top-level module be the package name.

Data.Functor from purescript-prelude would become Prelude.Data.Functor
etc.

Some packages already do this:
Affjax.RequestBody from purescript-affjax is already Affjax.RequestBody

That way we don’t need to break all purescript code, but still get the benefits

We started on this path already, although admittedly we didn’t go very far down it - this is why Effect lives in a module just called Effect rather than Control.Monad.Effect or whatever. I am not such a fan of this approach though, because it doesn’t solve the issue as neatly, but it would still be more or less equally big a breaking change as changing the language - if the majority of the libraries you depend on have breaking changes (renaming a module is a breaking change) that can be just as disruptive as the language itself having a breaking change.

The problem with a rule about translating package names to module names is that the translation isn’t always obvious and we would need tooling to enforce the convention if it was going to catch on. For example, a module in the strings-extra should be StringsExtra.Whatever, not Strings.Extra.Whatever, but the latter style is already common for extending an existing library, so I’m sure people would keep using that style unless we had tooling telling them not to.

1 Like

@srghma, it sounds like you’re in hurry with this. Why are you? If you like this solved then just starting discussion like this is kind of a great thing and you’re welcome to do so. Though you could relax a bit and let us (and maybe help us) find the best way to do a module system that we know of at the moment.

3 Likes