Should we add Namespaces to Haskell?

Hi All,

I have been following the Advent-of-Code using Rust, and one thing that really hit me is how well integrated the namespace system is compared to the one we have in Haskell. So, I was thinking about how namespaces could be implemented in Haskell, and I’m not sure it’s good enough to create a GHC proposal, so I would like your feedback.

First, why not just use modules? Well, modules are currently being tied to specific files, and often we want our namespaces to be more particular.

I’m going to use the X::a notation to indicate namespaces, we can differentiate it from types in the same way as we do for modules M.a. I guess, this will be controversial.

We can add functions and variables to namespace like so:

namespace Entry where
  empty :: Entry
  empty = ...
  toText :: Entry -> Text
  toText = ...

To use a namespace variable, you can refer to it directly:

main = do
  putStrLn (Entry::toText Entry::empty)

or you can open it:

main = do
  use Entry::*
  putStrLn (toText empty)

which is equivalent to:

main = do
  let toText = Entry::toText
      empty = Entry::empty
  putStrLn (toText empty)

*Benefits using namespaces, … *

  • we can make tooling give better code predictions. For example, if we type Entry:: the IDE would know there is only two interesting functions to report.

  • we could prevent name collisions on records in a structured way. In another extension, we could make record functions be automatically assigned to the namespace of the record.

    data Entry = Entry { date :: Date, mesg :: Text }
    data Error = Error { mesg :: Text }
    --- We can differentiate between them with 
    toTextBoth entry error = Entry::mesg entry <> Error::mesg error
    
  • we could encapsulate free orphan instances: an instance is only considered if the name space is open.

    namespace Danger where
       instance Show Bool where
         show True = "False"
         show False = "True"
    

    If you then need this orphan instance, you can open the namespace, as we do with modules, however, we can limit the scope and namespaces do not inherit each others variables.

    main = do
      do
        use Danger::()
        print True -- prints False
      print True -- prints True, again
    
  • we could add tests in the same module as our code, but not pollute the namespace.

    add :: Int -> Int -> Int
    add a b = a + b
    
    namespace Test where
      addShouldBeCommuative a b = 
         add a b === add b a
    
    

Questions

  • Should namespaces be open, so that you can redefine names of spaces later in the file or in other modules?

  • Should namespaces be nested, so that you can define sub namespaces?

I’m sure how hard it would be to introduce this to GHC, some of the changes seems like it’s only about names, which could be fixed with a weird naming scheme `, others are a little more involved.

So could/should we add namespaces to Haskell?

1 Like

Have you seen the Local modules GHC proposal? It is an interesting idea, but there are a lot of small details that need to be worked out.

Also, namespaces wouldn’t fix orphans. Imagine this code:

data X = A | B deriving (Show, Eq)

namespace O1 where
  instance Ord X where
    A <= _ = True
    x <= y = x == y

namespace O2 where
  instance Ord X where
    B <= _ = True
    x <= y = x == y

main = do
   let myset = use O1::() in Set.fromList [A,B]
   let x = use O2::() in Set.findMin myset
   print x

Will that print A or B?

Being able to locally use different instances breaks type class coherence and means among other things that you cannot implement polymorphic sets using balanced trees.

2 Likes

Thank you! Why didn’t I think of searching for “Local Modules”! I’ll definitely take a look.

About your example, I can see how it breaks type-class coherence, but maybe that is not preferable to have type-class coherence?
And maybe Set’s should make this dependence explicit, though I feel like that might be another discussion:

data Set o a = ...
fromList :: o ~ Ord a => [a] -> Set o a
findMin :: o ~ Ord a => Set o a -> a

Now ,your example would look something like this:

main do 
   let myset = 
          use O1::() 
          in Set.fromList [A,B]
   -- Error: ~~~~~~~~~~~~~~~~~~ The use of fromList binds (Ord O1), 
   --                           but this escapes it's scope.
   let x = use O2::() in Set.findMin myset
   print x

Anyway, thank you for pointing me in the right direction.

I see it’s also somewhat related to Add support for local types, GHC proposal, for anybody interested.

1 Like

Hi @kalhauge, good to see you’re thinking critically about Haskell!

Not only controversial, but unworkable:

is valid syntax for Entry being a data constructor, with toText being a type variable in scope, the space separator being toText applied to type Entry; so :: is giving a type annotation. (You’ll then get a parse error at the second :: – or that might be taken as a type-of-type aka Kind annotation; but it’s not reasonable to expect the compiler to backtrack and figure out what you’re doing with ::.)

Indeed. ‘Local Instances’ gets proposed on a regular basis – it’s as if there’s a community memory-loss of the reasons why it can’t work. (Note that ‘Local modules’ GHC proposal isn’t proposing local class instances – for the reasons @jaror points out. It is proposing data and newtype instances – we already have those for data families.)

Yeah. Note “absurd” and questions of “coherent” appear in that proposal or comments to it. I think that proposal is barking-mad. But even so, it isn’t proposing local instances in the sense you have here: rather, its instances are global, merely textually declared locally where the local type is in scope.

We already have namespaces in the sense of Module prefixes. I think that’s the mechanism to build on. But remember no amount of qualified importing hides the underlying types/classes/instances: it might hide the type names or data constructors, to hold them abstract in the importing module.

1 Like

Is IMO bad coding style. I’d like to insist on NoFieldSelectors everywhere. Prefer

toTextBoth (Entry{ nMesg=mesg }) (Error{ rMesg=mesg }) = nMesg <> rMesg

I agree that proliferation of brackets is ugly, but that’s merely a question of lexical syntax. And the syntax with { } trailing whatever it applies to is dysergonomic for many reasons, including being hard for humans to parse. Could be

toTextBoth {Entry | nMesg=mesg } {Error | rMesg=mesg } = nMesg <> rMesg

I think there is nothing controversial about GHC ability to parse it. It would parse in the same way as: putStrLn (Entry.toText Entry.empty) which is different from putStrLn (Entry . toText Entry . empty), so not unworkable.

We already have namespaces in the sense of Module prefixes. I think that’s the mechanism to build on.

Yep, that seems like the way “Local modules” is going. Too bad its dormant.

1 Like

Okay, let’s look away from instances and the :: notation, as they seem like a distraction.

It seems like everybody agrees that First class Modules GHC 295 is the best solution, but will introduce too many breakages. Maybe differentiation between modules and namespaces could be a solution?

  • Introduction

    • (Form 1), introducing a new namespace called X, with names y, and possible exports (…):
      space X [(..)] where
         y = 0
      
    • (Form 2), introducing a namespace with default value Y:
      space X = Y [(..)] where
         type Y = Bool
      
    • (Form 3), introducing a namespace when importing it
      import Module.Name named as X;
      
  • Elimination

    X.y -- Usage.
    X -- Usage if X has a default value.
    using X [(..)] -- Expansion into current namespace.
    
  • Import / Export: Namespaces are entities and are just exported like that; possible they can be renamed at export.

    module M (X [as Z]) where
    ...
    
    import M (X) -- namespace X is in scope
    import qualified M -- namespace X is named M.X
    

Now for some interesting examples:

Set

module Data.Set (Set) where

space Set = Set (fromList, Set) where
   data Set = ...
   fromList :: [a] -> Set a

Usage site:

import Data.Set (Set)

myFromList :: [a] -> Set a -- using the default name of Set = Set.Set
myFromList = Set.fromList -- using the list of name.

Records

Of cause this could make all entities namespaces.

data Entry { data :: Text }
-- could be interpreted as
space Entry = Entry (Entry, data) where
   data Entry { data :: Text } -- ^ Old semantics.

Merge

It’s illegal to merge namespaces, but we can create new ones:

module MySet (Set) where

import Data.Set named as OldSet -- creates a namespace OldSet
import Data.Set.Extra named as SetExtra -- creates a namespace SetExtra

space Set where
   using OldSet
   using SetExtra

The module system, with its lexical dottiness has been part of Haskell since well before 1998; so there’s special lexical handling for whether . has space and upper-case tokens around it. No such special handling for ::, so non-space surrounded :: currently has exactly the same syntactic interpretation as if there were spaces. (Also :: is not an operator.) As you say, invalidating a large number of appearances in current programs would be a distraction.

No, read those comments. Especially this one from the the proposal’s own author

Before proposing Haskell mindlessly follow Rust (or Scala for that matter), I suggest you draft what would be the Motivation section for your proposal. (I don’t think that proposal’s Motivation really adequately states the problem it’s addressing. It’s one of those topics ‘everybody just knows’ what is wrong – until you write it down, and discover each body ‘just knows’ something different.)

Also I’m unconvinced that field labels are a symptom of the same namespacing difficulty; and we now have a lot of extensions that combine to mean at least same-naming in the data decls isn’t so awkward. That is, it’s the usage sites that present more difficulties.

1 Like

Or Agda! I think their module system is pretty cool. It also differentiates modules and namespaces.

1 Like

Heh, heh. We could follow purescript following javascript for that bit. Or follow Hugs.

Good idea :slight_smile:

I think the key word is “incremental”. I believe that problem is in that the current suggestions what to change the module system. Adding a new system for controlling namespaces would not change the current semantics (in all its gore) of the module system.

By having a functioning namespace system I could see people migrating away from modules over time.

I was not trying to find a new solution to records, just showing that namespaces can be useful. The namespaces would also extend to constructors and class members :).

Thank you, great reference! I’ll take a look :).

Okay,

I have tried to make a draft, the motivation is mostly examples but I think they are relatively clear.

here

I know that some of the syntax and solutions are not perfect, but the perfect is the worst enemy of the good enough.

1 Like
let {just = Just; nil = []} in
   (Nothing:just True:nil)

-- ===> [Nothing,Just True]
  • Using (:) as a prefix is even more terrible than ::. It’s very common to put : without surrounding spaces. (Are you even trying?)

  • You want to allow namespaces named same as the artefacts they contain? Are you sure, when Haskell is moving away from punning? And can they be same-named as modules?

It’s fine to include examples in the Motivation: they should be examples of currently valid code, and why it can’t express something you want to do (or can only express it awkwardly/ambiguously). When I said

I expected your Motivation to include a ‘problem statement’. Because until we know what you think the problem is, we can’t decide whether we find it a problem, and we certainly can’t assess whether the proposal is a solution.

One problem you do identify, but then just leave on the table:

Many prior proposals have tried to come up with a solution … they have all gone dormant.

The ‘problem’ you seem to be addressing is: Haskell is not Rust/not other languages with namespaces. Which I assess as: meh.

I’m sure that most of the code you write has spaces between the arguments, and this extension would only be active in code, where you have activated it. You could just write
Nothing : just True : nil would still work. I changed to : because I think you are right that A::B looks too much like A :: B, I choose : because it is a reserved operator in Haskell, so the effects are known, compared to a random operator.

They will be able to refer to entities, that they contain. The namespace Set might mean Set:Set if used as a type. I think this is orthogonal to punning; but if you see a concrete problem, please let me know!

I guess, that the problem statement is, I would like to write code like the examples. You can also consult the two other proposals to see how many proposals that eventually would be solved by introducing namespaces.

But let me ask you, how would you write the code for example 1? (or any of the others).

I think the problem is that Haskell is too much like Java :). One file per feature.

After saying namespace proposals aren’t specifically about duplicate field names, your first example is of duplicate field names. I would go:

{-# LANGUAGE  DuplicateRecordFields,  NoFieldSelectors  #-}

module Pretty where

  class CPretty a  where pretty :: a -> Text

  data V2 = Mk2 { x :: Int, y :: Int }
  instance CPretty V2  where pretty (Mk2{ x, y }) = ...

  data V3 = Mk3 { x :: Int, y :: Int, z :: Int }
  instance CPretty V3 where pretty (Mk3{ x, y, z }) = ...

  main = print (pretty (Mk2 0 1) <> pretty (Mk3 0 1 0)) 

DuplicateRecordFields is there to allow data decls with same-named fields; not to use any of the type-directed disambiguation.

NoFieldSelectors is there to make sure I can’t even try to access via selector name.

Using namespaces to try to wrangle types will certainly come to grief in more complex cases – as @jaror already pointed out at “won’t fix orphans”. Types (and all the names they introduce) must be global, for type coherence.

Then the solution for you is easy: go away and write in Rust.

One of the proposals claims “compilation units = modules = files” – that’s in the discussion/not quoting the author. The discussion is also concerned about Template Haskell, where certainly it’s a royal pain that you need to compile the module/file declaring the TH functions before you can compile the module/files that use them. (And if one TH decl wants to use another, you must compile that first; it rapidly gets ugly.) I’m not seeing your proposal is tackling TH(?)

I think that equation is not right. Rather: 1 compilation unit = 1+many files = 1+many+few modules.

Where the 1 file is the immediate source you give to the compiler; the many are the files it imports.

The 1 module is per the 1 file; the many modules are the imports; the few extra are the modules getting imported twice as unqualified or with multiple qualifiers.

Most of those imports are libraries, including core libraries/about which I can do nothing. So any fancy business with modules/namespaces for my code has relatively small benefit. Balanced against the proposals for namespacing look like a lot of work and/or disruption. That I think is why proposals keep going dormant.

Addit: The ‘Local modules’ #283’s idea of module MyPrelude (...) where ... with a standard set of module aliases across my project seems appealing, until …

Somebody else is importing my project, and mashing it together with A.N.Other project – which it turns out also has a standard set of modules with a different bunch of ‘standard’ names that resolve to many of the same libraries. My.Set turns out to be ANOs.Collection. Did we simplify anybody’s life?

I get the point that copy/pasting a bunch of imports at the top of every file in a project is tedious, and liable to get out of synch. Isn’t that what #includes are for?

Regardless of what you think of the technical content of the proposal, I don’t see how this kind of unfriendly comment helps anything.

10 Likes

I think there are parts of the proposal which are interesting, but overall it just add syntactic or semantic overhead (introducing new symbols or given new meanings to current symbols) with very little benefit in exchange.

Example 1 Can be solved

  • With two modules Pretty.V2 and Pretty.V3 and importing them qualified as V2 and V2. This is pointed in the proposal, and apparently is a down point for the author. I don’t see any particular benefit in having all in one file honestly.
  • with a type class which I agree is not the same as a namespace and polutes function types (this is pointed in the proposal too)
  • If you want to have all in same module without type classes, you can simulate them with the new Record syntax and some added verbosity, but nothing too crazy. Argubly, this is not scalable solution for a very big name space, but It guarantees v2 and v3 will always have the same functions on it.
-- langs extensions I can't use right now, but probably TypeFamilies, RecordDotSyntax? 
-- In Vecs.hs
module  Vecs(v2, v3) where

type family Dimenson p :: Type

data Dim2 = Dim2 {x :: Int, y :: Int}
type instance Dimenson Dim2 = Int -> Int -> Dim2

data Dim3 = Dim3 {x :: Int, y :: Int, z :: Int}
type instance Dimenson Dim3 = Int -> Int -> Int -> Dim3

data Vec p = Vec
  { mk :: Dimenson p
  , pretty :: p -> String
  }

-- This is your name space for v2
v2 :: Vec Dim2 
v2 = Vec { mk = Dim2 
         , pretty = \(Dim2 x y) -> "(" ++ intercalate "," (fmap show [x, y]) ++ ")"}

-- This is your name space for v3
v3 :: Vec Dim3
v3 = Vec { mk = Dim3 
         , pretty = \(Dim3 x y z) -> "(" ++ intercalate "," (fmap show [x, y, z]) ++ ")"}

-- In Main.hs

module Main where

import Vecs (v2, v3) 

v2.pretty (v2.mk 2 3)
v3.pretty (v3.mk 2 3 4)

Example 2 simply looks a bad idea to me. Isn’t it the same as

import Data.Set (Set)
import Data.Set qualified as Set

The problem with your approach is that Set implicitly becomes two different things, the namespace and the type, which is exactly what happens in the current situation but explicitly and without new syntax. I think you’ve exchanged one less import (import Data.Set qualified as Set) for few more syntax

Example 3 I think is a good Idea, but I’d go against library authors having the options to do so at the risk of running import SomeLibrary and polute my namespace with whatever the library author considers to be “good modules to import”. Yes, wildcard imports are a bad practice but I don’t see too much benefit between

-- your proposal
import MyPrelude (BL, BS)

-- Current
import Data.ByteString.Lazy qualified as BL
import Data.ByteString.Strict qualified as BS

The only advantage, an important one to me, is to being able to define a file with standard qualified names so every file in the project imports Data.ByteString.Strict with name BS. I don’t know if this can be achive with cabal and mixins already. This is not a language feature in my opinion, but more of a good practise to follow and which should be encourage by the tools.

Example 4. Same as before, does It realy matters to use local type definitions?? If you have to use a type within only one function, why making such a type in the first place?

As a summary, to me this proposal adds new syntax, new semantics, new extension/s, new edge cases and more complexity in general. The benefits seems to serve only personal (argubly for quite a few persons) preferences in the style of writting (personally, I wouldn’t use namespaces because the syntax wouldn’t be as easy as Module.function).

Nevertheless It is always good to read about evolving the language :slight_smile: